Eleven releases. Eleven green "Create Release" runs. Eleven tags sitting on GitHub. And my npm package was still on the version from three weeks earlier.
The publish workflow had never run once. No failure, no red X, no email. It simply didn't exist in the Actions tab. My release workflow created tags and releases with the default GITHUB_TOKEN, and GITHUB_TOKEN won't trigger workflows. That's a deliberate rule, not a bug, and it took me an embarrassing amount of time to find it.
TL;DR
- Events created with the built-in
GITHUB_TOKEN(pushes, tags, releases, pull requests, comments) do not start new workflow runs. The two exceptions areworkflow_dispatchandrepository_dispatch. - GitHub does this on purpose so a workflow can't trigger itself in an infinite loop.
- The symptom is silence: no failed run, just a missing one. Bot-opened PRs show required checks stuck on "Expected — Waiting for status to be reported".
- Fixes: use a GitHub App token, a fine-grained PAT, an explicit
workflow_dispatchcall, or merge the two workflows into one job. - If you switch to a token that does trigger workflows, add a loop guard.
What exactly broke?
My setup looked like what half the internet runs:
# release.yml
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- run: |
VERSION=$(node -p "require('./package.json').version")
git tag "v$VERSION"
git push origin "v$VERSION"
gh release create "v$VERSION" --generate-notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# publish.yml
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm publish
The tag push is real. You can see the tag in the repo. But publish.yml never fires, because the push was authenticated with GITHUB_TOKEN. Same story if publish.yml listened on release: types: [published] instead. The release exists. The event is swallowed.
The kicker: when I pushed a tag by hand from my laptop to test it, publish ran perfectly. So my "test" proved the workflow was fine, and I went looking for bugs in the wrong file for an hour.
Why won't GITHUB_TOKEN trigger workflows?
GitHub blocks it to prevent recursive runs. If a workflow on push could push a commit with GITHUB_TOKEN and trigger itself, one formatting bot would spin forever and burn everyone's minutes.
So the rule is blunt: anything your workflow does with GITHUB_TOKEN is treated as "the workflow did it," and the workflow system ignores it as a trigger. The only events that still fire are workflow_dispatch and repository_dispatch, because those are explicit "please run this" requests, not side effects.
This applies no matter how the token reaches the API:
-
git pushafteractions/checkout(checkout storesGITHUB_TOKENin the local git config by default) -
ghCLI withGH_TOKEN: ${{ secrets.GITHUB_TOKEN }} -
actions/github-scriptusing the default token - Any marketplace action that defaults to
github.token, which is most of them
That last point is the sneaky one. Plenty of release and PR-bot actions take a token input that silently defaults to GITHUB_TOKEN. You never typed it, so you never thought about it.
Which GitHub Actions workflows are affected?
Any workflow whose trigger is an event a bot produced. The common casualties I've hit or watched others hit:
-
Tag-triggered publish jobs. A release workflow pushes
v1.2.3, theon: push: tagsworkflow never runs. My exact bug. -
release: publisheddeploys. A release-automation action creates the release, the deploy listening for it stays idle. -
CI on bot-opened pull requests. A workflow opens a PR (version bumps, generated code, dependency syncs) using
GITHUB_TOKEN. Yourpull_requestCI never runs on it. If those checks are required by branch protection, the PR sits forever on "Expected — Waiting for status to be reported" and nobody can merge it without admin override. -
Commit-then-test chains. A formatter or codegen job commits back to the branch, and the follow-up test workflow on
pushdoesn't run on the new commit. You end up merging an untested commit that looks tested because the previous commit was green. -
Comment-driven bots. A workflow posts an issue comment hoping to kick an
issue_commentworkflow. Nothing happens.
The pattern: if a human did the same action, it would work. If your workflow did it with the default token, it won't.
How do I make a GitHub Actions push trigger another workflow?
You have four real options. Pick based on who should "own" the action.
Fix 1: Use a GitHub App installation token (my pick)
Create a small GitHub App, install it on the repo with the permissions you need (Contents: write, Pull requests: write), and store its ID and private key. Then mint a short-lived token inside the job:
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- run: |
git tag "v$VERSION"
git push origin "v$VERSION"
gh release create "v$VERSION" --generate-notes
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
Passing the token to actions/checkout matters. Checkout persists whatever token it was given into the git config, so the later git push uses the App identity instead of GITHUB_TOKEN. Forget that line and you've fixed gh but not git push.
Why I prefer this: the token expires in an hour, it isn't tied to a person who might leave, and the commits show up as your-app[bot], which makes loop guards easy.
Fix 2: Use a fine-grained personal access token
Same idea, faster to set up. Create a fine-grained PAT scoped to the one repo, store it as a secret, and use it in place of GITHUB_TOKEN in both checkout and gh.
The trade-off is ownership. Every action shows up as you. It expires on whatever schedule you set, and the day it does, your releases go silent again. When you leave the team, so does the token.
Fix 3: Call workflow_dispatch explicitly
Since workflow_dispatch is one of the two exceptions, you can keep GITHUB_TOKEN and trigger the downstream workflow by name:
# publish.yml
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
required: true
# end of release.yml
permissions:
contents: write
actions: write # required to dispatch workflows
- run: gh workflow run publish.yml --ref "v$VERSION" -f tag="v$VERSION"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
No extra secrets. The catch: the actions: write permission is easy to forget, and the coupling is now explicit, so renaming publish.yml breaks the release job.
This doesn't help with bot-opened PRs. You can dispatch CI, but branch protection waits for checks reported against the PR, so for that case use Fix 1 or 2.
Fix 4: Don't chain at all
Often the cleanest answer. If publish always follows release, make it a second job in the same workflow:
jobs:
release:
# ...creates the tag
outputs:
version: ${{ steps.v.outputs.version }}
publish:
needs: release
# ...npm publish
No events, no tokens, no invisible walls. I ended up doing this for npm publish and keeping the App token only for the PR bot.
Won't a non-GITHUB_TOKEN push cause an infinite loop?
It can, which is exactly why GitHub put the wall there. Once you swap in an App token or PAT, a workflow that commits on push will trigger itself again. Add a guard:
jobs:
format:
if: github.actor != 'release-bot[bot]'
Or put [skip ci] in the bot's commit message. GitHub skips push and pull_request workflows for commits containing it. Or use paths-ignore so changes to CHANGELOG.md and package.json don't retrigger the release workflow. One of the three, every time.
How do I spot this bug quickly?
Look for a missing run, not a failed one. My checklist now:
- The downstream workflow has zero runs for the event you expected, while the upstream run is green.
- Pushing the same tag or opening the same PR by hand makes it work.
- A bot PR shows required checks as "Expected" with no run attached.
- Grep the upstream workflow for
secrets.GITHUB_TOKEN,github.token, and any action with an optionaltokeninput. Assume the default isGITHUB_TOKENuntil the README says otherwise.
So why doesn't GITHUB_TOKEN trigger workflows?
GITHUB_TOKEN won't trigger workflows because GitHub ignores every event created with it, apart from workflow_dispatch and repository_dispatch, to stop workflows from triggering themselves forever. Tags, releases, pushes and pull requests made with the default token are real, but they start no new runs. You won't see an error, just a missing run. To chain workflows, authenticate with a GitHub App token or fine-grained PAT (and pass it to actions/checkout too), dispatch the downstream workflow explicitly with actions: write, or merge both steps into one workflow with needs:. If you switch tokens, add a loop guard before the next push.
Written by the developer behind Preterview, an interview prep platform.
Top comments (0)