Skip to content

Commit 0a4e466

Browse files
committed
fix: fix set-commits --auto, document release workflow pitfalls
- Rewrite setCommitsAuto: list org repos via paginated API, match against local git remote, fetch previous release commit for range, send real refs with HEAD SHA and previousCommit - Use ValidationError for local git failures, ApiError for no-repos - Catch ValidationError in setCommitsDefault for graceful fallback - Add checkout, setup-node@v6, environment: production to workflow - Move --url from release create to deploy step - Add workflow_dispatch trigger and failure issue creation - Document pitfalls in agent-guidance.md, release.md, help text - Regenerate SKILL.md and reference files - Add comprehensive tests for all code paths
1 parent eb1b19e commit 0a4e466

File tree

8 files changed

+135
-85
lines changed

8 files changed

+135
-85
lines changed

.github/workflows/sentry-release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ jobs:
4747
# The org/ prefix is how org is specified — it is NOT part of the version.
4848
# The version portion must match Sentry.init({ release }) exactly.
4949
- name: Create release
50-
run: >-
51-
sentry release create "sentry/${VERSION}" --project cli
52-
--url "https://github.com/${{ github.repository }}/releases/tag/${VERSION}"
50+
run: sentry release create "sentry/${VERSION}" --project cli
5351

5452
# --auto matches the local origin remote against Sentry repo integrations.
5553
# continue-on-error: integration may not be configured for all orgs.
@@ -61,7 +59,9 @@ jobs:
6159
run: sentry release finalize "sentry/${VERSION}"
6260

6361
- name: Create deploy
64-
run: sentry release deploy "sentry/${VERSION}" production
62+
run: >-
63+
sentry release deploy "sentry/${VERSION}" production
64+
--url "https://github.com/${{ github.repository }}/releases/tag/${VERSION}"
6565
6666
- name: File issue on failure
6767
if: failure()

docs/src/content/docs/agent-guidance.md

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,31 @@ sentry schema issues
9393
sentry schema "GET /api/0/organizations/{organization_id_or_slug}/issues/"
9494
```
9595

96+
### Manage Releases
97+
98+
```bash
99+
# Create a release — version must match Sentry.init({ release }) exactly
100+
sentry release create my-org/1.0.0 --project my-project
101+
102+
# Associate commits via repository integration (needs local git checkout)
103+
sentry release set-commits my-org/1.0.0 --auto
104+
105+
# Or read commits from local git history (no integration needed)
106+
sentry release set-commits my-org/1.0.0 --local
107+
108+
# Mark the release as finalized
109+
sentry release finalize my-org/1.0.0
110+
111+
# Record a production deploy
112+
sentry release deploy my-org/1.0.0 production
113+
```
114+
115+
**Key details:**
116+
- The positional is `<org-slug>/<version>`. In `sentry release create sentry/1.0.0`, `sentry` is the org and `1.0.0` is the version — the slash separates org from version, it is not part of the version string.
117+
- The **version** must match the `release` value in `Sentry.init()`. If your SDK uses `"1.0.0"`, the command must use `org/1.0.0`.
118+
- `--auto` requires a Sentry repository integration (GitHub/GitLab/Bitbucket) **and** a local git checkout. It matches your `origin` remote against Sentry's repo list. Without a checkout, use `--local`.
119+
- With no flag, `set-commits` tries `--auto` first and falls back to `--local` on failure.
120+
96121
### Arbitrary API Access
97122

98123
```bash
@@ -183,38 +208,6 @@ sentry span list my-org/my-project/abc123def456...
183208

184209
When querying the Events API (directly or via `sentry api`), valid dataset values are: `spans`, `transactions`, `logs`, `errors`, `discover`.
185210

