ENG-59Recommended
Sign release tags and require verified commits on main
Sign every release tag. Require a verified signature on every commit that reaches the main branch, so a stolen password alone cannot add code under someone's name.
How to do it
The author name on a commit is free text, so anyone can write any name. A signature ties the commit to a key the person holds. Git signs with GPG, SSH or X.509 keys, and an SSH key that already pushes code is the simplest to set up. Upload the signing key to the code host. Turn on required signatures for the main branch in its branch rules. Commits that the code host creates for a merge are signed by the host. Sign release tags with git tag -s, so consumers can check them as Make release tags and files immutable says. Signing proves who committed, not that the code is safe, so review still follows Protect the main branch.
Examples
Shell example
# Sign with the SSH key you already use
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# Check: main requires signatures, and the last release tag verifies
gh api repos/example/shop/rules/branches/main --jq '.[].type' | grep -qx required_signatures || { echo "FAIL: signatures not required"; exit 1; }
git verify-tag "$(git describe --tags --abbrev=0)"
Python example
# CI gate: every commit in the pull request carries a good signature
import subprocess, sys
commits = subprocess.run(["git", "rev-list", "origin/main..HEAD"], capture_output=True, text=True, check=True).stdout.split()
unsigned = [c for c in commits if subprocess.run(["git", "verify-commit", c], capture_output=True).returncode != 0]
if unsigned:
sys.exit(f"FAIL: unsigned commits {', '.join(unsigned)}")
JavaScript example
// The same gate in Node.js
import { execFileSync, spawnSync } from 'node:child_process';
const commits = execFileSync('git', ['rev-list', 'origin/main..HEAD'], { encoding: 'utf8' }).split('\n').filter(Boolean);
const unsigned = commits.filter(commit => spawnSync('git', ['verify-commit', commit]).status !== 0);
if (unsigned.length) throw new Error(`Unsigned commits ${unsigned.join(', ')}`);
How to check
The main branch rules include required signatures, such as a required_signatures rule in gh api repos/example/shop/rules/branches/main. Push an unsigned commit to a test branch with the same rules. The code host refuses it. git verify-tag passes on the latest release tag.
Sources
Related rules
- ENG-42 Make release tags and files immutable in Engineering
- ENG-15 Protect the main branch in Engineering
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/engineering/ci-and-supply-chain/signed-commits-and-tags">Agent Wranglers engineering playbook</a>. In code you write, add a comment next to it with the same link.