Skip to content

chore(tooling): migrate justfile and lefthook to mise tasks and hk - #23

Merged
justin13888 merged 7 commits into
masterfrom
chore/mise-hk-tooling
Sep 21, 2026
Merged

justin13888 merged 7 commits into
masterfrom
chore/mise-hk-tooling

Conversation

@justin13888

@justin13888 justin13888 commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Replaces justfile + lefthook.yml with mise tasks + hk, and points every GitHub Actions job at the same tasks.

Why

cargo clippy --workspace --all-targets -- -D warnings was written in three places — justfile, lefthook.yml and ci.yml — and could drift in three directions. Two further problems surfaced while doing this:

  • lefthook.yml was inert. .git/hooks/ in a fresh clone holds only the stock .sample files; nothing ran lefthook install. Installing it by hand didn't help either: its commit-msg hook died with sh: convco: command not found, because the README only linked convco and left installing it to you.
  • The hooks validated the wrong tree. lefthook's pre-commit ran cargo fmt over the whole working tree. I reproduced this: with file A staged and an unrelated edit left unstaged in file B, B was rewritten in the working tree (sha changed, reformatted). It wasn't swept into the commit, but the bytes you deliberately left out were still not yours any more.

What changed

mise.toml is the single source of truth for every command. hk.pkl contains no cargo or convco invocation at all — it decides only when each task runs. CI calls the same tasks.

mise.toml  ──┬──>  hk.pkl        (pre-commit / commit-msg / pre-push)
             └──>  ci.yml, determinism.yml
Path Change
mise.toml new — pinned tools (hk, cargo-binstall, convco) + every task
hk.pkl new — hook wiring only; all steps are mise run <task>
scripts/determinism-hashes.sh new — the determinism render block, lifted from determinism.yml
justfile, lefthook.yml deleted
.github/workflows/ci.yml jobs call mise run <task>; cargo-binstall step dropped; new Tooling job
.github/workflows/determinism.yml mise run test + mise run determinism
.gitignore ignore mise.local.toml, .mise.local.toml, /hashes*.txt
README.md prerequisites, commands table, git-hooks and CI sections
AGENTS.md just check → mise run check; records the single-source-of-truth rule
docs/verification.md records that the determinism render is now locally reproducible

No application code is touched. HARD-DET, HARD-VER, HARD-RUST, HARD-FS are unaffected; HARD-LOCAL covers the app, not dev tooling; mise/hk/convco are MIT developer tools, not linked into any crate (HARD-LICENSE).

Every command string is character-identical to its predecessor — cargo fmt, cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, cargo build --workspace, cargo run -p focale-app --, convco check <range>, convco check --from-stdin.

The four gates, in all three places

Gate mise run check pre-push CI job
mise run fmt-check ✅ ✅ Format
mise run lint ✅ ✅ Clippy
mise run test ✅ ✅ Test
mise run validate-hooks ✅ ✅ Tooling config
mise run commits — (needs a range) ✅ Conventional commits (PRs)
mise run determinism — (two-arch comparison) — Determinism

Behaviour changes

  • pre-commit no longer touches unstaged work. hk's stash = "git" saves unstaged changes, runs against the staged content, stages only the files it selected, restores the rest.
  • mise run determinism runs the determinism check locally. It was CI-only inline bash before.
  • The Conventional commits job no longer installs a Rust toolchain, and drops cargo-bins/cargo-binstall@main — a floating branch ref in the supply chain.
  • New Tooling CI job runs hk validate, so a broken hk.pkl fails in CI rather than at the next contributor's commit. This adds one required status check.
  • mise is now required to run the project's commands. The raw cargo invocations remain readable in mise.toml for anyone without it.

Decisions taken

  1. mise owns the commands, not hk. hk's Builtins.cargo_fmt/cargo_clippy are deliberately unused: they exist to scope a linter to {{files}}, but cargo fmt/clippy are workspace-wide regardless, so the builtins would only have re-stated the flags in a second file.
  2. convco is the one conventional-commit implementation. hk's check_conventional_commit builtin validates a single message and hk has no range checker — pre-push and CI need one, so using the builtin would have meant two implementations of the same rule. Rationale recorded in hk.pkl.
  3. The Rust toolchain stays with rust-toolchain.toml + dtolnay/rust-toolchain@stable. mise [tools] manages only hk/convco/binstall, so there is no second Rust version declaration.
  4. convco uses the cargo: backend. It publishes no x86_64-apple-darwin binary; cargo-binstall takes the prebuilt artifact where one exists (all CI runners, Apple Silicon, Windows) and falls back to a source build on Intel macOS instead of hard-failing mise install.
  5. pre-push steps are unglobbed. They run workspace-wide cargo commands that ignore hk's file selection, so a glob would only gate whether the step runs — and a push touching only Cargo.toml or rust-toolchain.toml would skip a gate CI still enforces.
  6. mise run check is sequential, like the justfile recipe. depends runs in parallel, which buys nothing against cargo's target lock and interleaves three cargo logs.

Residual duplication (could not be collapsed)

  • The hk version appears in three places: mise.toml [tools], and hk.pkl's amends URL + min_hk_version. Inherent to hk's Pkl packaging. The Tooling job catches a bad bump.
  • determinism.yml's compare job keeps an inline diff. That job has no actions/checkout, so there is no mise.toml there to run a task from. Recorded in a comment at the step.

Validation

Local, on macOS/aarch64 at 08afdda:

