From 5254b402c0796083aa50ad5cfa6bed207bd936e5 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 31 Dec 2025 14:36:06 +0900 Subject: [PATCH 1/3] fix: replace prohibited 'as' type assertions with type guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all 'as' type assertions in production code with proper type guards using the 'in' operator or type narrowing, following the coding style guidelines in .agent/rules/coding-style.md. Changes: - packages/api-client/src/fetcher.ts: Use type guards for error body validation - packages/providers/core/src/base.ts: Remove unnecessary type assertion - packages/providers/anthropic/src/skills.ts: Add runtime validation for JSON.parse - packages/react/src/utils/group-by-run.ts: Use discriminated union type narrowing - packages/react/src/hooks/use-run.ts: Add type guards in switch cases - packages/react/src/hooks/use-runtime.ts: Add type guards in switch cases Closes #325 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- packages/api-client/src/fetcher.ts | 12 +++-- packages/providers/anthropic/src/skills.ts | 6 ++- packages/providers/core/src/base.ts | 2 +- packages/react/src/hooks/use-run.ts | 12 +++-- packages/react/src/hooks/use-runtime.ts | 58 +++++++++++----------- packages/react/src/utils/group-by-run.ts | 9 ++-- 6 files changed, 55 insertions(+), 44 deletions(-) diff --git a/packages/api-client/src/fetcher.ts b/packages/api-client/src/fetcher.ts index d93ff789..c45fb7f3 100644 --- a/packages/api-client/src/fetcher.ts +++ b/packages/api-client/src/fetcher.ts @@ -33,12 +33,16 @@ function createNetworkError(error: unknown): ApiError { } function createHttpError(status: number, statusText: string, body?: unknown): ApiError { - if (typeof body === "object" && body !== null) { - const errorBody = body as Record + if ( + typeof body === "object" && + body !== null && + "error" in body && + typeof body.error === "string" + ) { return { code: status, - message: (errorBody.error as string) ?? statusText, - reason: errorBody.reason, + message: body.error ?? statusText, + reason: "reason" in body ? body.reason : undefined, } } return { diff --git a/packages/providers/anthropic/src/skills.ts b/packages/providers/anthropic/src/skills.ts index d4dc1942..e753651e 100644 --- a/packages/providers/anthropic/src/skills.ts +++ b/packages/providers/anthropic/src/skills.ts @@ -24,10 +24,14 @@ function convertSkill(skill: AnthropicProviderSkill): AnthropicSkillConfig { } } try { + const parsed = JSON.parse(skill.definition) + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("Definition must be a JSON object") + } return { type: "custom", name: skill.name, - mcp_config: JSON.parse(skill.definition) as Record, + mcp_config: parsed, } } catch (error) { throw new Error( diff --git a/packages/providers/core/src/base.ts b/packages/providers/core/src/base.ts index 56271101..b79607f0 100644 --- a/packages/providers/core/src/base.ts +++ b/packages/providers/core/src/base.ts @@ -71,7 +71,7 @@ export abstract class BaseProviderAdapter implements ProviderAdapter { protected createProxyFetch(proxyUrl: string): typeof globalThis.fetch { const agent = new ProxyAgent(proxyUrl) return (input, init) => { - return undiciFetch(input, { ...init, dispatcher: agent }) as Promise + return undiciFetch(input, { ...init, dispatcher: agent }) } } } diff --git a/packages/react/src/hooks/use-run.ts b/packages/react/src/hooks/use-run.ts index 15cb9960..f35de3df 100644 --- a/packages/react/src/hooks/use-run.ts +++ b/packages/react/src/hooks/use-run.ts @@ -51,7 +51,9 @@ export function processStreamingEvent( } case "streamReasoning": { - const e = event as StreamingEvent & { type: "streamReasoning" } + if (event.type !== "streamReasoning") { + return { newState: prevState, handled: false } + } return { newState: { ...prevState, @@ -60,7 +62,7 @@ export function processStreamingEvent( [runId]: { ...prevState.runs[runId], expertKey, - reasoning: (prevState.runs[runId]?.reasoning ?? "") + e.delta, + reasoning: (prevState.runs[runId]?.reasoning ?? "") + event.delta, }, }, }, @@ -105,7 +107,9 @@ export function processStreamingEvent( } case "streamRunResult": { - const e = event as StreamingEvent & { type: "streamRunResult" } + if (event.type !== "streamRunResult") { + return { newState: prevState, handled: false } + } return { newState: { ...prevState, @@ -114,7 +118,7 @@ export function processStreamingEvent( [runId]: { ...prevState.runs[runId], expertKey, - runResult: (prevState.runs[runId]?.runResult ?? "") + e.delta, + runResult: (prevState.runs[runId]?.runResult ?? "") + event.delta, }, }, }, diff --git a/packages/react/src/hooks/use-runtime.ts b/packages/react/src/hooks/use-runtime.ts index 2f2cf224..e78ab4c7 100644 --- a/packages/react/src/hooks/use-runtime.ts +++ b/packages/react/src/hooks/use-runtime.ts @@ -38,36 +38,36 @@ export function useRuntime(): RuntimeResult { switch (event.type) { case "initializeRuntime": { - const e = event as RuntimeEvent & { type: "initializeRuntime" } + if (event.type !== "initializeRuntime") return false setRuntimeState((prev) => ({ ...prev, - query: e.query, - expertName: e.expertName, - model: e.model, - runtime: e.runtime, - runtimeVersion: e.runtimeVersion, + query: event.query, + expertName: event.expertName, + model: event.model, + runtime: event.runtime, + runtimeVersion: event.runtimeVersion, })) return true } case "skillStarting": { - const e = event as RuntimeEvent & { type: "skillStarting" } + if (event.type !== "skillStarting") return false setRuntimeState((prev) => { const skills = new Map(prev.skills) - skills.set(e.skillName, { name: e.skillName, status: "starting" }) + skills.set(event.skillName, { name: event.skillName, status: "starting" }) return { ...prev, skills } }) return true } case "skillConnected": { - const e = event as RuntimeEvent & { type: "skillConnected" } + if (event.type !== "skillConnected") return false setRuntimeState((prev) => { const skills = new Map(prev.skills) - skills.set(e.skillName, { - name: e.skillName, + skills.set(event.skillName, { + name: event.skillName, status: "connected", - serverInfo: e.serverInfo, + serverInfo: event.serverInfo, }) return { ...prev, skills } }) @@ -75,10 +75,10 @@ export function useRuntime(): RuntimeResult { } case "skillDisconnected": { - const e = event as RuntimeEvent & { type: "skillDisconnected" } + if (event.type !== "skillDisconnected") return false setRuntimeState((prev) => { const skills = new Map(prev.skills) - skills.set(e.skillName, { name: e.skillName, status: "disconnected" }) + skills.set(event.skillName, { name: event.skillName, status: "disconnected" }) return { ...prev, skills } }) return true @@ -90,27 +90,27 @@ export function useRuntime(): RuntimeResult { return true case "dockerBuildProgress": { - const e = event as RuntimeEvent & { type: "dockerBuildProgress" } + if (event.type !== "dockerBuildProgress") return false setRuntimeState((prev) => ({ ...prev, dockerBuild: { - stage: e.stage, - service: e.service, - message: e.message, - progress: e.progress, + stage: event.stage, + service: event.service, + message: event.message, + progress: event.progress, }, })) return true } case "dockerContainerStatus": { - const e = event as RuntimeEvent & { type: "dockerContainerStatus" } + if (event.type !== "dockerContainerStatus") return false setRuntimeState((prev) => { const dockerContainers = new Map(prev.dockerContainers) - dockerContainers.set(e.service, { - status: e.status, - service: e.service, - message: e.message, + dockerContainers.set(event.service, { + status: event.status, + service: event.service, + message: event.message, }) return { ...prev, dockerContainers } }) @@ -118,14 +118,14 @@ export function useRuntime(): RuntimeResult { } case "proxyAccess": { - const e = event as RuntimeEvent & { type: "proxyAccess" } + if (event.type !== "proxyAccess") return false setRuntimeState((prev) => ({ ...prev, proxyAccess: { - action: e.action, - domain: e.domain, - port: e.port, - reason: e.reason, + action: event.action, + domain: event.domain, + port: event.port, + reason: event.reason, }, })) return true diff --git a/packages/react/src/utils/group-by-run.ts b/packages/react/src/utils/group-by-run.ts index c03c839e..4606f932 100644 --- a/packages/react/src/utils/group-by-run.ts +++ b/packages/react/src/utils/group-by-run.ts @@ -1,4 +1,4 @@ -import type { ActivityOrGroup, ParallelActivitiesGroup } from "@perstack/core" +import type { ActivityOrGroup } from "@perstack/core" /** * Represents a group of activities belonging to the same run @@ -22,11 +22,10 @@ function getActivityProps(activityOrGroup: ActivityOrGroup): { delegatedBy?: { expertKey: string; runId: string } } { if (activityOrGroup.type === "parallelGroup") { - const group = activityOrGroup as ParallelActivitiesGroup - const firstActivity = group.activities[0] + const firstActivity = activityOrGroup.activities[0] return { - runId: group.runId, - expertKey: group.expertKey, + runId: activityOrGroup.runId, + expertKey: activityOrGroup.expertKey, delegatedBy: firstActivity?.delegatedBy, } } From cc30a2cfcf0d6e8f7c7538789aedfeae53784577 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 31 Dec 2025 21:05:23 +0900 Subject: [PATCH 2/3] fix: address bugbot feedback and add test coverage --- .changeset/fix-type-assertions.md | 9 ++ packages/api-client/src/fetcher.test.ts | 38 ++++++ packages/api-client/src/fetcher.ts | 29 ++-- .../providers/anthropic/src/skills.test.ts | 39 ++++++ packages/react/src/utils/group-by-run.test.ts | 128 ++++++++++++++++++ 5 files changed, 233 insertions(+), 10 deletions(-) create mode 100644 .changeset/fix-type-assertions.md create mode 100644 packages/react/src/utils/group-by-run.test.ts diff --git a/.changeset/fix-type-assertions.md b/.changeset/fix-type-assertions.md new file mode 100644 index 00000000..382d117e --- /dev/null +++ b/.changeset/fix-type-assertions.md @@ -0,0 +1,9 @@ +--- +"@perstack/api-client": patch +"@perstack/anthropic-provider": patch +"@perstack/provider-core": patch +"@perstack/react": patch +--- + +Fixed prohibited 'as' type assertions by replacing them with proper type guards + diff --git a/packages/api-client/src/fetcher.test.ts b/packages/api-client/src/fetcher.test.ts index 6b350dbc..b20d09e4 100644 --- a/packages/api-client/src/fetcher.test.ts +++ b/packages/api-client/src/fetcher.test.ts @@ -131,6 +131,44 @@ describe("fetcher.get", () => { } }) + it("returns error with reason when body has reason but no error field", async () => { + server.use( + http.get(`${BASE_URL}/api/test`, () => { + return HttpResponse.json({ reason: "Detailed reason without error" }, { status: 400 }) + }), + ) + + const fetcher = createFetcher() + const schema = z.object({ id: z.number() }) + const result = await fetcher.get("/api/test", schema) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.error.code).toBe(400) + expect(result.error.message).toBe("Bad Request") + expect(result.error.reason).toBe("Detailed reason without error") + } + }) + + it("returns error with statusText when body has non-string error field", async () => { + server.use( + http.get(`${BASE_URL}/api/test`, () => { + return HttpResponse.json({ error: 123, reason: "Some reason" }, { status: 422 }) + }), + ) + + const fetcher = createFetcher() + const schema = z.object({ id: z.number() }) + const result = await fetcher.get("/api/test", schema) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.error.code).toBe(422) + expect(result.error.message).toBe("Unprocessable Entity") + expect(result.error.reason).toBe("Some reason") + } + }) + it("returns validation error on schema mismatch", async () => { server.use( http.get(`${BASE_URL}/api/test`, () => { diff --git a/packages/api-client/src/fetcher.ts b/packages/api-client/src/fetcher.ts index c45fb7f3..c76700d4 100644 --- a/packages/api-client/src/fetcher.ts +++ b/packages/api-client/src/fetcher.ts @@ -33,16 +33,25 @@ function createNetworkError(error: unknown): ApiError { } function createHttpError(status: number, statusText: string, body?: unknown): ApiError { - if ( - typeof body === "object" && - body !== null && - "error" in body && - typeof body.error === "string" - ) { - return { - code: status, - message: body.error ?? statusText, - reason: "reason" in body ? body.reason : undefined, + if (typeof body === "object" && body !== null) { + const hasReason = "reason" in body + + if ("error" in body && typeof body.error === "string") { + const errorMessage: string = body.error + return { + code: status, + message: errorMessage, + reason: hasReason ? body.reason : undefined, + } + } + + // Preserve reason even if error field is missing or not a string + if (hasReason) { + return { + code: status, + message: statusText, + reason: body.reason, + } } } return { diff --git a/packages/providers/anthropic/src/skills.test.ts b/packages/providers/anthropic/src/skills.test.ts index 01339db2..1dab009c 100644 --- a/packages/providers/anthropic/src/skills.test.ts +++ b/packages/providers/anthropic/src/skills.test.ts @@ -61,6 +61,45 @@ describe("buildProviderOptions", () => { /Invalid JSON in custom skill definition for "bad-tool"/, ) }) + + it("throws error for custom skill with JSON array definition", () => { + const skills: AnthropicProviderSkill[] = [ + { + type: "custom", + name: "array-tool", + definition: '["item1", "item2"]', + }, + ] + expect(() => buildProviderOptions(skills)).toThrow( + /Invalid JSON in custom skill definition for "array-tool"/, + ) + }) + + it("throws error for custom skill with JSON primitive definition", () => { + const skills: AnthropicProviderSkill[] = [ + { + type: "custom", + name: "primitive-tool", + definition: '"just a string"', + }, + ] + expect(() => buildProviderOptions(skills)).toThrow( + /Invalid JSON in custom skill definition for "primitive-tool"/, + ) + }) + + it("throws error for custom skill with JSON null definition", () => { + const skills: AnthropicProviderSkill[] = [ + { + type: "custom", + name: "null-tool", + definition: "null", + }, + ] + expect(() => buildProviderOptions(skills)).toThrow( + /Invalid JSON in custom skill definition for "null-tool"/, + ) + }) }) describe("hasCustomProviderSkills", () => { diff --git a/packages/react/src/utils/group-by-run.test.ts b/packages/react/src/utils/group-by-run.test.ts new file mode 100644 index 00000000..28ef8402 --- /dev/null +++ b/packages/react/src/utils/group-by-run.test.ts @@ -0,0 +1,128 @@ +import type { Activity, ActivityOrGroup, ParallelActivitiesGroup } from "@perstack/core" +import { describe, expect, it } from "vitest" +import { groupActivitiesByRun } from "./group-by-run.js" + +function createActivity(overrides: Partial = {}): Activity { + return { + type: "complete", + id: "a-1", + runId: "run-1", + expertKey: "test-expert@1.0.0", + text: "Completed", + ...overrides, + } as Activity +} + +function createParallelGroup( + overrides: Partial = {}, +): ParallelActivitiesGroup { + return { + type: "parallelGroup", + id: "pg-1", + runId: "run-1", + expertKey: "test-expert@1.0.0", + activities: [createActivity()], + ...overrides, + } +} + +describe("groupActivitiesByRun", () => { + it("groups activities by runId", () => { + const activities: ActivityOrGroup[] = [ + createActivity({ id: "a-1", runId: "run-1" }), + createActivity({ id: "a-2", runId: "run-1" }), + createActivity({ id: "a-3", runId: "run-2" }), + ] + + const result = groupActivitiesByRun(activities) + + expect(result).toHaveLength(2) + expect(result[0].runId).toBe("run-1") + expect(result[0].activities).toHaveLength(2) + expect(result[1].runId).toBe("run-2") + expect(result[1].activities).toHaveLength(1) + }) + + it("preserves order of first appearance", () => { + const activities: ActivityOrGroup[] = [ + createActivity({ id: "a-1", runId: "run-2" }), + createActivity({ id: "a-2", runId: "run-1" }), + createActivity({ id: "a-3", runId: "run-2" }), + ] + + const result = groupActivitiesByRun(activities) + + expect(result[0].runId).toBe("run-2") + expect(result[1].runId).toBe("run-1") + }) + + it("handles parallelGroup activities", () => { + const activities: ActivityOrGroup[] = [ + createParallelGroup({ + id: "pg-1", + runId: "run-1", + expertKey: "expert-a@1.0.0", + activities: [createActivity({ id: "a-1", runId: "run-1", expertKey: "expert-a@1.0.0" })], + }), + ] + + const result = groupActivitiesByRun(activities) + + expect(result).toHaveLength(1) + expect(result[0].runId).toBe("run-1") + expect(result[0].expertKey).toBe("expert-a@1.0.0") + expect(result[0].activities).toHaveLength(1) + expect(result[0].activities[0].type).toBe("parallelGroup") + }) + + it("extracts delegatedBy from parallelGroup first activity", () => { + const activities: ActivityOrGroup[] = [ + createParallelGroup({ + id: "pg-1", + runId: "run-1", + expertKey: "delegated-expert@1.0.0", + activities: [ + createActivity({ + id: "a-1", + runId: "run-1", + expertKey: "delegated-expert@1.0.0", + delegatedBy: { expertKey: "parent-expert@1.0.0", runId: "run-parent" }, + }), + ], + }), + ] + + const result = groupActivitiesByRun(activities) + + expect(result).toHaveLength(1) + expect(result[0].delegatedBy).toEqual({ + expertKey: "parent-expert@1.0.0", + runId: "run-parent", + }) + }) + + it("handles empty activities array", () => { + const result = groupActivitiesByRun([]) + expect(result).toEqual([]) + }) + + it("handles mixed regular and parallelGroup activities", () => { + const activities: ActivityOrGroup[] = [ + createActivity({ id: "a-1", runId: "run-1", expertKey: "expert-a@1.0.0" }), + createParallelGroup({ + id: "pg-1", + runId: "run-1", + expertKey: "expert-a@1.0.0", + activities: [createActivity({ id: "a-2", runId: "run-1", expertKey: "expert-a@1.0.0" })], + }), + createActivity({ id: "a-3", runId: "run-2", expertKey: "expert-b@1.0.0" }), + ] + + const result = groupActivitiesByRun(activities) + + expect(result).toHaveLength(2) + expect(result[0].activities).toHaveLength(2) + expect(result[0].activities[0].type).toBe("complete") + expect(result[0].activities[1].type).toBe("parallelGroup") + }) +}) From 0d6d809a51f66d2dcef3015f7fcc6e8fe9a999dc Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Sat, 10 Jan 2026 01:01:54 +0900 Subject: [PATCH 3/3] refactor(ci): parallelize CI jobs for faster execution - Split quality job into parallel jobs: lint, typecheck, check-deps, validate-versions, validate-changeset, check-schema-diff - Remove ci-success aggregation job (unnecessary) - Delete issue-bot.yml (to be recreated later) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ci.yml | 139 ++++++++++++++++++++++++++------ .github/workflows/issue-bot.yml | 50 ------------ 2 files changed, 114 insertions(+), 75 deletions(-) delete mode 100644 .github/workflows/issue-bot.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a29cf7a..3009d093 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,13 +14,13 @@ permissions: contents: read env: - CI: 'true' - PNPM_VERSION: '10.10.0' - NODE_VERSION: '22' + CI: "true" + PNPM_VERSION: "10.10.0" + NODE_VERSION: "22" jobs: - quality: - name: Lint / Format / Knip + lint: + name: Lint / Format runs-on: ubuntu-24.04 if: github.event_name == 'push' || github.event.pull_request.draft == false steps: @@ -35,7 +35,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' + cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -43,22 +43,125 @@ jobs: - name: Lint & Format check run: pnpm run format-and-lint + typecheck: + name: Type Check + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Type check run: pnpm run typecheck + check-deps: + name: Check Dependencies + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Check unused dependencies run: pnpm run check-deps + validate-versions: + name: Validate Versions + runs-on: ubuntu-24.04 + if: github.event_name == 'push' || github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate version sync run: pnpm run validate:versions + validate-changeset: + name: Validate Changeset + runs-on: ubuntu-24.04 + if: github.event_name == 'pull_request' && github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Validate changesets run: pnpm run validate:changeset - if: github.event_name == 'pull_request' + + check-schema-diff: + name: Check Schema Diff + runs-on: ubuntu-24.04 + if: github.event_name == 'pull_request' && github.event.pull_request.draft == false + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile - name: Check schema diff run: pnpm run check:schema-diff - if: github.event_name == 'pull_request' test: name: Test @@ -76,7 +179,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' + cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -112,7 +215,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' + cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -149,24 +252,10 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - cache: 'pnpm' + cache: "pnpm" - name: Install dependencies run: pnpm install --frozen-lockfile - name: Check changeset run: pnpm changeset status --since=origin/main - - ci-success: - name: CI Success - runs-on: ubuntu-24.04 - needs: [quality, test, build] - if: always() && (github.event_name == 'push' || github.event.pull_request.draft == false) - steps: - - name: Check all job statuses - run: | - if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then - echo "::error::One or more CI jobs failed" - exit 1 - fi - echo "✅ All CI checks passed!" \ No newline at end of file diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml deleted file mode 100644 index 4d7c8047..00000000 --- a/.github/workflows/issue-bot.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Issue Bot -on: - issue_comment: - types: [created] - issues: - types: [opened] -jobs: - respond: - if: | - (github.event_name == 'issues') || - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@perstack-issue-bot')) - runs-on: ubuntu-latest - permissions: - issues: write - contents: read - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - - name: Add reaction to indicate processing - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if [ "${{ github.event_name }}" = "issue_comment" ]; then - gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions -f content=eyes - else - gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions -f content=eyes - fi - - name: Post processing status - id: status_comment - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - COMMENT_URL=$(gh issue comment ${{ github.event.issue.number }} --body "🤖 Starting...") - COMMENT_ID=$(echo "$COMMENT_URL" | grep -oE '[0-9]+$') - echo "comment_id=$COMMENT_ID" >> $GITHUB_OUTPUT - - name: Run Issue Bot - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPO: ${{ github.repository }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - COMMENT_ID: ${{ steps.status_comment.outputs.comment_id }} - run: | - echo 'model = "claude-sonnet-4-5"' > perstack.toml - echo '[provider]' >> perstack.toml - echo 'providerName = "anthropic"' >> perstack.toml - npm install -g tsx - npx perstack run @perstack/github-issue-bot "Answer issue #$ISSUE_NUMBER" 2>&1 | tsx scripts/checkpoint-filter.ts