From 0a4e46699db173ca8f44624c2c0e87f26e663834 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 3 Apr 2026 09:59:59 +0000 Subject: [PATCH] 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 --- .github/workflows/sentry-release.yml | 8 +-- docs/src/content/docs/agent-guidance.md | 57 ++++++++----------- docs/src/content/docs/commands/release.md | 11 ++-- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 57 ++++++++----------- .../skills/sentry-cli/references/release.md | 4 +- src/commands/release/set-commits.ts | 2 +- src/lib/api/releases.ts | 48 +++++++++++++++- test/isolated/set-commits-auto.test.ts | 33 +++++++++-- 8 files changed, 135 insertions(+), 85 deletions(-) diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml index 36a750693..7697decf0 100644 --- a/.github/workflows/sentry-release.yml +++ b/.github/workflows/sentry-release.yml @@ -47,9 +47,7 @@ jobs: # The org/ prefix is how org is specified — it is NOT part of the version. # The version portion must match Sentry.init({ release }) exactly. - name: Create release - run: >- - sentry release create "sentry/${VERSION}" --project cli - --url "https://github.com/${{ github.repository }}/releases/tag/${VERSION}" + run: sentry release create "sentry/${VERSION}" --project cli # --auto matches the local origin remote against Sentry repo integrations. # continue-on-error: integration may not be configured for all orgs. @@ -61,7 +59,9 @@ jobs: run: sentry release finalize "sentry/${VERSION}" - name: Create deploy - run: sentry release deploy "sentry/${VERSION}" production + run: >- + sentry release deploy "sentry/${VERSION}" production + --url "https://github.com/${{ github.repository }}/releases/tag/${VERSION}" - name: File issue on failure if: failure() diff --git a/docs/src/content/docs/agent-guidance.md b/docs/src/content/docs/agent-guidance.md index 3c2b5e165..aae8658ee 100644 --- a/docs/src/content/docs/agent-guidance.md +++ b/docs/src/content/docs/agent-guidance.md @@ -93,6 +93,31 @@ sentry schema issues sentry schema "GET /api/0/organizations/{organization_id_or_slug}/issues/" ``` +### Manage Releases + +```bash +# Create a release — version must match Sentry.init({ release }) exactly +sentry release create my-org/1.0.0 --project my-project + +# Associate commits via repository integration (needs local git checkout) +sentry release set-commits my-org/1.0.0 --auto + +# Or read commits from local git history (no integration needed) +sentry release set-commits my-org/1.0.0 --local + +# Mark the release as finalized +sentry release finalize my-org/1.0.0 + +# Record a production deploy +sentry release deploy my-org/1.0.0 production +``` + +**Key details:** +- The positional is `/`. 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. +- 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`. +- `--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`. +- With no flag, `set-commits` tries `--auto` first and falls back to `--local` on failure. + ### Arbitrary API Access ```bash @@ -183,38 +208,6 @@ sentry span list my-org/my-project/abc123def456... When querying the Events API (directly or via `sentry api`), valid dataset values are: `spans`, `transactions`, `logs`, `errors`, `discover`. -## Release Workflow - -The `sentry release` command group manages Sentry releases for tracking deploys and associating commits with errors. A typical CI workflow: - -```bash -# Create a release (version must match Sentry.init() release value) -sentry release create my-org/1.0.0 --project my-project - -# Associate commits via repository integration (requires git checkout) -sentry release set-commits my-org/1.0.0 --auto - -# Mark the release as finalized -sentry release finalize my-org/1.0.0 - -# Record a deploy -sentry release deploy my-org/1.0.0 production -``` - -**Key details:** - -- The `org/version` positional is `/`, 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`. -- 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. -- `--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. -- When neither `--auto` nor `--local` is specified, the CLI tries `--auto` first and falls back to `--local` on failure. - -### CI/CD Setup Notes - -- 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`. -- If `SENTRY_AUTH_TOKEN` is scoped to a GitHub environment (e.g., `production`), set `environment: production` on the job. -- A full git checkout (`fetch-depth: 0`) is needed for `--auto` to discover the remote URL and HEAD. -- `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. - ## Common Mistakes - **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix. diff --git a/docs/src/content/docs/commands/release.md b/docs/src/content/docs/commands/release.md index d5a0f54e8..4adddfb84 100644 --- a/docs/src/content/docs/commands/release.md +++ b/docs/src/content/docs/commands/release.md @@ -190,8 +190,8 @@ sentry release create $(sentry release propose-version) sentry release list --json sentry release view 1.0.0 --json -# CI/CD: full release workflow with org prefix -sentry release create my-org/1.0.0 --project my-project --url "https://github.com/org/repo/releases/tag/1.0.0" +# Full release workflow with explicit org +sentry release create my-org/1.0.0 --project my-project sentry release set-commits my-org/1.0.0 --auto sentry release finalize my-org/1.0.0 sentry release deploy my-org/1.0.0 production @@ -200,7 +200,6 @@ sentry release deploy my-org/1.0.0 production ## Important Notes - **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`. -- **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. -- **`--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`. -- **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. -- **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. +- **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. +- **`--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. +- **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. diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index ece25673c..ba17a2a2a 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -103,6 +103,31 @@ sentry schema issues sentry schema "GET /api/0/organizations/{organization_id_or_slug}/issues/" ``` +#### Manage Releases + +```bash +# Create a release — version must match Sentry.init({ release }) exactly +sentry release create my-org/1.0.0 --project my-project + +# Associate commits via repository integration (needs local git checkout) +sentry release set-commits my-org/1.0.0 --auto + +# Or read commits from local git history (no integration needed) +sentry release set-commits my-org/1.0.0 --local + +# Mark the release as finalized +sentry release finalize my-org/1.0.0 + +# Record a production deploy +sentry release deploy my-org/1.0.0 production +``` + +**Key details:** +- The positional is `/`. 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. +- 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`. +- `--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`. +- With no flag, `set-commits` tries `--auto` first and falls back to `--local` on failure. + #### Arbitrary API Access ```bash @@ -193,38 +218,6 @@ sentry span list my-org/my-project/abc123def456... When querying the Events API (directly or via `sentry api`), valid dataset values are: `spans`, `transactions`, `logs`, `errors`, `discover`. -### Release Workflow - -The `sentry release` command group manages Sentry releases for tracking deploys and associating commits with errors. A typical CI workflow: - -```bash -# Create a release (version must match Sentry.init() release value) -sentry release create my-org/1.0.0 --project my-project - -# Associate commits via repository integration (requires git checkout) -sentry release set-commits my-org/1.0.0 --auto - -# Mark the release as finalized -sentry release finalize my-org/1.0.0 - -# Record a deploy -sentry release deploy my-org/1.0.0 production -``` - -**Key details:** - -- The `org/version` positional is `/`, 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`. -- 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. -- `--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. -- When neither `--auto` nor `--local` is specified, the CLI tries `--auto` first and falls back to `--local` on failure. - -#### CI/CD Setup Notes - -- 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`. -- If `SENTRY_AUTH_TOKEN` is scoped to a GitHub environment (e.g., `production`), set `environment: production` on the job. -- A full git checkout (`fetch-depth: 0`) is needed for `--auto` to discover the remote URL and HEAD. -- `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. - ### Common Mistakes - **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix. diff --git a/plugins/sentry-cli/skills/sentry-cli/references/release.md b/plugins/sentry-cli/skills/sentry-cli/references/release.md index b472d240b..2bb473051 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/release.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/release.md @@ -121,8 +121,8 @@ sentry release create $(sentry release propose-version) sentry release list --json sentry release view 1.0.0 --json -# CI/CD: full release workflow with org prefix -sentry release create my-org/1.0.0 --project my-project --url "https://github.com/org/repo/releases/tag/1.0.0" +# Full release workflow with explicit org +sentry release create my-org/1.0.0 --project my-project sentry release set-commits my-org/1.0.0 --auto sentry release finalize my-org/1.0.0 sentry release deploy my-org/1.0.0 production diff --git a/src/commands/release/set-commits.ts b/src/commands/release/set-commits.ts index 2a371b308..41d859a28 100644 --- a/src/commands/release/set-commits.ts +++ b/src/commands/release/set-commits.ts @@ -142,7 +142,7 @@ async function setCommitsDefault( ); return setCommitsFromLocal(org, version, cwd, depth); } - if (error instanceof ValidationError) { + if (error instanceof ValidationError && error.field === "repository") { log.warn( `Auto-discovery failed: ${error.message}. ` + "Falling back to local git history." diff --git a/src/lib/api/releases.ts b/src/lib/api/releases.ts index 6096da0af..99bb2ba59 100644 --- a/src/lib/api/releases.ts +++ b/src/lib/api/releases.ts @@ -271,6 +271,41 @@ export async function createReleaseDeploy( return data as unknown as DeployResponse; } +/** + * Get the last commit SHA from the previous release that has commits. + * + * Uses the undocumented `/previous-with-commits/` endpoint (same as the + * reference sentry-cli) to determine the commit baseline for range-based + * commit association. Without this, Sentry can't compute which commits + * are new in the current release and reports 0 commits. + * + * @param orgSlug - Organization slug + * @param version - Current release version + * @returns Previous release's last commit SHA, or undefined if no previous release + */ +async function getPreviousReleaseCommit( + orgSlug: string, + version: string +): Promise { + try { + const regionUrl = await resolveOrgRegion(orgSlug); + const encodedVersion = encodeURIComponent(version); + const { data } = await apiRequestToRegion<{ + lastCommit?: { id: string } | null; + }>( + regionUrl, + `organizations/${orgSlug}/releases/${encodedVersion}/previous-with-commits/`, + { method: "GET" } + ); + return data?.lastCommit?.id; + } catch { + // Not critical — if we can't get the previous commit, we still send + // refs without previousCommit. Sentry will try to determine the range + // from its own data (may result in 0 commits for first releases). + return; + } +} + /** * Set commits on a release using auto-discovery mode. * @@ -323,9 +358,16 @@ export async function setCommitsAuto( ); if (match) { const headCommit = getHeadCommit(cwd); - return setCommitsWithRefs(orgSlug, version, [ - { repository: match.name, commit: headCommit }, - ]); + const previousCommit = await getPreviousReleaseCommit(orgSlug, version); + const ref: { + repository: string; + commit: string; + previousCommit?: string; + } = { repository: match.name, commit: headCommit }; + if (previousCommit) { + ref.previousCommit = previousCommit; + } + return setCommitsWithRefs(orgSlug, version, [ref]); } if (!result.nextCursor) { diff --git a/test/isolated/set-commits-auto.test.ts b/test/isolated/set-commits-auto.test.ts index 8140435b0..be53cc8b7 100644 --- a/test/isolated/set-commits-auto.test.ts +++ b/test/isolated/set-commits-auto.test.ts @@ -77,7 +77,7 @@ afterEach(() => { }); describe("setCommitsAuto", () => { - test("lists repos, discovers HEAD, and sends refs to the API", async () => { + test("lists repos, discovers HEAD, fetches previous commit, and sends refs", async () => { const withCommits = { ...SAMPLE_RELEASE, commitCount: 5 }; const requests: { method: string; url: string }[] = []; @@ -85,7 +85,7 @@ describe("setCommitsAuto", () => { const req = new Request(input!, init); requests.push({ method: req.method, url: req.url }); - // First request: list org repositories (SDK uses /repos/ endpoint) + // List org repositories (SDK uses /repos/ endpoint) if (req.url.includes("/repos/")) { expect(req.method).toBe("GET"); return new Response(JSON.stringify([SAMPLE_REPO]), { @@ -94,16 +94,32 @@ describe("setCommitsAuto", () => { }); } - // Second request: PUT refs on the release + // Previous release commit lookup + if (req.url.includes("/previous-with-commits/")) { + expect(req.method).toBe("GET"); + return new Response( + JSON.stringify({ + lastCommit: { id: "prev000000000000000000000000000000000000" }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ); + } + + // PUT refs on the release expect(req.method).toBe("PUT"); expect(req.url).toContain("/releases/1.0.0/"); const body = (await req.json()) as { - refs: Array<{ repository: string; commit: string }>; + refs: Array<{ + repository: string; + commit: string; + previousCommit?: string; + }>; }; expect(body.refs).toEqual([ { repository: "getsentry/cli", commit: "abc123def456789012345678901234567890abcd", + previousCommit: "prev000000000000000000000000000000000000", }, ]); return new Response(JSON.stringify(withCommits), { @@ -115,7 +131,6 @@ describe("setCommitsAuto", () => { const release = await setCommitsAuto("test-org", "1.0.0", "/tmp"); expect(release.commitCount).toBe(5); - expect(requests).toHaveLength(2); }); test("throws ApiError when org has no repositories", async () => { @@ -174,6 +189,14 @@ describe("setCommitsAuto", () => { }); } + // Previous release commit lookup (no previous release) + if (req.url.includes("/previous-with-commits/")) { + return new Response(JSON.stringify({}), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + // PUT refs on the release return new Response(JSON.stringify(withCommits), { status: 200,