Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/sentry-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
Expand Down
57 changes: 25 additions & 32 deletions docs/src/content/docs/agent-guidance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<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.
- 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
Expand Down Expand Up @@ -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 `<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`.
- 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.
Expand Down
11 changes: 5 additions & 6 deletions docs/src/content/docs/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
57 changes: 25 additions & 32 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<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.
- 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
Expand Down Expand Up @@ -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 `<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`.
- 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.
Expand Down
4 changes: 2 additions & 2 deletions plugins/sentry-cli/skills/sentry-cli/references/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/commands/release/set-commits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
48 changes: 45 additions & 3 deletions src/lib/api/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined> {
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.
*
Expand Down Expand Up @@ -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) {
Expand Down
33 changes: 28 additions & 5 deletions test/isolated/set-commits-auto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ 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 }[] = [];

globalThis.fetch = mockFetch(async (input, init) => {
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]), {
Expand All @@ -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), {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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,
Expand Down
Loading