#!/usr/bin/env bash # Refuses a push to main that brings a commit not signed by a key in deploy/allowed-signers: # the rule deploy/redeploy.sh applies before it deploys main, after CI passed. Checked here, a # push of an unsigned commit fails on the laptop instead of after CI (it did on 2026-10-09, # when 37 such commits had kept learn.halkyon.eu at v0.2.2 since 2026-10-07). # # tool/check.sh runs it first as the pre-push hook, passing on git's arguments (the remote) and # stdin (a line per ref pushed: local ref, local commit, remote ref, remote commit). It checks # the commits pushed to main that no ref of that remote has yet. By hand, for what main would # bring: # # printf 'refs/heads/main %s refs/heads/main 0\n' "$(git rev-parse main)" | # tool/check_signatures.sh origin set -euo pipefail remote=${1:-origin} allowed=$(mktemp) trap 'rm -f "$allowed"' EXIT unsigned='' while read -r _local_ref local_sha remote_ref _remote_sha; do [ "$remote_ref" = refs/heads/main ] || continue # Deleting a branch pushes no commits. case $local_sha in *[!0]*) ;; *) continue ;; esac # The list as the pushed commit has it; its comment lines match no fingerprint. git show "$local_sha:deploy/allowed-signers" >"$allowed" unsigned+=$(git log --format='%H %G? %GP' "$local_sha" --not --remotes="$remote" | while read -r commit status key; do # G: a good signature; U: good, by a key the keyring doesn't vouch for (the list does). if { [ "$status" = G ] || [ "$status" = U ]; } && [ -n "$key" ] && grep -qx "$key" "$allowed"; then continue fi echo "$commit" done)$'\n' done if [ -n "${unsigned//[[:space:]]/}" ]; then echo 'Not signed by a key in deploy/allowed-signers, which deploying main requires:' >&2 for commit in $unsigned; do git log -1 --format=' %h %s' "$commit" >&2 done echo 'Sign them, e.g. git rebase --force-rebase --gpg-sign ,' \ 'and push again.' >&2 exit 1 fi