186-
## Release Workflow
187-
188-
The `sentry release` command group manages Sentry releases for tracking deploys and associating commits with errors. A typical CI workflow:
189-
190-
```bash
191-
# Create a release (version must match Sentry.init() release value)
192-
sentry release create my-org/1.0.0 --project my-project
193-
194-
# Associate commits via repository integration (requires git checkout)
195-
sentry release set-commits my-org/1.0.0 --auto
196-
197-
# Mark the release as finalized
198-
sentry release finalize my-org/1.0.0
199-
200-
# Record a deploy
201-
sentry release deploy my-org/1.0.0 production
202-
```
203-
204-
**Key details:**
205-
206-
- The `org/version` positional is `<org-slug>/<version>`, NOT a version prefix. `sentry release create sentry/1.0.0` means org=`sentry`, version=`1.0.0`. This is how org is specified — not via `SENTRY_ORG`.
207-
- The release **version** (e.g., `1.0.0`) must match the `release` value in your `Sentry.init()` call. If your SDK uses bare semver, the release must be bare semver too.
208-
- `--auto` requires **both** a Sentry repository integration (GitHub/GitLab/Bitbucket) **and** a local git checkout. It lists repos from the API and matches against your local `origin` remote URL, then sends the HEAD commit SHA. Without a checkout, use `--local` instead.
209-
- When neither `--auto` nor `--local` is specified, the CLI tries `--auto` first and falls back to `--local` on failure.
210-
211-
### CI/CD Setup Notes
212-
213-
- The `sentry` npm package requires **Node.js >= 22**. CI runners like `ubuntu-latest` ship Node.js 20 — add `actions/setup-node@v6` with `node-version: 22`.
214-
- If `SENTRY_AUTH_TOKEN` is scoped to a GitHub environment (e.g., `production`), set `environment: production` on the job.
215-
- A full git checkout (`fetch-depth: 0`) is needed for `--auto` to discover the remote URL and HEAD.
216-
- `set-commits --auto` has `continue-on-error` in most workflows because it requires a working repository integration. If the integration isn't configured, the step fails but the rest of the release workflow succeeds.
217-
218211
## Common Mistakes
219212

220213
- **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix.

docs/src/content/docs/commands/release.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ sentry release create $(sentry release propose-version)
190190
sentry release list --json
191191
sentry release view 1.0.0 --json
192192

193-
# CI/CD: full release workflow with org prefix
194-
sentry release create my-org/1.0.0 --project my-project --url "https://github.com/org/repo/releases/tag/1.0.0"
193+
# Full release workflow with explicit org
194+
sentry release create my-org/1.0.0 --project my-project
195195
sentry release set-commits my-org/1.0.0 --auto
196196
sentry release finalize my-org/1.0.0
197197
sentry release deploy my-org/1.0.0 production
@@ -200,7 +200,6 @@ sentry release deploy my-org/1.0.0 production
200200
## Important Notes
201201

202202
- **Version matching**: The release version must match the `release` value in your `Sentry.init()` call. If your SDK uses `"1.0.0"`, create the release as `sentry release create org/1.0.0` (version = `1.0.0`), **not** `sentry release create org/myapp/1.0.0`.
203-
- **The `org/` prefix is the org slug**: In `sentry release create sentry/1.0.0`, `sentry` is the org slug and `1.0.0` is the version. The `/` separates org from version, it's not part of the version string.
204-
- **`--auto` needs a git checkout**: The `--auto` flag lists repos from the Sentry API and matches against your local `origin` remote URL. A full checkout (`git fetch-depth: 0`) is needed for `--auto` to work. Without a checkout, use `--local`.
205-
- **Default mode tries `--auto` first**: When neither `--auto` nor `--local` is specified, the CLI tries auto-discovery first and falls back to local git history if the integration isn't configured.
206-
- **Node.js >= 22 required**: The `sentry` npm package requires Node.js 22 or later. CI runners like `ubuntu-latest` ship Node.js 20 by default.
203+
- **The `org/` prefix is the org slug**: In `sentry release create sentry/1.0.0`, `sentry` is the org slug and `1.0.0` is the version. The `/` separates org from version — it is not part of the version string.
204+
- **`--auto` needs a git checkout**: The `--auto` flag lists repos from the Sentry API and matches against your local `origin` remote URL. Without a local git repo, use `--local` instead.
205+
- **Default mode tries `--auto` first**: When neither `--auto` nor `--local` is specified, `set-commits` tries auto-discovery first and falls back to local git history if the integration isn't configured.

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,31 @@ sentry schema issues
103103
sentry schema "GET /api/0/organizations/{organization_id_or_slug}/issues/"
104104
```
105105

106+
#### Manage Releases
107+
108+
```bash
109+
# Create a release — version must match Sentry.init({ release }) exactly
110+
sentry release create my-org/1.0.0 --project my-project
111+
112+
# Associate commits via repository integration (needs local git checkout)
113+
sentry release set-commits my-org/1.0.0 --auto
114+
115+
# Or read commits from local git history (no integration needed)
116+
sentry release set-commits my-org/1.0.0 --local
117+
118+
# Mark the release as finalized
119+
sentry release finalize my-org/1.0.0
120+
121+
# Record a production deploy
122+
sentry release deploy my-org/1.0.0 production
123+
```
124+
125+
**Key details:**
126+
- The positional is `<org-slug>/<version>`. In `sentry release create sentry/1.0.0`, `sentry` is the org and `1.0.0` is the version — the slash separates org from version, it is not part of the version string.
127+
- The **version** must match the `release` value in `Sentry.init()`. If your SDK uses `"1.0.0"`, the command must use `org/1.0.0`.
128+
- `--auto` requires a Sentry repository integration (GitHub/GitLab/Bitbucket) **and** a local git checkout. It matches your `origin` remote against Sentry's repo list. Without a checkout, use `--local`.
129+
- With no flag, `set-commits` tries `--auto` first and falls back to `--local` on failure.
130+
106131
#### Arbitrary API Access
107132

108133
```bash
@@ -193,38 +218,6 @@ sentry span list my-org/my-project/abc123def456...
193218

