diff --git a/src/commands/issue/utils.ts b/src/commands/issue/utils.ts index f2cbff001..07f035c6a 100644 --- a/src/commands/issue/utils.ts +++ b/src/commands/issue/utils.ts @@ -455,7 +455,7 @@ async function resolveSelector( throw new ResolutionError( `Selector '${selector}'`, "no unresolved issues found", - `sentry issue list ${orgSlug}/ -q "is:resolved"`, + `sentry issue list ${orgSlug}/`, [`The ${label} issue selector only matches unresolved issues.`] ); } diff --git a/src/commands/release/set-commits.ts b/src/commands/release/set-commits.ts index f1917b506..44c2b650c 100644 --- a/src/commands/release/set-commits.ts +++ b/src/commands/release/set-commits.ts @@ -134,7 +134,11 @@ async function setCommitsDefault( clearRepoIntegrationCache(org); return release; } catch (error) { - if (error instanceof ApiError && error.status === 400) { + if ( + error instanceof ApiError && + error.status === 400 && + error.message.includes("No repository integrations") + ) { cacheNoRepoIntegration(org); log.warn( "Could not auto-discover commits (no repository integration). " + diff --git a/src/types/seer.ts b/src/types/seer.ts index 74fb3193b..a5a7da43b 100644 --- a/src/types/seer.ts +++ b/src/types/seer.ts @@ -253,7 +253,11 @@ function searchContainersForRootCauses( containers: WithCauses[] ): RootCause[] | null { for (const container of containers) { - if (container.key === "root_cause_analysis" && container.causes) { + if ( + container.key === "root_cause_analysis" && + container.causes && + container.causes.length > 0 + ) { return container.causes; } } diff --git a/test/commands/issue/utils.test.ts b/test/commands/issue/utils.test.ts index f105232f6..993044e03 100644 --- a/test/commands/issue/utils.test.ts +++ b/test/commands/issue/utils.test.ts @@ -1992,7 +1992,7 @@ describe("resolveOrgAndIssueId: magic @ selectors", () => { expect(String(err)).toContain("no unresolved issues found"); expect(String(err)).toContain("most recent"); expect(String(err)).toContain("sentry issue list"); - expect(String(err)).toContain('-q "is:resolved"'); + expect(String(err)).not.toContain("is:resolved"); }); test("throws ContextError when org cannot be resolved for bare @selector", async () => { diff --git a/test/commands/release/set-commits.test.ts b/test/commands/release/set-commits.test.ts index 1d9f7efee..4a20f0d89 100644 --- a/test/commands/release/set-commits.test.ts +++ b/test/commands/release/set-commits.test.ts @@ -21,7 +21,7 @@ vi.mock("../../../src/lib/api-client.js", async (importOriginal) => { // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking import * as apiClient from "../../../src/lib/api-client.js"; -import { ValidationError } from "../../../src/lib/errors.js"; +import { ApiError, ValidationError } from "../../../src/lib/errors.js"; vi.mock("../../../src/lib/resolve-target.js", async (importOriginal) => { const actual = @@ -265,6 +265,72 @@ describe("release set-commits (default mode)", () => { resolveOrgSpy.mockRestore(); }); + test("propagates unrelated 400 errors from setCommitsAuto", async () => { + resolveOrgSpy.mockResolvedValue({ org: "my-org" }); + setCommitsAutoSpy.mockRejectedValue( + new ApiError("Invalid commit SHA.", 400, undefined, "releases/1.0.0/") + ); + + const repoRoot = new URL("../../..", import.meta.url).pathname.replace( + /\/$/, + "" + ); + const { context } = createMockContext(repoRoot); + const func = await setCommitsCommand.loader(); + await expect( + func.call( + context, + { + auto: false, + local: false, + clear: false, + commit: undefined, + "initial-depth": 20, + json: true, + }, + "1.0.0" + ) + ).rejects.toThrow("Invalid commit SHA."); + + expect(setCommitsAutoSpy).toHaveBeenCalled(); + expect(setCommitsLocalSpy).not.toHaveBeenCalled(); + }); + + test("falls back to local only on 'No repository integrations' 400", async () => { + resolveOrgSpy.mockResolvedValue({ org: "my-org" }); + setCommitsAutoSpy.mockRejectedValue( + new ApiError( + "No repository integrations configured for this organization.", + 400, + undefined, + "releases/1.0.0/" + ) + ); + setCommitsLocalSpy.mockResolvedValue(sampleRelease); + + const repoRoot = new URL("../../..", import.meta.url).pathname.replace( + /\/$/, + "" + ); + const { context } = createMockContext(repoRoot); + const func = await setCommitsCommand.loader(); + await func.call( + context, + { + auto: false, + local: false, + clear: false, + commit: undefined, + "initial-depth": 20, + json: true, + }, + "1.0.0" + ); + + expect(setCommitsAutoSpy).toHaveBeenCalled(); + expect(setCommitsLocalSpy).toHaveBeenCalled(); + }); + test("falls back to local on ValidationError from auto", async () => { resolveOrgSpy.mockResolvedValue({ org: "my-org" }); setCommitsAutoSpy.mockRejectedValue( diff --git a/test/types/seer.test.ts b/test/types/seer.test.ts index db049216f..b89dabd27 100644 --- a/test/types/seer.test.ts +++ b/test/types/seer.test.ts @@ -226,6 +226,44 @@ describe("extractRootCauses", () => { expect(causes[0]?.relevant_repos).toEqual(["org/backend"]); }); + test("falls through to agent artifacts when legacy causes array is empty", () => { + const state = { + run_id: 789, + status: "COMPLETED", + blocks: [ + { + key: "root_cause_analysis", + status: "COMPLETED", + causes: [], + artifacts: [], + }, + { + id: "block-2", + message: { role: "assistant", content: "Found it" }, + timestamp: "2025-01-01T00:00:00Z", + artifacts: [ + { + key: "root_cause", + data: { + one_line_description: "Race condition in auth middleware", + five_whys: ["Token refresh not atomic"], + relevant_repo: "org/api-server", + }, + reason: "", + }, + ], + }, + ], + } as unknown as AutofixState; + + const causes = extractRootCauses(state); + expect(causes).toHaveLength(1); + expect(causes[0]?.description).toBe( + "Race condition in auth middleware" + ); + expect(causes[0]?.relevant_repos).toEqual(["org/api-server"]); + }); + test("extracts root cause from agent artifact without relevant_repo", () => { const state = { run_id: 101,