R2D2-MERIDIAN/lefthook.yml
Joshua Belke 043361afcd
Some checks failed
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Meridian Harness / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Meridian Harness / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Meridian Harness / Publish rolling release (push) Has been cancelled
Meridian Harness / Publish tagged release (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Isolated DB Gate (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Admin Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Server Cross-Compile (push) Has been cancelled
CI / Server Cross-Compile-1 (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
ci(charts): enforce the Chart.yaml version bump instead of asking for it
deploy/AGENTS.md has said it plainly for a long time: "Bump `Chart.yaml`
`version` on any chart change. ArgoCD tracks chart versions; an unbumped
chart deploys stale templates." Nothing checked it. helm lint does not,
helm unittest does not, check-alert-runbooks does not, and the failure is
both silent and remote -- the chart renders, the suite passes, CI is
green, and the cluster keeps serving the previous templates.

Commit b6443a823 is the worked example. It added the entire
meridian-relay.bus-reachability group plus MeridianBusPeersUnreachable to
templates/prometheusrule.yaml and left Chart.yaml at 0.1.13, so a new
alert was doubly unreachable: no promtool case proving it could fire, and
a chart version that did not contain it. A human reading two files caught
it three commits later (meridian-8s3s).

The contract is differential -- "this changed without that changing" --
which is unusual here, because every other guard in scripts/ reads the
current tree. Two diff-shaped placements were considered and rejected:

  - a merge-base diff inside `just check`. There is no trustworthy
    baseline. The root contract forbids branches, so work lands directly
    on a constantly-rebased `main`; a merge-base with origin/main is not
    a release boundary, and in a fresh or shallow checkout it does not
    exist at all. A guard that invents its baseline reports whatever the
    local ref happens to be.
  - a CI-only diff against the PR base. The baseline is real there, but
    it is only real on pull_request, and this repo pushes to main. It
    would also be invisible to `just ci` and to the pre-push hook, which
    is where this repo prefers to catch things -- and helm-chart.yml,
    the one workflow that already touches charts, is `paths`-filtered on
    pull_request, which is precisely the conditional gate that does not
    run when it matters.

So the baseline is checked in instead, the same shape as
scripts/n-minus-one-pins.json and .settings/stellar/snapshot.json:
scripts/chart-version-manifest.json records, per chart, the sha256 of
every file that renders into a cluster against the version they were last
recorded at. Any edit to templates/, crds/, values*.yaml|json, Chart.yaml
or Chart.lock moves a digest, and a moved digest without a higher
`version` is a hard failure naming the file. No diff, no baseline branch,
works in `just check` and in a shallow clone.

Two normalizations make the guard say what it means. Chart.yaml is hashed
with its top-level `version:` line removed, so a bump with no template
change stays silent while an edit to `dependencies:` or `appVersion`
still demands one; Chart.lock is hashed without `generated:`, which
`helm dependency build` rewrites without changing what deploys. tests/,
ci/, examples/ and README.md are deliberately outside the set -- none of
them reach a cluster, and demanding a bump for a test-only edit teaches
people to bump reflexively, which is the same failure wearing a hat.

`--write` is not an escape hatch: it refuses to record a chart whose
rendered files moved while `version` did not, printing the same message
the check does. The only route to a green tree is the bump.

Wired three ways, because a guard in one place is a guard someone skips:
`just check` (via check-chart-version-bump), the ci.yml guards job, and a
new lefthook pre-push entry -- before which a `deploy/charts/**`-only push
matched no glob at all and ran zero local hooks, which is how b6443a823
got out.

scripts/test-chart-version-bump-guard.sh is the "prove it fires" half. It
asserts the exit codes rather than the prose: refused with exit 1 on a
template edit and on a values.yaml edit, `--write` refusing to launder
either, exit 0 on a Chart.yaml-only bump and on a tests/ edit, every
chart under deploy/charts/ actually present in the manifest, and -- when
history is deep enough to reach it -- b6443a823 replayed against its own
parent's baseline and refused for the unbumped 0.1.13.

Covers all three charts: meridian@0.1.14, meridian-control-plane@0.1.1,
meridian-push-gateway@0.1.0, 50 rendered files. No file under
deploy/charts/ is touched by this commit.

Refs: meridian-8s3s
Signed-off-by: Joshua Belke <joshua@innovationhub-act.org>
2026-08-21 04:30:21 -04:00

138 lines
7.9 KiB
YAML

# Glob patterns below mirror the `changes` job's dorny/paths-filter groups in
# .github/workflows/ci.yml — keep the two in sync. Deliberate deviations:
# - The `pre-commit` fix commands narrow their globs to the extensions their
# formatter accepts (`crates/**/*.rs`, not `crates/**`), because they pass the
# matched paths straight to the formatter. Widening one back to a directory
# glob hands `rustfmt` a `Cargo.toml`. CI parity lives in the `pre-push`
# globs, which still mirror the filter groups directly.
# - The `.github/workflows/ci.yml` path CI adds to its `rust`/`mobile` filters
# is omitted; a CI-workflow-only edit doesn't need a local test run.
# - `desktop-check`/`desktop-test` don't trigger on `rust` changes, though CI's
# Desktop Core job does. Those commands are pure TS (biome + node:test) with
# no Rust dependency, so the extra trigger would be spurious locally.
# - Deletion-only surface changes do not trigger local hooks: lefthook 2.1.x
# drops deleted paths from push-file discovery (`extractFiles` existence
# check, repository.go). CI's dorny/paths-filter catches deletions.
# Deliberate — accepted, not worked around.
# - `commit-msg` has no glob: it rewrites the commit message, not files. Note
# Git only runs it for `git commit` and `git merge` — other flows need their own
# flag: `git rebase --signoff`, `git cherry-pick -s`.
#
# EVERY fix command below is scoped to `{staged_files}`, and that is load-bearing
# rather than an optimisation. The whole-tree variants (`just fmt`,
# `just desktop-fix`, …) rewrite files the author never staged, and `stage_fixed`
# then adds them to the commit: on 2026-08-07 commit `2c2eb9153` carried seven of
# another agent's in-flight files despite the author staging three paths
# explicitly (bead meridian-jejg, finding F077). This branch is shared by
# construction — the root contract forbids branching so parallel agents
# interleave on `main` — so a formatter with tree-wide reach is a cross-agent
# write, not a tidy-up. Keep the file list in every `run:`; a command that
# formats `.` re-opens the defect.
#
# The whole-tree recipes stay right where they are, for `just fix-all` and CI,
# where formatting the whole tree is the intent.
pre-commit:
parallel: true
commands:
# rustfmt directly rather than `cargo fmt`: `cargo fmt -- <file>` still
# formats every crate root, so the file list would not scope anything.
# Edition mirrors `[workspace.package] edition` in Cargo.toml; there is no
# rustfmt.toml, so the remaining settings are rustfmt's defaults either way.
rust-fmt:
glob: ["crates/**/*.rs", "examples/countdown-bot/**/*.rs"]
run: rustfmt --edition 2021 {staged_files}
stage_fixed: true
desktop-tauri-fmt:
glob: ["REMAPPING/meridian-desktop/src-tauri/**/*.rs"]
run: rustfmt --edition 2021 {staged_files}
stage_fixed: true
# `root:` keeps the working directory at `REMAPPING/meridian-desktop/`, so biome resolves
# `REMAPPING/meridian-desktop/biome.json` exactly as `just desktop-fix` does. Running biome from
# the repo root would pick up the root `biome.json` instead and reformat
# against the wrong ruleset.
desktop-fix:
root: "REMAPPING/meridian-desktop/"
glob: ["*.{ts,tsx,js,jsx,mjs,cjs,json,jsonc,css}"]
# `*.generated.json` mirrors the `includes` deny-list in `REMAPPING/meridian-desktop/biome.json`.
# Since the formatters started taking `{staged_files}`, a commit whose only
# desktop file is a generated artifact handed biome one path that biome's own
# config ignores; biome then processed zero files and exited 1. That is a
# normal commit — `just check-mips --write` produces exactly it — so the two
# ignore lists have to agree or the workflow is unlandable.
exclude: ["src-tauri/**", "**/*.generated.json"]
# `check:file-sizes` takes the staged set too. It is a "what am I about
# to publish" check, and running it over the whole tree in a hook means
# one agent mid-way through splitting a large module blocks every other
# agent from committing anything — observed live, two files at once.
# CI and `just check` still scan the whole project, which is what
# actually protects `main`.
run: pnpm exec biome check --write {staged_files} && pnpm check:file-sizes {staged_files}
stage_fixed: true
web-fix:
root: "REMAPPING/meridian-web/"
glob: ["*.{ts,tsx,js,jsx,mjs,cjs,json,jsonc,css}"]
# `check:file-sizes` stays whole-project here, unlike desktop above: only
# the desktop wrapper reads a path list so far, and `REMAPPING/meridian-web/` has not
# produced a cross-agent block. Unscoped is the safer default — scope it
# when it actually gets in someone's way, not pre-emptively.
run: pnpm exec biome check --write {staged_files} && pnpm check:file-sizes
stage_fixed: true
# Split from the analyzer: `dart format` takes the staged file list, while
# `flutter analyze` is whole-project and read-only, so it cannot stage
# anything and needs no scoping.
mobile-fmt:
root: "REMAPPING/meridian-mobile/"
glob: ["*.dart"]
run: unset GIT_DIR GIT_WORK_TREE && dart format {staged_files}
stage_fixed: true
mobile-analyze:
root: "REMAPPING/meridian-mobile/"
glob: ["*"]
run: unset GIT_DIR GIT_WORK_TREE && flutter analyze
# Appends the DCO Signed-off-by trailer the required "DCO Check" enforces.
# `--if-exists doNothing` makes it idempotent and preserves an existing
# sign-off (including `git commit -s` and a different signer's trailer).
commit-msg:
commands:
signoff:
run: 'git interpret-trailers --if-exists doNothing --trailer "Signed-off-by: $(git var GIT_COMMITTER_IDENT | sed ''s/ [0-9]* [+-][0-9]*$//'')" --in-place {1}'
pre-push:
parallel: true
commands:
branch-skew:
run: ./scripts/check-branch-skew.sh
rust-tests:
glob: ["crates/**", "migrations/**", "schema/**", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "deny.toml", "scripts/run-tests.sh", "justfile"]
run: just test-unit
desktop-check:
glob: ["REMAPPING/meridian-desktop/**", "pnpm-lock.yaml"]
exclude: ["REMAPPING/meridian-desktop/src-tauri/**"]
# `desktop-check` is biome + guards only; `tsc` is a separate recipe.
# Without it here a type error survives every local gate and first
# surfaces in CI's `desktop-build`.
run: just desktop-check && just desktop-typecheck
desktop-test:
glob: ["REMAPPING/meridian-desktop/**", "pnpm-lock.yaml"]
exclude: ["REMAPPING/meridian-desktop/src-tauri/**"]
run: just desktop-test
desktop-tauri-checks:
# Keep local lint parity with Desktop Core CI for every path that can
# affect the Tauri crate or its path dependencies. Run clippy and tests
# serially so parallel pre-push hooks do not contend for Cargo's lock.
glob: ["REMAPPING/meridian-desktop/src-tauri/**", "crates/**", "migrations/**", "schema/**", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "deny.toml", "scripts/run-tests.sh", "justfile"]
run: just desktop-tauri-clippy && just desktop-tauri-test
mobile-test:
glob: ["REMAPPING/meridian-mobile/**"]
run: just mobile-test
# A chart-only change matches no other glob above, so before this entry a
# `deploy/charts/**` push ran ZERO local hooks. That is how b6443a823
# reached main with an alert group added at Chart.yaml 0.1.13: ArgoCD tracks
# chart versions, so the cluster kept the previous templates and nothing
# anywhere said so. Cheap (node, no toolchain) and state-based, so it needs
# no diff. `scripts/**` is in the glob because the manifest and the guard
# itself live there.
chart-version-bump:
glob: ["deploy/charts/**", "scripts/chart-version-manifest.json", "scripts/check-chart-version-bump*.mjs", "scripts/test-chart-version-bump-guard.sh"]
run: just check-chart-version-bump