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.

Tag push against release publishedA tag push trigger fires the moment a matching tag is pushed, from any source that can push. A release-published trigger fires only when a release is published through the forge, which adds a manual or automated gate and carries release notes, but does not fire for tags pushed without a release.push: tagsrelease: publishedfires whentag ref createdrelease object publishedneeds forge UI/APInoyespre-release flagfrom tag namerelease.prereleasecarries notesnoyespick one — listening to both is the usual cause of double releases
# 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.

Why a release ran twiceA developer pushes main and a new tag together. The forge emits one push event for refs/heads/main and another for refs/tags/v2.4.0. A workflow that listens for pushes to main and for version tags starts two runs on the same commit, one of which publishes.developerforgeworkflowpush main + v2.4.0push refs/heads/mainpush refs/tags/v2.4.0two runs, same commitsplit branch CI and release publishing into separate workflows, or guard on the ref type
# 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'
A tag was created but nothing ranIf the tag was pushed by a workflow with the default token, forge rules suppress further triggers, so chain with workflow_run or use an app token. If the tag name did not match the pattern, fix the filter. If the workflow file is not on the default branch, tag events may not see it.Why didn't the release workflow start?pushed by GITHUB_TOKENTrigger suppressedworkflow_run / app tokenname didn't matchFilter too narrowtest the patternfile not on defaultWorkflow not loadedmerge it to mainthe first cause accounts for most 'nothing happened' reports

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.