How do you cut a GitHub release automatically when a version tag is pushed?
answer
- The tag push is the only human act
- Say what the token may write
- Never publish before the files land
- A token that writes may not re-trigger
- Who can push v* is now a security question
basics
~20 sTrigger a workflow on pushes of tags matching your version pattern, grant it contents: write, build the artifacts, then create the release from the pushed tag with generated notes and upload the artifacts as assets before publishing.
solid answer
~50 sThe shape is: `on: push: tags:` with a pattern such as `v*`, a job with `permissions: contents: write`, a build step, then a release step that uses the pushed tag — available as `github.ref_name` — as `tag_name`, with generated notes and the built files as assets. Three details separate a working pipeline from a fragile one. **Least privilege**: set `permissions` explicitly on the job so the automatic `GITHUB_TOKEN` gets `contents: write` and nothing else. **Atomicity**: create the release as a draft, upload every asset, then flip it to published, so consumers never see a release with half its files. **The token chaining rule**: a release created using `GITHUB_TOKEN` does **not** trigger workflows listening on the `release` event — GitHub deliberately blocks that recursion, so a downstream publish job must run in the same workflow or be driven by an App or personal token.
code
yaml · 23 linesname: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- name: Build
run: ./gradlew --no-daemon assemble
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
--title "$GITHUB_REF_NAME" \
--generate-notes \
build/libs/*.jargo deeper
Know that a GitHub Actions workflow can trigger on a tag push with on: push: tags: and create the release from that tag, so releases are not assembled by hand.
Explain the pieces — the tag pattern, github.ref_name as the tag, permissions: contents: write for the automatic token, and attaching built files as release assets.
Demonstrate the operational knowledge: draft-then-publish for multi-job builds, conditional prerelease and latest flags, and the rule that GITHUB_TOKEN-created releases do not trigger release-event workflows.
Own the tag namespace as a privileged surface. Once a tag push produces a published artifact, who may create matching tags is a supply-chain control, and the release pipeline's provenance is what an audit will ask about.
## Why the tag is the trigger Tagging is the human decision — "this commit is 2.5.0". Everything after it is mechanical, and mechanical steps done by hand drift: someone forgets a checksum, uploads a locally built jar from a dirty tree, or writes notes that omit a change. Making the tag push the trigger means the released artifact is always the one CI built from exactly that commit, which is the property you actually care about when a security question arrives eighteen months later. ## The trigger ``` on: push: tags: - 'v*' ``` A tag-push trigger runs with the tag as the ref, so `github.ref_name` is the tag name — the value you feed to the release. Choose the pattern deliberately: `v*` catches `v2.5.0` and also `v2.5.0-rc.1`, which is usually what you want since candidates should be released too, just flagged as prereleases. ## Permissions Creating a release is a write to repository contents. Declare it at the job level: ``` permissions: contents: write ``` Declaring `permissions` at all switches the token to exactly what you list, so this is a narrowing, not a widening. Organisations that set the default token permissions to read-only make this mandatory; on repositories where it is still permissive, writing it anyway documents intent and survives the setting being tightened later. ## Creating the release Two routes with the same effect: `POST /repos/{owner}/{repo}/releases` via `curl`, or `gh release create` with `GH_TOKEN` in the environment. Either way, pass: - the tag from `github.ref_name`, - `--generate-notes` (or `generate_release_notes: true`) so the changelog comes from merged pull requests, - the asset paths, - `--prerelease` when the tag carries a prerelease suffix, and `--latest=false` when the tag is on a maintenance line. Those last two are conditionals worth writing: a release-candidate tag that quietly becomes Latest is a genuine incident, because installers following Latest downgrade or jump onto unstable code silently. ## Making it atomic If more than one job produces artifacts — several platforms, several packages — publishing first and uploading afterwards exposes an incomplete release. The fix is the draft: 1. A first job creates the release with `--draft`. 2. Build jobs upload their assets into it. 3. A final job runs `gh release edit <tag> --draft=false`. Every publicly visible state is then complete. ## The chaining trap This is the detail interviewers use to separate reading from experience. **Events triggered by `GITHUB_TOKEN` do not start new workflow runs.** A release created by your workflow using the automatic token therefore fires no `release` event, so a separate workflow with `on: release: types: [published]` never runs. People lose hours to this. The options are: do the downstream work in the same workflow (simplest, and usually right); or create the release with a GitHub App installation token or a fine-grained token stored as a secret, which does chain. Choose the second only when the downstream job genuinely belongs to another workflow or repository, since it reintroduces a credential you now have to rotate. ## Guarding who can tag Since the tag now triggers a production artifact, the tag namespace is a privileged surface. GitHub rulesets can target tags as well as branches, restricting who may create or update tags matching `v*`. Without that, anyone with write access can cut a release by pushing a tag, which is exactly the gap an attacker with a stolen contributor token looks for. ## What still needs a human Generation gives an accurate inventory of merged pull requests. It cannot write the migration note for a breaking change. The common pattern is to have the workflow create the release as a draft with generated notes, and require a maintainer to add the prose lead and publish — automation for the mechanical parts, judgment retained where judgment is needed.
- Your release workflow succeeds, but a separate workflow listening on the release event never runs. Why?Events produced using the automatic `GITHUB_TOKEN` do not start new workflow runs — GitHub blocks that recursion deliberately. Either move the downstream work into the same workflow, or create the release with a GitHub App installation token or a fine-grained token stored as a secret, accepting the credential you then have to rotate.
- Why declare permissions: contents: write rather than relying on the default?Declaring `permissions` replaces the token's scope with exactly what you list, so it narrows rather than widens. It also makes the workflow independent of an organisation default that may be read-only today or tightened tomorrow, and it documents for a reviewer precisely which write this job needs.
- Anyone with write access can push a v2.6.0 tag and trigger a production artifact. How do you contain that?Use a GitHub ruleset targeting tags rather than branches, restricting who may create or update refs matching `v*`. Tag pushes now produce signed, published artifacts, so the tag namespace is a privileged surface; without a restriction, a stolen contributor token is enough to publish a release under your name.
- Should the workflow publish the release outright, or leave a draft?Publish outright when the notes are entirely generated and no human judgment is required. Leave a draft when a release needs a migration note or a breaking-change summary a generator cannot write — the workflow still builds and attaches every artifact reproducibly, and a maintainer adds prose and presses publish.
saying these in an interview costs you the question
- Uploads locally built artifacts to the release by hand
- Assumes GITHUB_TOKEN-created releases trigger release workflows
- Publishes the release before assets finish uploading
- Leaves the token at default permissions for a write job
- Ignores who is allowed to push a version tag