chore: the pre-push hook refuses unsigned commits for main, as the deploy does
deploy/redeploy.sh deploys main only if every new commit is signed by a key in deploy/allowed-signers, and finds out after CI. On 2026-10-09 that held learn.halkyon.eu at v0.2.2 for two days: 37 commits had reached main unsigned, and every deploy since refused them. tool/check_signatures.sh applies the same rule to a push, to the commits it brings to main that the remote doesn't have yet (the list as the pushed commit has it), and tool/check.sh runs it first when it runs as the pre-push hook: a second, where the rest takes minutes. CI runs check.sh under its own name and skips it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
44cee0d6c6
commit
3bbdb9d747
3 changed files with 53 additions and 1 deletions
|
|
@ -70,7 +70,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
||||||
- **Before keeping a framework default, name the alternatives** and recommend one, especially where a choice becomes expensive once users or data exist.
|
- **Before keeping a framework default, name the alternatives** and recommend one, especially where a choice becomes expensive once users or data exist.
|
||||||
- Every decision carries its reason (in `NEXT.md` or an ADR under `docs/adr/`).
|
- Every decision carries its reason (in `NEXT.md` or an ADR under `docs/adr/`).
|
||||||
- **Commits** are GPG-signed and carry a DCO `Signed-off-by` trailer (`git commit -s`; a local `prepare-commit-msg` hook adds it automatically).
|
- **Commits** are GPG-signed and carry a DCO `Signed-off-by` trailer (`git commit -s`; a local `prepare-commit-msg` hook adds it automatically).
|
||||||
- **Pushes** pass `tool/check.sh` first, through a local `pre-push` hook (in a fresh clone: `ln -s ../../tool/check.sh .git/hooks/pre-push`). CI runs it again, and a green `main` deploys itself (see *CI and deploys*), so an unsigned commit on `main` blocks every deploy after it.
|
- **Pushes** pass `tool/check.sh` first, through a local `pre-push` hook (in a fresh clone: `ln -s ../../tool/check.sh .git/hooks/pre-push`). CI runs it again, and a green `main` deploys itself (see *CI and deploys*), so an unsigned commit on `main` blocks every deploy after it. As the hook, the script first refuses a push to `main` that brings a commit not signed by a key in `deploy/allowed-signers` (`tool/check_signatures.sh`, the deploy's own rule), so it fails on the laptop rather than after CI.
|
||||||
- **Lints**: LeanCode's strict set (`leancode_lint`), configured once in the root `analysis_options.yaml`, which also enables its analyzer plugin. Plugins can only be enabled at the root of a pub workspace and only reach packages whose options include that file, so the server and the app include it; the generated client doesn't. Every `// ignore:` gets a comment saying why (`document_ignores`). The server switches off the four rules for Dart v3.13's constructor syntax (`use_primary_constructors`, `use_declaring_parameters`, `empty_container_bodies`, `unnecessary_type_name_in_constructor`): Serverpod's code generator (checked with v4.0.3) can't parse that syntax in endpoint files yet.
|
- **Lints**: LeanCode's strict set (`leancode_lint`), configured once in the root `analysis_options.yaml`, which also enables its analyzer plugin. Plugins can only be enabled at the root of a pub workspace and only reach packages whose options include that file, so the server and the app include it; the generated client doesn't. Every `// ignore:` gets a comment saying why (`document_ignores`). The server switches off the four rules for Dart v3.13's constructor syntax (`use_primary_constructors`, `use_declaring_parameters`, `empty_container_bodies`, `unnecessary_type_name_in_constructor`): Serverpod's code generator (checked with v4.0.3) can't parse that syntax in endpoint files yet.
|
||||||
|
|
||||||
## App architecture
|
## App architecture
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,12 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
# As the pre-push hook, first: commits going to main must be signed as deploying it requires
|
||||||
|
# (tool/check_signatures.sh), which takes a moment, where the rest takes minutes.
|
||||||
|
if [ "$(basename "$0")" = pre-push ]; then
|
||||||
|
tool/check_signatures.sh "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
if ! git diff --quiet HEAD; then
|
if ! git diff --quiet HEAD; then
|
||||||
echo 'Note: the working tree has uncommitted changes; they are checked too.'
|
echo 'Note: the working tree has uncommitted changes; they are checked too.'
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
46
tool/check_signatures.sh
Executable file
46
tool/check_signatures.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/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 <the commit before the first>,' \
|
||||||
|
'and push again.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Loading…
Reference in a new issue