pre-commit: internal hook repos, take two
• 551 words • 3 min • updated
Problem statement: an internal hook repo has to be cloned by CI and by every developer, and the credentials each side owns are not the same.
A .pre-commit-config.yaml refers to hooks by repository:
repos:
- repo: https://github.com/<org>/pre-commit-hooks
rev: 528e165cff8a11bc4f9f49a9b25f3249e09e4237 # frozen: v0.0.22
hooks:
- id: just-formatBefore every run, prek clones each of those repositories into its cache. A
public repo clones anonymously and nobody thinks about it again. Ours moved out
of a personal account into an org-owned repo, which is INTERNAL — so the
clone now needs credentials, from whoever is running.
In June I gave the runners a short-lived app token and called it done. CI went green. The next morning, a coworker could no longer commit:
$ prek run -a
error: Failed to init hooks
caused by: Failed to initialize repo `https://github.com/<org>/pre-commit-hooks`
caused by: Command `git full clone` exited with an error:
[status]
exit status: 128
[stderr]
fatal: could not read Username for 'https://github.com': terminal prompts disabledNot a permissions problem: the same coworker could open and clone the repo by
hand. prek clones non-interactively, and an internal repo over https://
needs a credential helper to answer. Mine had a token cached; theirs did not:
% git -c credential.helper= clone https://github.com/<org>/pre-commit-hooks
Cloning into 'pre-commit-hooks'...
fatal: could not read Username for 'https://github.com': terminal prompts disabledSKIP= does not rescue this. The clone happens while hooks are being
initialized, long before SKIP is consulted, so the whole run dies and commits
with it. Three days after the migration, I reverted it everywhere:
4f74a46a Revert: ci: migrate pre-commit hooks to <org>/pre-commit-hooks
ea747ebe Revert: ci: migrate pre-commit hooks to <org>/pre-commit-hooks
4ec7e8063 Revert: ci: migrate pre-commit hooks to <org>/pre-commit-hooksThat June token was handed to git as a URL rewrite: https://github.com/
became https://x-access-token:$TOKEN@github.com/ on the runner. I had also
considered git@github.com:<org>/pre-commit-hooks back then, and dropped it
because runners have no SSH key. The premise was right and the conclusion was
backwards — a rewrite runs in whichever direction we point it, and I had pointed
it at the side that could not adapt.
So the config takes the URL that developers already have credentials for:
- - repo: https://github.com/<org>/pre-commit-hooks
- rev: 528e165cff8a11bc4f9f49a9b25f3249e09e4237 # frozen: v0.0.22
+ - repo: git@github.com:<org>/pre-commit-hooks
+ rev: 3030e9389488a2e4543a4d5d45d78a63bb591864 # frozen: v1.1.0
And the keyless runner rewrites that SSH prefix back to HTTPS, with the same short-lived app token as before:
- name: Authenticate git for the internal hooks repo
env:
TOKEN: ${{ steps.generate-token.outputs.token }}
run: git config --global url."https://x-access-token:${TOKEN}@github.com/<org>/".insteadOf "git@github.com:<org>/"A warm cache is what hid the bug the first time, so both paths were verified against a cold one. The developer path, cloning over SSH:
% XDG_CACHE_HOME=$(mktemp -d) prek run -a just-format
Format Justfiles.........................................................PassedThe runner path, with SSH made unusable and nothing but the rewrite in $HOME:
% export HOME=$(mktemp -d)
% printf '[url "https://x-access-token:%s@github.com/<org>/"]\n\tinsteadOf = git@github.com:<org>/\n' "$(gh auth token)" > "$HOME/.gitconfig"
% SSH_AUTH_SOCK= GIT_SSH_COMMAND=/bin/false XDG_CACHE_HOME="$HOME/.cache" prek run -a just-format
Format Justfiles.........................................................PassedThat second run also exposed where the rewrite belongs. Asking for a single hook populated the cache with every hook repo in the config:
% ls "$HOME/.cache/prek/repos" | wc -l
13prek initializes all of them up front, so the rewrite goes in every job that
runs prek — not only the linting one. ∎
/bloggify,
then reviewed and edited by me.
More agent-assisted posts.