Local Test CI Using Git Hooks and Notes
I like the idea of git pre-commit hooks for ensuring that each commit pass tests. However, in practice pre-commit hooks tend to be painful, especially once tests take longer than a few seconds to run, or you’re doing some refactoring, then they can really break your flow. So I created a less intrusive solution, and added a post-commit hook that runs tests in the background and reports it back to the commit. It looks like this:
commit ae922649690abb0a2928800b62273617e4cca81e (HEAD)
Author: mccd <marc@mccd.space>
Date: Thu Jan 15 13:20:08 2026 +0100
Add a ci script
Notes:
mccd: ci PASS
---------------------------------------
? filed [no test files]
? filed/cmd [no test files]
ok filed/store 0.044sThe results are added as a git-note, which appends a message to an existing commit. If you run tmux, the script also reports failures to the status line (and it should also send a notification on Mac, but I haven’t tested this).
#!/bin/sh
# Runs tests in a separate worktree and reports the result as a git-note
set -o pipefail
USER_NAME=$(git config user.name || echo "Unknown User")
TEMP_DIR="$(mktemp -d)"
COMMIT=$(git rev-parse HEAD)
unset GIT_INDEX_FILE
unset GIT_DIR
unset GIT_WORK_TREE
# Enforce script to run in the background by using parens
(
git worktree add --detach "$TEMP_DIR" $COMMIT > /dev/null
cd "$TEMP_DIR"
TEST_OUTPUT=$(go test ./... 2>&1)
RESULT=$?
STATUS="PASS"
if [ $RESULT -ne 0 ]; then
STATUS="FAIL"
fi
TOTAL_LINES=$(echo "$TEST_OUTPUT" | wc -l)
if [ "$TOTAL_LINES" -gt 5 ]; then
OMITTED=$((TOTAL_LINES - 5))
SHORTENED=$(echo "$TEST_OUTPUT" | tail -n5)
TEST_OUTPUT=$(echo "...\n$SHORTENED\n($OMITTED lines omitted)")
fi
NOTE_MESSAGE=$(cat <<EOF
$USER_NAME: ci $STATUS
---------------------------------------
$TEST_OUTPUT
EOF
)
git notes append -m "$NOTE_MESSAGE" $COMMIT
git worktree remove $TEMP_DIR
# Notify on Tmux
if [ -n $TMUX ] && [ $RESULT -ne 0 ]; then
tmux display-message -d 10000 "CI: Tests failed for latest commit. Check git logs."
fi
# Notify on Mac OS X, untested
if command -v osascript >/dev/null 2>&1 && [ $RESULT -ne 0 ]; then
osascript -e "display notification 'CI: Tests failed for latest commit. Check git logs.'"
fi
) > /tmp/ci.log 2>&1 &The script works by creating a git-worktree where it will run the test, afterward it appends the git-note with test results. The test it runs is go test ./..., but it’s easy to change to whatever you’re using. To install it, you need to add the hook, which is done by writing the above post-commit hook to $REPO/.git/hooks/post-commit. Make sure the script is executable.
By default, these notes will not be shared with the upstream, so you need to manually push the notes:
git push origin 'refs/notes/*'
And you can also fetch notes remotely with:
git fetch origin 'refs/notes/*:refs/notes/*'
However, that is a bit clumsy, so alternatively you can configure your local git config to automatically include them:
$ # Assuming origin is the upstream $ git config --add remote.origin.fetch '+refs/notes/*:refs/notes/*' $ git config --add remote.origin.push 'refs/notes/*'
I think this feature could be useful if you want to setup your own CI, but have the “CI runner” be local. For example, you could have it so that the post-receive hook blocks any pushes to master where the tests are failing. If anyone wants to review your changes before merging to master, they could also check that the tests ran and were successful. You could also modify the post-receive hook to add a note if deployment was successful, and if not a command to view build logs (or just include them).