Signing bot commits made by CI workflows Jump to heading
Pipelines commit more than people expect. A release job bumps a version file. A nightly job regenerates API clients or documentation. A formatting bot pushes fixes back to a pull request branch. Each of those commits arrives at your signature gate unsigned, and the gate does the right thing and rejects it — so somebody adds an exemption, then another, and soon the gate trusts anything with “bot” in the email. The durable fix is to give automation a real signing identity with the same properties as a person’s: its own key, its own principal, registered and verifiable. This page builds that for a CI workflow, as part of signed commits in CI pipelines.
When to use this approach Jump to heading
- A workflow creates commits on branches protected by a signature requirement.
- You currently exempt bot accounts from signature checks and want to remove the exemption.
- The commits are created with
git commiton the runner, not through a forge API — for API-created commits see verified bot commits with a GitHub App identity. - You can store a key securely in your CI system, ideally scoped as described in protecting CI signing keys with environment secrets.
Step 1 — Create one identity per bot role Jump to heading
Give each kind of automated change its own identity, rather than one “ci-bot” for everything. Distinct principals let your gates and audits distinguish a release bump from a formatting fix, and let you revoke one without touching the other.
for role in release-bot format-bot codegen-bot; do
ssh-keygen -t ed25519 -N '' -C "signing:$role@example.com:$(date +%Y-%m)" -f "$role"
done
ls *.pub Step 2 — Register the keys and trust them Jump to heading
Add each public key to the allowed signers file with its own principal. If the forge should show the commits as verified too, register each key as a signing key on the bot’s machine account, and verify the bot’s email on that account.
# allowed_signers — automation block, maintained by hand
[email protected] namespaces="git" ssh-ed25519 AAAA... signing:release-bot:2026-10
[email protected] namespaces="git" ssh-ed25519 AAAA... signing:format-bot:2026-10
[email protected] namespaces="git" ssh-ed25519 AAAA... signing:codegen-bot:2026-10 Store each private key as a secret scoped to the workflows that need it, then delete the local copies.
gh secret set RELEASE_BOT_KEY --env release-signing < release-bot
gh secret set FORMAT_BOT_KEY < format-bot
shred -u release-bot format-bot codegen-bot Step 3 — Configure Git in the job and commit with signing Jump to heading
The job configures identity and signing for its own checkout only — --global on a shared runner would leak into other jobs. The key goes into an in-memory agent.
#!/bin/sh
# ci/bot-commit.sh "<message>" — signs and commits whatever is staged
set -eu
eval "$(ssh-agent -s)" >/dev/null; trap 'ssh-agent -k >/dev/null' EXIT
printf '%s\n' "$BOT_KEY" | ssh-add - >/dev/null; unset BOT_KEY
git config user.name "$BOT_NAME"
git config user.email "$BOT_EMAIL"
git config gpg.format ssh
git config user.signingKey "key::$(ssh-add -L | head -1)"
git config commit.gpgSign true
git diff --cached --quiet && { echo "nothing to commit"; exit 0; }
git commit -m "$1"
git log -1 --format='%h %G? %ce' # Workflow step using it
- run: |
npm version patch --no-git-tag-version
git add package.json package-lock.json
./ci/bot-commit.sh "chore(release): bump version to $(node -p "require('./package.json').version")"
git push origin HEAD:main
env:
BOT_KEY: ${{ secrets.RELEASE_BOT_KEY }}
BOT_NAME: release-bot
BOT_EMAIL: release-[email protected] Step 4 — Constrain what each bot may change Jump to heading
A signing identity proves who made the commit; it does not limit what the commit contains. Pair each bot identity with a check that its commits touch only the paths it is supposed to.
# In the gate, after signature verification
case "$(git log -1 --format='%GS' "$c")" in
[email protected])
git diff-tree --no-commit-id --name-only -r "$c" | grep -vE '\.(js|ts|css|md)$' && {
echo "$c: format-bot touched non-formattable files"; exit 1; } ;;
[email protected])
git diff-tree --no-commit-id --name-only -r "$c" | grep -vxE 'package(-lock)?\.json|CHANGELOG\.md' && {
echo "$c: release-bot touched unexpected files"; exit 1; } ;;
esac Step 5 — Avoid the loop: bot commits triggering bot workflows Jump to heading
A workflow that commits and pushes can trigger itself. Forges usually suppress workflow triggers for pushes made with the default job token, but pushes made with a personal access token or app token do trigger. Guard against loops explicitly.
on:
push:
branches: [main]
jobs:
bump:
if: github.actor != 'release-bot' && !contains(github.event.head_commit.message, 'chore(release)') # Verification: the bot's own push does not start another run
gh run list --workflow bump.yml --limit 5 --json event,headSha,conclusion Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Can the bot sign with the forge’s own key instead? Jump to heading
Only for commits created through the forge’s API, where the forge signs on the bot’s behalf. Commits made with git commit on a runner are signed by whatever key the runner holds. If API-created commits fit your use case, they avoid managing a key at all.
Should bot commits go through pull requests? Jump to heading
For anything that changes behaviour — code generation, dependency changes — yes. For a version bump immediately before tagging a release, pushing directly with a narrowly scoped identity is common and reasonable, as long as the gate’s path check is in place.
What if the bot needs to commit to a pull request branch from a fork? Jump to heading
It cannot, safely. Workflows triggered by fork pull requests should not receive signing secrets at all. Have the bot suggest the change as a review comment or a patch artefact instead.
Related Jump to heading
- Signed Commits in CI Pipelines — the parent topic.
- Signing Release Tags from a Pipeline — the tag that usually follows a bot’s version bump.
- Rotating CI Signing Identities Without Breaking Verification — keeping these keys fresh.
- Rolling Out Signature Enforcement in Warn-Only Mode — where bot stragglers usually surface.