Release workflow never runs after a bot pushes a tag with GITHUB_TOKEN
Our versioning workflow created a tag and pushed it, and the release workflow that listens for tags never started. Events created with the default GITHUB_TOKEN do not trigger new workflow runs. We switched to a GitHub App token for the push and made the release explicit with workflow_call.
We automated releases for an internal SDK: on merge to main, a version workflow computes the next semantic version from commit messages, updates the changelog, commits it, creates a tag like v2.14.0 and pushes. A separate release workflow runs on: push: tags: ['v*'], builds artifacts and publishes them.
The version workflow ran green. The tag existed on GitHub. The release workflow never started. No failed run, no skipped run, nothing in the Actions tab.
$ git ls-remote --tags origin | tail -1
9f3c1e2a... refs/tags/v2.14.0
Reproducing it
Pushing the same tag by hand from a laptop triggered the release workflow immediately. Pushing it from the version workflow did not. The difference was the credential:
- uses: actions/checkout@v4 # persists GITHUB_TOKEN for git push
- run: |
git tag "v${{ steps.semver.outputs.version }}"
git push origin --tags
What we ruled out
- Tag filter mismatch. The pattern
v*matchedv2.14.0, and a manual push proved the trigger worked. - Workflow disabled. It was enabled and ran on manual pushes.
- Branch protection or tag protection rules. The push succeeded; the tag was there.
Root cause
GitHub does not start new workflow runs for events caused by the repository's GITHUB_TOKEN, with the exceptions of workflow_dispatch and repository_dispatch. This is deliberate, to stop workflows from triggering each other in infinite loops. Our tag push was authenticated with GITHUB_TOKEN (persisted by actions/checkout), so the push event for the tag was created but no workflows were triggered by it.
What is happening under the hood
Each workflow job gets an installation token for the GitHub Actions app, exposed as GITHUB_TOKEN. Actions it performs (pushes, tags, issue comments, PRs) are attributed to github-actions[bot], and events from that identity are filtered out of workflow triggers. Events caused by other identities, a personal access token, or a token from your own GitHub App, do trigger workflows normally.
actions/checkout stores the token in the local git config by default (persist-credentials: true), so later git push commands use it without any visible authentication step, which makes the cause easy to miss.
Environment
| Component | Version / value |
|---|---|
| CI | GitHub Actions, GitHub-hosted runners |
| Actions | actions/checkout@v5 |
| Workflows | version.yml on push to main, release.yml on tag push v* |
| Repository | Private, organisation-owned |
Minimal reproduction
# .github/workflows/tagger.yml
on: workflow_dispatch
permissions: {contents: write}
jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: git tag demo-$(date +%s) && git push origin --tags
# .github/workflows/on-tag.yml
on: {push: {tags: ['demo-*']}}
jobs:
echo: {runs-on: ubuntu-latest, steps: [{run: echo triggered}]}
on-tag.yml never runs from the tagger.
The fix
We considered two fixes and used both for different reasons.
1. Push with a GitHub App installation token. We created an internal GitHub App with contents: write on the SDK repository, stored its app id and private key as secrets, and generated a short-lived token in the workflow:
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
token: ${{ steps.app-token.outputs.token }}
- run: |
git config user.name "release-bot[bot]"
git config user.email "${{ vars.RELEASE_BOT_USER_ID }}+release-bot[bot]@users.noreply.github.com"
git tag "v${VERSION}"
git push origin "v${VERSION}"
Tags pushed this way trigger release.yml like a human push would, and the tag shows the app as the actor.
2. Make the release an explicit call. For the SDK we also refactored release.yml into a reusable workflow and call it from the version workflow after tagging. The release no longer depends on event propagation at all, and the whole chain shows up as one run:
# release.yml
on:
push:
tags: ['v*'] # still works for manual tags
workflow_call:
inputs:
version: {type: string, required: true}
# version.yml
jobs:
version:
outputs:
version: ${{ steps.semver.outputs.version }}
# ... computes, commits, tags with the app token
release:
needs: version
uses: ./.github/workflows/release.yml
with:
version: ${{ needs.version.outputs.version }}
secrets: inherit
Alternatives and trade-offs
- Personal access token. Works, but ties automation to a person, uses a long-lived credential, and breaks when they leave. We do not use PATs for automation.
workflow_dispatchfrom the first workflow via the API withGITHUB_TOKEN. Allowed by the exception and simple, but the release then runs with the default branch ref unless you pass inputs carefully.- Deploy keys. SSH keys per repository; they trigger workflows but are long-lived and cannot be scoped as narrowly as app permissions.
How to test the fix
We ran the version workflow on a test repository with the same setup. The tag push from the app token started the tag-triggered workflow, and the workflow_call path ran the release job in the same run. We also confirmed that the app's commit to main (the changelog) did not create an infinite loop: the version workflow skips commits with [skip release] in the message and checks the actor.
Confirming in production
- The next three merges to
mainproduced tags and published releases automatically. - The audit log shows the app as the actor for tags and changelog commits, separate from humans.
Regression guard
- The release workflow posts to the team channel on success; a missing post after a version bump is noticed quickly.
- A shared workflow template for releases uses the app token pattern, so new repositories do not repeat the mistake.
Security implications
Bypassing the recursion protection means you must prevent loops yourself, which we did with the skip marker and actor check. The app's private key is a powerful secret: it is stored only in this repository's environment, the app is installed only on the repositories that need it, and its permissions are limited to contents: write. Tokens it issues expire after an hour.
Prevention checklist
- Know that
GITHUB_TOKEN-created events do not trigger workflows, except dispatch events. - Use a GitHub App token when automation must trigger other workflows.
- Prefer
workflow_callfor chained steps you own. - Guard against self-triggering loops when using non-default tokens.
- Avoid personal access tokens for automation.
References
- GitHub, triggering a workflow from a workflow — https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#triggering-a-workflow-from-a-workflow
- Automatic token authentication (GITHUB_TOKEN) — https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication
- actions/create-github-app-token — https://github.com/actions/create-github-app-token
- Reusing workflows with workflow_call — https://docs.github.com/en/actions/sharing-automations/reusing-workflows
Add a label
Embed this post
<iframe src="https://hibug.me/github-actions-push-with-github-token-no-trigger" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.