Skip to content

Commit 72c425e

Browse files
committed
fix(ci): add checkout and release URL, fix set-commits --auto API call
- Add actions/checkout with fetch-depth: 0 so set-commits --auto can discover the git remote and HEAD commit - Add --url pointing to the GitHub release page - Fix setCommitsAuto: list org repos, match against local git remote, send real refs with HEAD SHA instead of bogus {repository: 'auto'} - Pass cwd through to setCommitsAuto from the command layer - Move setCommitsAuto test to test/isolated/ (requires mock.module)
1 parent f930abc commit 72c425e

File tree

5 files changed

+192
-42
lines changed

5 files changed

+192
-42
lines changed

.github/workflows/sentry-release.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ jobs:
2727
# matching both the npm package version and Sentry release version.
2828
VERSION: ${{ github.event.release.tag_name || inputs.version }}
2929
steps:
30+
- uses: actions/checkout@v7
31+
with:
32+
fetch-depth: 0
33+
3034
- name: Setup Node.js
3135
uses: actions/setup-node@v6
3236
with:
@@ -36,7 +40,9 @@ jobs:
3640
run: npm install -g "sentry@${VERSION}"
3741

3842
- name: Create release
39-
run: sentry release create "sentry/${VERSION}" --project cli
43+
run: >-
44+
sentry release create "sentry/${VERSION}" --project cli
45+
--url "https://github.com/${{ github.repository }}/releases/tag/${VERSION}"
4046
4147
- name: Set commits
4248
continue-on-error: true

src/commands/release/set-commits.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ async function setCommitsDefault(
130130
}
131131

