Skip to main content

max / makenotwork

Catch a stale openapi.json at commit time, not 15 minutes into a Sando build The spec embeds CARGO_PKG_VERSION, so every version bump invalidates the committed copy whether or not a route changed, and nothing local noticed: the /deploy pre-push guard is a cargo test --no-run compile check, and the spec is read at runtime by path rather than include_str!, so a stale copy compiles. The v0.11.8 bump pushed clean to all three remotes and killed Sando run 38 about fifteen minutes in, for a one-line diff in info.version. The hook compares against the staged copy, not the working-tree one, so regenerating without restaging fails the same way. export-openapi grows --stdout so the check needs no temp file in the source tree, and the failure message names the synckit-client vendored copy, which lives in another repo and cannot ride along in this commit.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-07 01:01 UTC
Signed with PGP, not checked
Commit: 79a5228bbf4ca50b2bf1cd111cafc181a6f2def2
Parent: 6c27312
2 files changed, +50 insertions, -0 deletions
@@ -66,6 +66,45 @@
66 66 fi
67 67 echo "pre-commit: migrations unmodified."
68 68
69 + # --- openapi.json staleness -------------------------------------------------
70 + # server/openapi.json is a committed artifact and `openapi::tests::
71 + # committed_spec_matches_generated` asserts it matches the generated spec. The
72 + # spec embeds CARGO_PKG_VERSION, so EVERY version bump invalidates it even when
73 + # no route changed.
74 + #
75 + # Nothing local caught that. The /deploy pre-push guard is a `cargo test
76 + # --no-run` compile check, and the spec is read at runtime by path rather than
77 + # include_str!, so a stale copy compiles fine. On 2026-08-06 the v0.11.8 bump
78 + # left the spec at 0.11.7, pushed clean to all three remotes, and killed Sando
79 + # run 38 about fifteen minutes in — two full remote build cycles for a one-line
80 + # diff in info.version.
81 + #
82 + # So: regenerate to stdout and compare against the STAGED copy (not the working
83 + # tree one — regenerating without restaging is the same bug wearing a hat).
84 + specish="$(git diff --cached --name-only --diff-filter=ACMR \
85 + -- 'server/Cargo.toml' 'server/src/*.rs' 'server/src/**/*.rs' 'server/openapi.json')"
86 + if [ -n "$specish" ]; then
87 + echo "pre-commit: checking openapi.json against the generated spec..."
88 + gen="$(mktemp)"
89 + trap 'rm -f "$gen"' EXIT
90 + if (cd "$ROOT/server" && cargo run --quiet --bin export-openapi -- --stdout) > "$gen" 2>/dev/null; then
91 + if ! git show :server/openapi.json 2>/dev/null | diff -q - "$gen" >/dev/null; then
92 + echo "pre-commit: server/openapi.json is stale (or regenerated but not staged)."
93 + echo " cd server && cargo run --bin export-openapi"
94 + echo " git add server/openapi.json"
95 + echo " Then vendor the same bytes into the OTHER repo, which this"
96 + echo " commit cannot carry and Sando will fail on:"
97 + echo " cp server/openapi.json ../synckit/synckit-client/tests/openapi.json"
98 + echo " Bypass: git commit --no-verify."
99 + exit 1
100 + fi
101 + echo "pre-commit: openapi.json current."
102 + else
103 + echo "pre-commit: could not build export-openapi; skipping spec check."
104 + echo " cargo_test in Sando is the backstop, 15 minutes into the build."
105 + fi
106 + fi
107 +
69 108 # Paths the gate ignores (extended regex, matched against repo-relative paths).
70 109 # Empty means check everything.
71 110 SKIP_PATHS="${SKIP_PATHS:-}"
@@ -11,11 +11,22 @@
11 11 //! ```sh
12 12 //! cargo run --bin export-openapi
13 13 //! ```
14 + //!
15 + //! `--stdout` writes the same bytes to standard output instead of the file, so
16 + //! a caller can diff the generated spec against a committed copy without
17 + //! touching the source tree. The pre-commit hook uses it to compare against the
18 + //! *staged* `openapi.json`, which is the copy the commit would actually carry.
14 19
15 20 use std::io::Write as _;
16 21
17 22 fn main() -> std::io::Result<()> {
18 23 let spec = makenotwork::openapi::spec_json();
24 +
25 + if std::env::args().any(|a| a == "--stdout") {
26 + std::io::stdout().write_all(spec.as_bytes())?;
27 + return Ok(());
28 + }
29 +
19 30 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/openapi.json");
20 31 let mut file = std::fs::File::create(path)?;
21 32 file.write_all(spec.as_bytes())?;