Triggering workflows on tags and releases Jump to heading
Release pipelines are usually triggered by a tag: someone (or something) pushes v2.4.0, and the pipeline builds, signs and publishes. It sounds simple, and then the details arrive. Should the trigger be the tag push or the forge’s “release published” event? Does a pre-release tag v2.4.0-rc.1 start the same pipeline as a final release? Why did the workflow run twice — once for the branch push and once for the tag? Why did a tag created by another workflow not trigger anything? This page maps out the trigger options, their filters and their pitfalls, so a release pipeline starts exactly once, for exactly the right refs, within CI/CD pipeline trigger mapping.
When to use this approach Jump to heading
- Releases are cut by pushing tags, and a pipeline should build and publish each one.
- You publish releases through the forge’s release feature and want the pipeline tied to it.
- Pre-releases and final releases need different handling.
- Your pipelines already map branch and path triggers, as in optimizing CI triggers for path-specific changes, and you are adding release triggers.
Step 1 — Choose between tag push and release events Jump to heading
There are two different events a release pipeline can listen for. A tag push fires when the tag ref is created. A release event fires when someone publishes a release object on the forge, which usually references a tag that already exists.
# Option A: tag push
on:
push:
tags: ["v*.*.*"]
# Option B: release published
on:
release:
types: [published] Choose tag push when tags are the source of truth and created by a guarded process, as in signing release tags from a pipeline. Choose release events when a person publishes releases through the forge and the notes matter.
Step 2 — Filter tags precisely Jump to heading
Glob patterns on tags are easy to get too loose. v* matches v2.4.0, v2.4.0-rc.1, vendor-update and very-old-tag. Use a pattern that matches only version tags, and route pre-releases separately.
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+" # final releases only
- "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+" # release candidates jobs:
publish:
runs-on: ubuntu-latest
steps:
- id: kind
run: |
case "$GITHUB_REF_NAME" in
*-rc.*) echo "channel=next" >> "$GITHUB_OUTPUT" ;;
*) echo "channel=latest" >> "$GITHUB_OUTPUT" ;;
esac
- run: ./scripts/publish.sh --channel "${{ steps.kind.outputs.channel }}" GitHub’s filter patterns support + for “one or more” and character classes, but not full regular expressions. Test the pattern against a list of real tag names before relying on it.
Step 3 — Stop double runs from branch and tag pushes Jump to heading
Pushing a commit and a tag together (git push --follow-tags) delivers two events: one for the branch and one for the tag. A workflow listening to both branches and tags under push runs twice for the same commit.
# ci.yml — branches only
on:
push:
branches: [main]
tags-ignore: ["**"]
# release.yml — tags only
on:
push:
tags: ["v[0-9]+.[0-9]+.[0-9]+"] Inside a combined workflow, guard publishing steps with the ref type instead: if: startsWith(github.ref, 'refs/tags/').
Step 4 — Know which refs the run sees Jump to heading
On a tag push, the checked-out commit is the tagged commit, and GITHUB_REF is refs/tags/<name>. The run has no branch checked out. Scripts that call git describe, compute changelogs or check which branch the commit is on need history and tags fetched explicitly.
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for describe / changelog
fetch-tags: true
- run: |
git describe --tags --exact-match # confirms the tag points here
git merge-base --is-ancestor HEAD origin/main || { echo "tag is not on main"; exit 1; } The ancestry guard matters: anyone with push access can tag a commit on a side branch, and without it the release pipeline would publish that commit.
Step 5 — Handle tags created by automation Jump to heading
A tag pushed by a workflow using the default job token does not trigger other workflows on GitHub, to prevent recursive runs. If one workflow creates the release tag and another should publish it, either publish in the same workflow, use workflow_run to chain them, or push the tag with an app token.
# Chain: run release.yml when tag.yml completes successfully
on:
workflow_run:
workflows: ["Create release tag"]
types: [completed]
jobs:
publish:
if: github.event.workflow_run.conclusion == 'success' Chaining is covered in more depth in chaining workflows with workflow_run.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does deleting and re-pushing a tag re-run the release? Jump to heading
Yes — a new tag push is a new event. That is one more reason never to move published tags; see moving or deleting a published tag.
Can I trigger on annotated tags only? Jump to heading
Not with the trigger filter. Check inside the job: git cat-file -t "$GITHUB_REF_NAME" prints tag for annotated tags and commit for lightweight ones, and the job can exit early for the latter.
What about GitLab? Jump to heading
GitLab pipelines run for tags with CI_COMMIT_TAG set. Use rules: - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ on release jobs, and exclude them from branch pipelines; the general rule syntax is in mapping GitLab CI rules to branch and path changes.
Related Jump to heading
- CI/CD Pipeline Trigger Mapping — the parent topic.
- Manual Dispatch Workflows with Inputs — releasing on demand instead of on tag.
- Generating Provenance for a Tagged Release — what the release workflow should produce.
- Tagging Pre-Releases and Release Candidates — naming tags so filters stay simple.