Check Command Result
Full gate mise run check exit 0 — sequential fmt-check → lint → test → validate-hooks
hk config mise run validate-hooks hk.pkl is valid
Workflows actionlint .github/workflows/*.yml clean
Determinism mise run determinism exit 0, same 9 labelled lines in the same order as the old inline block; byte-identical across runs
Push gate git push all five pre-push steps ran and passed

CI at 08afdda: all 8 checks pass — Format, Clippy, Test, Tooling config, Conventional commits, Render (x86_64), Render (aarch64), Compare architectures.

The Compare architectures job emitted hashes byte-identical to the local macOS/aarch64 run, across all 9 labels — so the script lift is behaviour-preserving on macOS/aarch64, Linux/x86_64 and Linux/aarch64.

Hook behaviour, exercised end-to-end in throwaway clones:

  • pre-commit stash — staged file formatted and committed; unstaged file byte-identical before/after and absent from the commit. Same scenario under the old lefthook.yml: the unstaged file was rewritten.
  • commit-msg — this is not a conventional commit rejected (exit 1, HEAD unmoved); chore: good message test accepted; a real git merge with Merge branch 'side-branch' passed via the MERGE_HEAD exemption.
  • pre-push — clean branch: all steps pass, push succeeds. Branch with a non-conventional commit: commits fails, push blocked. Branch with a clippy error: lint fails, push blocked.
  • Unglobbed-gate check — this branch changes zero .rs files. hk run pre-push --plan selects every step; with the globs it reported ○ fmt-check (no files matched filters).

Coverage gaps

  • Local mise run determinism proves the script runs, not that architectures agree. The HARD-DET guarantee still lives entirely in the two-arch CI matrix.
  • scripts/determinism-hashes.sh has no lint gate — nothing runs shellcheck/shfmt over it (pre-existing for scripts/fetch-models.sh, but this script now carries a HARD-DET-critical step).
  • hk validate parses hk.pkl but does not resolve task names or reject unknown fields. A typo like mise run lnt validates clean and would surface at hook time.
  • Intel macOS is untested. The cargo/binstall fallback is reasoned from convco's asset list, not demonstrated.
  • Every CI job installs hk and convco, including jobs that use neither, because mise-action installs the whole [tools] set. Accepted for consistency; it does mean a release-fetch hiccup can fail an unrelated job.
  • No mise lockfile. Tool versions are pinned in mise.toml, but there is no mise.lock recording resolved artifacts.

Open questions

  1. Should scripts/ get a shellcheck gate, now that a HARD-DET step lives there? Out of scope here.
  2. Should mise.toml set [settings] idiomatic_version_file_enable_tools = [] as defence-in-depth, so a contributor who globally enabled rust idiomatic version files doesn't get mise shadowing rustup? Today's default is node-only, so this is not a live bug.
  3. .versionrc is a standard-version (Node) config that nothing in the repo consumes — release-plz uses its own changelog config and there is no Node toolchain. Left untouched deliberately; delete separately?

mise.toml now holds every command Focale runs; hk.pkl decides only when each
task fires and against which files, and scripts/determinism-hashes.sh makes the
determinism render reproducible locally.

hk's pre-commit stashes unstaged work and runs against the staged content, so
formatting no longer rewrites files you deliberately left unstaged — verified
against the old lefthook config, which reformatted them in the working tree.
mise also provisions convco, which lefthook's commit-msg hook assumed was
already on PATH.

convco goes through the cargo backend rather than a direct release download: it
publishes no x86_64-apple-darwin binary, so binstall takes the prebuilt artifact
where one exists and falls back to a source build instead of hard-failing.

The pre-push steps are unglobbed on purpose. Each runs a workspace-wide cargo
command that ignores hk's file selection, so a glob would only decide whether
the step runs — and a push touching only Cargo.toml or rust-toolchain.toml would
skip a gate CI still enforces.
Each job now runs the same `mise run <task>` the git hooks and developers run,
so CI and the hooks cannot drift apart. The commits job drops cargo-binstall
entirely — mise provisions convco — and no longer needs a Rust toolchain.

A new Tooling job runs `mise run validate-hooks` so a broken hk.pkl fails in CI
rather than at the next developer's commit; nothing machine-checked that file
before.

The determinism render block moves verbatim into scripts/determinism-hashes.sh
behind `mise run determinism`, parameterised only by HASHES_OUT so the artifact
names are unchanged. The compare job keeps its inline diff and says why: it has
no checkout, so there is no mise.toml there to run a task from.
Both are superseded: every recipe is a mise task and every hook is an hk step.
lefthook.yml was inert in fresh clones anyway — nothing ran `lefthook install`,
so .git/hooks held only the stock samples.
Prerequisites, the commands table, the git-hooks section and the agent
instructions now point at mise tasks, and state that mise.toml is the single
source of truth the hooks and CI both call. Records that the determinism
render/hash step is reproducible locally, and that `mise run hooks` installs
into this clone only and needs mise on git's PATH.
Adding an hk.pkl validation job to CI without adding it to `mise run check` or
pre-push made three claims false at once — mise.toml's "everything CI runs",
AGENTS.md's CI-parity line, and README's "pushes should not fail CI". The task
now runs in both, so a broken hk.pkl is caught before it reaches CI.

Also: pin cargo-binstall like every other tool, since the README calls these
pinned; stop claiming `hk validate` resolves task names (it does not — a typo'd
`mise run` target validates clean); drop the README's `--global --mise` PATH
remedy, which changes where the hook is registered rather than the PATH it
needs; stop implying mise installs cmake; and run mise-action after the
toolchain step so a binstall fallback to `cargo install` has a managed
toolchain.
The previous commit's step reorder moved jdx/mise-action past the `mise run
test` step in this job, so it failed with `mise: command not found`. Every job
that invokes a task now sets mise up first.
Remove multi-line comments about cargo-binstall/convco and task
execution that were not adding clarity to the configuration.
@justin13888
justin13888 merged commit b5c7ecc into master Sep 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant