|
| 1 | +# Creating the release in CI — commits, finalize, deploy |
| 2 | + |
| 3 | +The SDK half tags events. This half creates the **release object** in Sentry and hangs the useful |
| 4 | +metadata off it. Both halves are required; neither works alone. |
| 5 | + |
| 6 | +## Why bother, when Sentry auto-creates releases |
| 7 | + |
| 8 | +Sentry creates a release entity the first time it sees an event carrying an unknown release name. That |
| 9 | +auto-created release is a bare label: no commits, so no suspect commits from release data and no |
| 10 | +`Fixes` resolution; no finalize timestamp, so "resolve in next release" has nothing to anchor to; no |
| 11 | +deploy, so no deploy notifications. Creating it deliberately in CI is what turns the label into the |
| 12 | +feature set. |
| 13 | + |
| 14 | +## Where it goes in the pipeline |
| 15 | + |
| 16 | +Order matters, and getting it wrong is the most common reason a correct-looking setup does nothing: |
| 17 | + |
| 18 | +1. **Check out with full git history.** Commit association walks the log between the previous release |
| 19 | + and `HEAD`. A shallow clone (CI default) has nothing to walk — on GitHub Actions that means |
| 20 | + `fetch-depth: 0`. |
| 21 | +2. **Build**, with the release name baked in (see [`tagging.md`](tagging.md)). |
| 22 | +3. **Create the release and upload artifacts** — source maps or debug files, from *this* build. |
| 23 | +4. **Associate commits.** |
| 24 | +5. **Finalize** the release. |
| 25 | +6. **Deploy**, then **record the deploy** into its environment. |
| 26 | + |
| 27 | +The rule underneath it: the release step runs **after the build, before the deploy**, and the files you |
| 28 | +deploy must be the files that were built when the artifacts were uploaded. Uploading after deploy means |
| 29 | +events arrive before Sentry can process them. |
| 30 | + |
| 31 | +Every path below needs an auth token — it's a write to your org. See |
| 32 | +[`../auth-token.md`](../auth-token.md); the token, `SENTRY_ORG`, and |
| 33 | +`SENTRY_PROJECT` are the same three variables for all of them. |
| 34 | + |
| 35 | +## Path A — a JavaScript bundler plugin (check this first) |
| 36 | + |
| 37 | +If the project already builds with `@sentry/webpack-plugin`, `@sentry/vite-plugin`, |
| 38 | +`@sentry/rollup-plugin`, `@sentry/esbuild-plugin`, or a framework SDK that wraps one (`@sentry/nextjs`, |
| 39 | +`@sentry/sveltekit`, `@sentry/react-router`, `@sentry/tanstackstart-react`, `@sentry/nuxt`), **most of |
| 40 | +this is already happening.** Do not add a parallel `sentry-cli` pipeline next to it — you'll get two |
| 41 | +releases fighting over the same name. Configure the plugin instead: |
| 42 | + |
| 43 | +```js |
| 44 | +sentryVitePlugin({ |
| 45 | + org: process.env.SENTRY_ORG, |
| 46 | + project: process.env.SENTRY_PROJECT, |
| 47 | + authToken: process.env.SENTRY_AUTH_TOKEN, |
| 48 | + |
| 49 | + release: { |
| 50 | + // name: defaults to a detected CI value, else the git HEAD SHA |
| 51 | + // inject: true — injects the name into the bundle for the SDK |
| 52 | + // create: true — creates the release in Sentry |
| 53 | + // finalize: true — finalizes when the build ends |
| 54 | + // setCommits: { auto: true } — associates commits |
| 55 | + deploy: {env: 'production'}, |
| 56 | + }, |
| 57 | +}) |
| 58 | +``` |
| 59 | + |
| 60 | +The defaults do the right thing: `inject`, `create`, and `finalize` are all `true`, and `setCommits` |
| 61 | +defaults to `{auto: true}`. In practice there are only three things to check: |
| 62 | + |
| 63 | +- **`deploy` is not set by default** — add `deploy: {env: '<environment>'}` to get deploy tracking. |
| 64 | +- **Commit association still needs git history in CI.** The plugin shells out to the same logic |
| 65 | + `sentry-cli` uses, so `fetch-depth: 0` applies here too. |
| 66 | +- `release.name` is unset and undetectable (no git, no recognized CI) → **no release is created at |
| 67 | + all**, silently. Set it explicitly in that case. |
| 68 | + |
| 69 | +Useful escapes: `setCommits: {auto: true, ignoreMissing: true}` when history is rewritten by |
| 70 | +squash-merges, `setCommits: {repo: 'owner/name', commit: '<sha>'}` when the build has no repo access, |
| 71 | +`setCommits: false` to opt out, and `release.vcsRemote` if the remote isn't `origin`. |
| 72 | + |
| 73 | +## Path B — GitHub Actions |
| 74 | + |
| 75 | +For everything that isn't a JS bundler build, `getsentry/action-release` is the shortest correct path. |
| 76 | +It creates the release, associates commits, finalizes, and records the deploy in one step: |
| 77 | + |
| 78 | +```yaml |
| 79 | +- uses: actions/checkout@v4 |
| 80 | + with: |
| 81 | + fetch-depth: 0 # required: commit association needs history |
| 82 | + |
| 83 | +# ... your build steps here ... |
| 84 | + |
| 85 | +- name: Create Sentry release |
| 86 | + uses: getsentry/action-release@v3 |
| 87 | + env: |
| 88 | + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} |
| 89 | + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} |
| 90 | + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} |
| 91 | + with: |
| 92 | + environment: production |
| 93 | + release: ${{ github.sha }} # optional; defaults to the triggering commit SHA |
| 94 | + sourcemaps: ./dist # only if you have JS source maps to upload |
| 95 | +``` |
| 96 | +
|
| 97 | +The action's default release name is the GitHub SHA that triggered the workflow — fine, as long as the |
| 98 | +SDK tags the same value. Wire `SENTRY_RELEASE=${{ github.sha }}` into the build step if you rely on |
| 99 | +that default. |
| 100 | + |
| 101 | +Self-hosted Sentry also needs `SENTRY_URL`. |
| 102 | + |
| 103 | +## Path C — `sentry-cli`, any CI |
| 104 | + |
| 105 | +The explicit form. Use it for CI providers without an integration, for mobile builds, and whenever you |
| 106 | +need to see each step: |
| 107 | + |
| 108 | +```bash |
| 109 | +export SENTRY_AUTH_TOKEN=... # from CI secrets |
| 110 | +export SENTRY_ORG=my-org |
| 111 | +export SENTRY_PROJECT=my-project |
| 112 | +
|
| 113 | +VERSION=$(sentry-cli releases propose-version) # or your own version string |
| 114 | +
|
| 115 | +sentry-cli releases new "$VERSION" |
| 116 | +
|
| 117 | +# ... build, and upload source maps / debug files for this build ... |
| 118 | +
|
| 119 | +sentry-cli releases set-commits "$VERSION" --auto |
| 120 | +sentry-cli releases finalize "$VERSION" |
| 121 | +
|
| 122 | +# ... deploy ... |
| 123 | +
|
| 124 | +sentry-cli deploys new --release "$VERSION" -e production |
| 125 | +``` |
| 126 | + |
| 127 | +Notes on the individual steps: |
| 128 | + |
| 129 | +- **`new`** takes multiple projects when a release spans them: `-p project1 -p project2`. Remember |
| 130 | + releases are org-global — prefix the version accordingly. |
| 131 | +- **`--finalize`** on `new` collapses steps 1 and 5 if you don't need the window in between. Finalizing |
| 132 | + separately, at deploy time, is more accurate: the finalize timestamp is what "the next release" means |
| 133 | + when resolving issues, and it's the base release for `--auto` commit association. |
| 134 | +- **`set-commits --auto`** discovers the repo from the working directory and associates everything |
| 135 | + between the previous release's head commit and the current `HEAD`. With no SCM integration installed |
| 136 | + it falls back to the local git tree (the last 10–20 commits on a first release, tunable with |
| 137 | + `--initial-depth`); `--local` makes that fallback the explicit default. |
| 138 | +- When the build can't reach the repo, name the commits: `--commit "owner/repo@<sha>"`, repeated per |
| 139 | + repo, or a range `--commit "owner/repo@<prev>..<current>"`. The repo name must match what it's called |
| 140 | + in Sentry — `sentry-cli repos list` prints the valid names. |
| 141 | +- **`--ignore-missing`** rescues `set-commits` when a commit from the previous release no longer exists |
| 142 | + (amend, rebase, squash-merge, force-push). It falls back to the default commit count instead of |
| 143 | + failing the build. |
| 144 | +- **`deploys new`** accepts `-t <seconds>` to record how long the deploy took, and `deploys list |
| 145 | + --release "$VERSION"` to read them back. Deploys can't be deleted. |
| 146 | + |
| 147 | +### Sending commit metadata without the CLI |
| 148 | + |
| 149 | +When the deploy environment can't run `sentry-cli` at all, POST the commits with the release. This is |
| 150 | +also the path for orgs that won't connect an SCM integration: |
| 151 | + |
| 152 | +```bash |
| 153 | +curl https://sentry.io/api/0/organizations/<org>/releases/ \ |
| 154 | + -X POST \ |
| 155 | + -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \ |
| 156 | + -H 'Content-Type: application/json' \ |
| 157 | + -d '{ |
| 158 | + "version": "2.0rc2", |
| 159 | + "projects": ["project-1"], |
| 160 | + "commits": [{ |
| 161 | + "id": "8371445ab8a9facd271df17038ff295a48accae7", |
| 162 | + "repository": "owner-name/repo-name", |
| 163 | + "author_name": "Author Name", |
| 164 | + "author_email": "author@example.com", |
| 165 | + "timestamp": "2018-09-20T11:50:22+03:00", |
| 166 | + "message": "This is the commit message.", |
| 167 | + "patch_set": [ |
| 168 | + {"path": "path/to/added-file.html", "type": "A"}, |
| 169 | + {"path": "path/to/modified-file.html", "type": "M"}, |
| 170 | + {"path": "path/to/deleted-file.html", "type": "D"} |
| 171 | + ] |
| 172 | + }] |
| 173 | + }' |
| 174 | +``` |
| 175 | + |
| 176 | +Two fields carry the weight: **`patch_set`** (types `A`dd, `M`odify, `D`elete) is what powers suspect |
| 177 | +commits and suggested assignees — omit it and you get a commit list and nothing else — and |
| 178 | +**`author_email`** is what makes the suggested assignee resolvable. `timestamp` controls ordering; |
| 179 | +without it, commits stay in the order given. |
| 180 | + |
| 181 | +## Path D — mobile and Flutter |
| 182 | + |
| 183 | +The SDK build plugins on these platforms upload **artifacts** but do not manage releases, with one |
| 184 | +exception: |
| 185 | + |
| 186 | +| Platform | Release object | |
| 187 | +|---|---| |
| 188 | +| Flutter / Dart | `sentry_dart_plugin` handles it — `release` defaults to `name@version` from `pubspec.yaml` and `commits` defaults to `auto`. `ignore_missing: true` is available for rewritten history. | |
| 189 | +| Android | The Gradle plugin uploads mappings and source bundles only. Create the release from CI with Path B or C, using the same `packageName@versionName+versionCode` string the SDK tags. | |
| 190 | +| Apple / Cocoa | The Xcode build phase uploads dSYMs only. Same: create the release from CI. | |
| 191 | +| React Native | The bundled `sentry-cli` build integration creates the release for default names. Custom `release`/`dist` values break it — then it's Path C, plus manual source map upload. | |
| 192 | + |
| 193 | +## Related |
| 194 | + |
| 195 | +- [`tagging.md`](tagging.md) — the name both halves must agree on. |
| 196 | +- [`suspect-commits.md`](suspect-commits.md) — what commit association actually unlocks. |
| 197 | +- [`troubleshooting.md`](troubleshooting.md) — when the pipeline runs but Sentry shows nothing. |
0 commit comments