132132
try {
133-
const release = await setCommitsAuto(org, version);
133+
const release = await setCommitsAuto(org, version, cwd);
134134
clearRepoIntegrationCache(org);
135135
return release;
136136
} catch (error) {
@@ -312,7 +312,7 @@ export const setCommitsCommand = buildCommand({
312312
);
313313
} else if (flags.auto) {
314314
// Explicit --auto: use repo integration, fail hard on error
315-
release = await setCommitsAuto(resolved.org, version);
315+
release = await setCommitsAuto(resolved.org, version, cwd);
316316
} else {
317317
// Default (no flag): try auto with cached fallback
318318
release = await setCommitsDefault(

src/lib/api/releases.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
updateAnOrganization_sRelease,
1717
} from "@sentry/api";
1818

19+
import { ApiError } from "../errors.js";
20+
import { getHeadCommit, getRepositoryName } from "../git.js";
1921
import { resolveOrgRegion } from "../region.js";
2022
import {
2123
apiRequestToRegion,
@@ -24,6 +26,7 @@ import {
2426
unwrapPaginatedResult,
2527
unwrapResult,
2628
} from "./infrastructure.js";
29+
import { listRepositories } from "./repositories.js";
2730

2831
// We cast through `unknown` to bridge the gap between the SDK's internal
2932
// return types and the public response types — the shapes are compatible
@@ -269,32 +272,60 @@ export async function createReleaseDeploy(
269272
/**
270273
* Set commits on a release using auto-discovery mode.
271274
*
272-
* This uses the internal API format `refs: [{repository: "auto", commit: "auto"}]`
273-
* which is not part of the OpenAPI spec, so we use apiRequestToRegion directly.
275+
* Lists the org's repositories from the Sentry API, matches against the
276+
* local git remote URL to find the corresponding Sentry repo, then sends
277+
* a refs payload with the HEAD commit SHA. This is the equivalent of the
278+
* reference sentry-cli's `--auto` mode.
274279
*
275-
* Requires a GitHub/GitLab/Bitbucket integration configured in Sentry.
280+
* Requires a GitHub/GitLab/Bitbucket integration configured in Sentry
281+
* AND a local git repository whose origin remote matches a Sentry repo.
276282
*
277283
* @param orgSlug - Organization slug
278284
* @param version - Release version
285+
* @param cwd - Working directory to discover git remote and HEAD from
279286
* @returns Updated release detail with commit count
287+
* @throws {ApiError} When the org has no repository integrations (400)
280288
*/
281289
export async function setCommitsAuto(
282290
orgSlug: string,
283-
version: string
291+
version: string,
292+
cwd?: string
284293
): Promise<OrgReleaseResponse> {
285-
const regionUrl = await resolveOrgRegion(orgSlug);
286-
const encodedVersion = encodeURIComponent(version);
287-
const { data } = await apiRequestToRegion<OrgReleaseResponse>(
288-
regionUrl,
289-
`organizations/${orgSlug}/releases/${encodedVersion}/`,
290-
{
291-
method: "PUT",
292-
body: {
293-
refs: [{ repository: "auto", commit: "auto" }],
294-
},
295-
}
294+
const repos = await listRepositories(orgSlug);
295+
if (repos.length === 0) {
296+
throw new ApiError(
297+
"No repository integrations configured for this organization.",
298+
400,
299+
`organizations/${orgSlug}/releases/${encodeURIComponent(version)}/`
300+
);
301+
}
302+
303+
const localRepo = getRepositoryName(cwd);
304+
if (!localRepo) {
305+
throw new ApiError(
306+
"Could not determine repository name from local git remote.",
307+
400,
308+
`organizations/${orgSlug}/releases/${encodeURIComponent(version)}/`
309+
);
310+
}
311+
312+
// Match local remote (e.g., "getsentry/cli") against Sentry repo names
313+
const matchedRepo = repos.find(
314+
(r) => r.name.toLowerCase() === localRepo.toLowerCase()
296315
);
297-
return data;
316+
if (!matchedRepo) {
317+
throw new ApiError(
318+
`No Sentry repository matching '${localRepo}'. ` +
319+
`Available: ${repos.map((r) => r.name).join(", ")}`,
320+
400,
321+
`organizations/${orgSlug}/releases/${encodeURIComponent(version)}/`
322+
);
323+
}
324+
325+
const headCommit = getHeadCommit(cwd);
326+
return setCommitsWithRefs(orgSlug, version, [
327+
{ repository: matchedRepo.name, commit: headCommit },
328+
]);
298329
}
299330

300331
/**
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Isolated tests for setCommitsAuto
3+
*
4+
* Uses mock.module() for git helpers, so must run in isolation
5+
* to avoid polluting other test files' module state.
6+
*/
7+
8+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
9+
import type { OrgReleaseResponse } from "@sentry/api";
10+
import { setAuthToken } from "../../src/lib/db/auth.js";
11+
import { setOrgRegion } from "../../src/lib/db/regions.js";
12+
import { mockFetch, useTestConfigDir } from "../helpers.js";
13+
14+
useTestConfigDir("set-commits-auto-");
15+
16+
mock.module("../../src/lib/git.js", () => ({
17+
getRepositoryName: () => "getsentry/cli",
18+
getHeadCommit: () => "abc123def456789012345678901234567890abcd",
19+
isInsideGitWorkTree: () => true,
20+
isShallowRepository: () => false,
21+
getCommitLog: () => [],
22+
getUncommittedFiles: () => [],
23+
parseRemoteUrl: (url: string) => url,
24+
}));
25+
26+
// Import after mock.module so the mocked git helpers are used
27+
const { setCommitsAuto } = await import("../../src/lib/api/releases.js");
28+
29+
const SAMPLE_RELEASE: OrgReleaseResponse = {
30+
id: 1,
31+
version: "1.0.0",
32+
shortVersion: "1.0.0",
33+
status: "open",
34+
dateCreated: "2025-01-01T00:00:00Z",
35+
dateReleased: null,
36+
firstEvent: null,
37+
lastEvent: null,
38+
ref: null,
39+
url: null,
40+
commitCount: 0,
41+
deployCount: 0,
42+
newGroups: 0,
43+
authors: [],
44+
projects: [
45+
{
46+
id: 1,
47+
slug: "test-project",
48+
name: "Test Project",
49+
platform: "javascript",
50+
platforms: ["javascript"],
51+
hasHealthData: false,
52+
newGroups: 0,
53+
},
54+
],
55+
data: {},
56+
versionInfo: null,
57+
};
58+
59+
const SAMPLE_REPO = {
60+
id: "1",
61+
name: "getsentry/cli",
62+
url: "https://github.com/getsentry/cli",
63+
provider: { id: "integrations:github", name: "GitHub" },
64+
status: "active",
65+
};
66+
67+
let originalFetch: typeof globalThis.fetch;
68+
69+
beforeEach(async () => {
70+
originalFetch = globalThis.fetch;
71+
await setAuthToken("test-token");
72+
setOrgRegion("test-org", "https://us.sentry.io");
73+
});
74+
75+
afterEach(() => {
76+
globalThis.fetch = originalFetch;
77+
});
78+
79+
describe("setCommitsAuto", () => {
80+
test("lists repos, discovers HEAD, and sends refs to the API", async () => {
81+
const withCommits = { ...SAMPLE_RELEASE, commitCount: 5 };
82+
const requests: { method: string; url: string }[] = [];
83+
84+
globalThis.fetch = mockFetch(async (input, init) => {
85+
const req = new Request(input!, init);
86+
requests.push({ method: req.method, url: req.url });
87+
88+
// First request: list org repositories (SDK uses /repos/ endpoint)
89+
if (req.url.includes("/repos/")) {
90+
expect(req.method).toBe("GET");
91+
return new Response(JSON.stringify([SAMPLE_REPO]), {
92+
status: 200,
93+
headers: { "Content-Type": "application/json" },
94+
});
95+
}
96+
97+
// Second request: PUT refs on the release
98+
expect(req.method).toBe("PUT");
99+
expect(req.url).toContain("/releases/1.0.0/");
100+
const body = (await req.json()) as {
101+
refs: Array<{ repository: string; commit: string }>;
102+
};
103+
expect(body.refs).toEqual([
104+
{
105+
repository: "getsentry/cli",
106+
commit: "abc123def456789012345678901234567890abcd",
107+
},
108+
]);
109+
return new Response(JSON.stringify(withCommits), {
110+
status: 200,
111+
headers: { "Content-Type": "application/json" },
112+
});
113+
});
114+
115+
const release = await setCommitsAuto("test-org", "1.0.0", "/tmp");
116+
117+
expect(release.commitCount).toBe(5);
118+
expect(requests).toHaveLength(2);
119+
});
120+
121+
test("throws when org has no repositories", async () => {
122+
globalThis.fetch = mockFetch(
123+
async () =>
124+
new Response(JSON.stringify([]), {
125+
status: 200,
126+
headers: { "Content-Type": "application/json" },
127+
})
128+
);
129+
130+
expect(setCommitsAuto("test-org", "1.0.0", "/tmp")).rejects.toThrow(
131+
/No repository integrations/
132+
);
133+
});
134+
});

test/lib/api/releases.test.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
getRelease,
1616
listReleaseDeploys,
1717
listReleasesPaginated,
18-
setCommitsAuto,
1918
setCommitsLocal,
2019
updateRelease,
2120
} from "../../../src/lib/api/releases.js";
@@ -234,28 +233,8 @@ describe("createReleaseDeploy", () => {
234233
// setCommitsAuto
235234
// =============================================================================
236235

237-
describe("setCommitsAuto", () => {
238-
test("sends auto refs to the API", async () => {
239-
const withCommits = { ...SAMPLE_RELEASE, commitCount: 5 };
240-
globalThis.fetch = mockFetch(async (input, init) => {
241-
const req = new Request(input!, init);
242-
expect(req.method).toBe("PUT");
243-
expect(req.url).toContain("/releases/1.0.0/");
244-
const body = (await req.json()) as {
245-
refs: Array<{ repository: string; commit: string }>;
246-
};
247-
expect(body.refs).toEqual([{ repository: "auto", commit: "auto" }]);
248-
return new Response(JSON.stringify(withCommits), {
249-
status: 200,
250-
headers: { "Content-Type": "application/json" },
251-
});
252-
});
253-
254-
const release = await setCommitsAuto("test-org", "1.0.0");
255-
256-
expect(release.commitCount).toBe(5);
257-
});
258-
});
236+
// setCommitsAuto tests are in test/isolated/set-commits-auto.test.ts
237+
// because they require mock.module() for git helpers.
259238

260239
// =============================================================================
261240
// setCommitsLocal

0 commit comments

Comments
 (0)