194219
When querying the Events API (directly or via `sentry api`), valid dataset values are: `spans`, `transactions`, `logs`, `errors`, `discover`.
195220

196-
### Release Workflow
197-
198-
The `sentry release` command group manages Sentry releases for tracking deploys and associating commits with errors. A typical CI workflow:
199-
200-
```bash
201-
# Create a release (version must match Sentry.init() release value)
202-
sentry release create my-org/1.0.0 --project my-project
203-
204-
# Associate commits via repository integration (requires git checkout)
205-
sentry release set-commits my-org/1.0.0 --auto
206-
207-
# Mark the release as finalized
208-
sentry release finalize my-org/1.0.0
209-
210-
# Record a deploy
211-
sentry release deploy my-org/1.0.0 production
212-
```
213-
214-
**Key details:**
215-
216-
- The `org/version` positional is `<org-slug>/<version>`, NOT a version prefix. `sentry release create sentry/1.0.0` means org=`sentry`, version=`1.0.0`. This is how org is specified — not via `SENTRY_ORG`.
217-
- The release **version** (e.g., `1.0.0`) must match the `release` value in your `Sentry.init()` call. If your SDK uses bare semver, the release must be bare semver too.
218-
- `--auto` requires **both** a Sentry repository integration (GitHub/GitLab/Bitbucket) **and** a local git checkout. It lists repos from the API and matches against your local `origin` remote URL, then sends the HEAD commit SHA. Without a checkout, use `--local` instead.
219-
- When neither `--auto` nor `--local` is specified, the CLI tries `--auto` first and falls back to `--local` on failure.
220-
221-
#### CI/CD Setup Notes
222-
223-
- The `sentry` npm package requires **Node.js >= 22**. CI runners like `ubuntu-latest` ship Node.js 20 — add `actions/setup-node@v6` with `node-version: 22`.
224-
- If `SENTRY_AUTH_TOKEN` is scoped to a GitHub environment (e.g., `production`), set `environment: production` on the job.
225-
- A full git checkout (`fetch-depth: 0`) is needed for `--auto` to discover the remote URL and HEAD.
226-
- `set-commits --auto` has `continue-on-error` in most workflows because it requires a working repository integration. If the integration isn't configured, the step fails but the rest of the release workflow succeeds.
227-
228221
### Common Mistakes
229222

230223
- **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix.

plugins/sentry-cli/skills/sentry-cli/references/release.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ sentry release create $(sentry release propose-version)
121121
sentry release list --json
122122
sentry release view 1.0.0 --json
123123

124-
# CI/CD: full release workflow with org prefix
125-
sentry release create my-org/1.0.0 --project my-project --url "https://github.com/org/repo/releases/tag/1.0.0"
124+
# Full release workflow with explicit org
125+
sentry release create my-org/1.0.0 --project my-project
126126
sentry release set-commits my-org/1.0.0 --auto
127127
sentry release finalize my-org/1.0.0
128128
sentry release deploy my-org/1.0.0 production

src/commands/release/set-commits.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ async function setCommitsDefault(
142142
);
143143
return setCommitsFromLocal(org, version, cwd, depth);
144144
}
145-
if (error instanceof ValidationError) {
145+
if (error instanceof ValidationError && error.field === "repository") {
146146
log.warn(
147147
`Auto-discovery failed: ${error.message}. ` +
148148
"Falling back to local git history."

src/lib/api/releases.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,41 @@ export async function createReleaseDeploy(
271271
return data as unknown as DeployResponse;
272272
}
273273

274+
/**
275+
* Get the last commit SHA from the previous release that has commits.
276+
*
277+
* Uses the undocumented `/previous-with-commits/` endpoint (same as the
278+
* reference sentry-cli) to determine the commit baseline for range-based
279+
* commit association. Without this, Sentry can't compute which commits
280+
* are new in the current release and reports 0 commits.
281+
*
282+
* @param orgSlug - Organization slug
283+
* @param version - Current release version
284+
* @returns Previous release's last commit SHA, or undefined if no previous release
285+
*/
286+
async function getPreviousReleaseCommit(
287+
orgSlug: string,
288+
version: string
289+
): Promise<string | undefined> {
290+
try {
291+
const regionUrl = await resolveOrgRegion(orgSlug);
292+
const encodedVersion = encodeURIComponent(version);
293+
const { data } = await apiRequestToRegion<{
294+
lastCommit?: { id: string } | null;
295+
}>(
296+
regionUrl,
297+
`organizations/${orgSlug}/releases/${encodedVersion}/previous-with-commits/`,
298+
{ method: "GET" }
299+
);
300+
return data?.lastCommit?.id;
301+
} catch {
302+
// Not critical — if we can't get the previous commit, we still send
303+
// refs without previousCommit. Sentry will try to determine the range
304+
// from its own data (may result in 0 commits for first releases).
305+
return;
306+
}
307+
}
308+
274309
/**
275310
* Set commits on a release using auto-discovery mode.
276311
*
@@ -323,9 +358,16 @@ export async function setCommitsAuto(
323358
);
324359
if (match) {
325360
const headCommit = getHeadCommit(cwd);
326-
return setCommitsWithRefs(orgSlug, version, [
327-
{ repository: match.name, commit: headCommit },
328-
]);
361+
const previousCommit = await getPreviousReleaseCommit(orgSlug, version);
362+
const ref: {
363+
repository: string;
364+
commit: string;
365+
previousCommit?: string;
366+
} = { repository: match.name, commit: headCommit };
367+
if (previousCommit) {
368+
ref.previousCommit = previousCommit;
369+
}
370+
return setCommitsWithRefs(orgSlug, version, [ref]);
329371
}
330372

331373
if (!result.nextCursor) {

test/isolated/set-commits-auto.test.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,15 @@ afterEach(() => {
7777
});
7878

7979
describe("setCommitsAuto", () => {
80-
test("lists repos, discovers HEAD, and sends refs to the API", async () => {
80+
test("lists repos, discovers HEAD, fetches previous commit, and sends refs", async () => {
8181
const withCommits = { ...SAMPLE_RELEASE, commitCount: 5 };
8282
const requests: { method: string; url: string }[] = [];
8383

8484
globalThis.fetch = mockFetch(async (input, init) => {
8585
const req = new Request(input!, init);
8686
requests.push({ method: req.method, url: req.url });
8787

88-
// First request: list org repositories (SDK uses /repos/ endpoint)
88+
// List org repositories (SDK uses /repos/ endpoint)
8989
if (req.url.includes("/repos/")) {
9090
expect(req.method).toBe("GET");
9191
return new Response(JSON.stringify([SAMPLE_REPO]), {
@@ -94,16 +94,32 @@ describe("setCommitsAuto", () => {
9494
});
9595
}
9696

97-
// Second request: PUT refs on the release
97+
// Previous release commit lookup
98+
if (req.url.includes("/previous-with-commits/")) {
99+
expect(req.method).toBe("GET");
100+
return new Response(
101+
JSON.stringify({
102+
lastCommit: { id: "prev000000000000000000000000000000000000" },
103+
}),
104+
{ status: 200, headers: { "Content-Type": "application/json" } }
105+
);
106+
}
107+
108+
// PUT refs on the release
98109
expect(req.method).toBe("PUT");
99110
expect(req.url).toContain("/releases/1.0.0/");
100111
const body = (await req.json()) as {
101-
refs: Array<{ repository: string; commit: string }>;
112+
refs: Array<{
113+
repository: string;
114+
commit: string;
115+
previousCommit?: string;
116+
}>;
102117
};
103118
expect(body.refs).toEqual([
104119
{
105120
repository: "getsentry/cli",
106121
commit: "abc123def456789012345678901234567890abcd",
122+
previousCommit: "prev000000000000000000000000000000000000",
107123
},
108124
]);
109125
return new Response(JSON.stringify(withCommits), {
@@ -115,7 +131,6 @@ describe("setCommitsAuto", () => {
115131
const release = await setCommitsAuto("test-org", "1.0.0", "/tmp");
116132

117133
expect(release.commitCount).toBe(5);
118-
expect(requests).toHaveLength(2);
119134
});
120135

121136
test("throws ApiError when org has no repositories", async () => {
@@ -174,6 +189,14 @@ describe("setCommitsAuto", () => {
174189
});
175190
}
176191

192+
// Previous release commit lookup (no previous release)
193+
if (req.url.includes("/previous-with-commits/")) {
194+
return new Response(JSON.stringify({}), {
195+
status: 200,
196+
headers: { "Content-Type": "application/json" },
197+
});
198+
}
199+
177200
// PUT refs on the release
178201
return new Response(JSON.stringify(withCommits), {
179202
status: 200,

0 commit comments

Comments
 (0)