Repository navigation
fix(health-check): cross-check content before flagging a compiled artifact stale - #2329
Conversation
…ifact stale `mark_stale_if_outdated` makes three mtime comparisons. The process-start path (sonichi#253) and the bridges path (sonichi#255) both ask git whether the content actually changed before flagging, because `git checkout`, `pull`, and `rebase` bump mtime on files that are byte-identical. The compiled-artifact branch arrived later (sonichi#529) and is the only one without that cross-check, so any mtime bump older than the threshold reports "rebuild needed" regardless of whether the source changed. Observed 2026-07-26: `upstream-sync` fast-forwarded `main` and restored the deployment branch with a checkout. That restore restamped 116 byte-identical files, `src/Sutando/main.swift` among them, and health-check reported `sutando-app: stale — binary is 2679 min older than source — rebuild needed`. The binary was current; the source's last content change predated the build by six days. Because the sync runs daily, so did the false alarm. Reuses the existing `_file_unchanged_since` helper with the binary's build time as the reference point: if the source content at build time matches the content now, the bump was idempotent and the binary is current. The helper already fails safe (returns False on any git error), so an unresolvable cross-check still flags. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 65%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
…gate The `--fix` path for sutando-app decides whether it may launch the existing binary with a bare mtime comparison (`binary.st_mtime >= source.st_mtime`). That is the same defect as the stale check this branch already fixes, in the opposite direction: after a checkout restamps `main.swift`, the gate refuses to launch a current binary and prints "needs manual rebuild + relaunch", so a down app stays down until someone rebuilds what did not need rebuilding. Extracts `_binary_is_current(binary, src)` — mtime ordering OR an idempotent bump proven by the existing content cross-check — and uses it at both call sites, so "is this binary current" has one definition. Fails safe on stat error, matching `_file_unchanged_since`. A genuine post-build content edit still returns False, so the auto-launch gate does not become more permissive about actually-stale binaries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed a second commit (
Both call sites now go through one Two things I'd rather flag than let a reviewer discover:
Happy to split this into its own PR if you'd rather keep the first commit single-concern — it's the same root cause and the same file, which is why I pushed here per CONTRIBUTING. |
|
Housekeeping note for whoever reviews this, and a correction on my part. Commit 1 of this PR duplicates work that already existed. A branch This PR is a superset, so it should be the one that lands: commit 2 fixes the same defect at its other call site — No change to the diff here; flagging it so the duplication is visible in the record rather than discovered later. |
|
Adversarial self-review of this diff, since it's the standing rule here and I hadn't done it. Three attacks; one real hole found, and it is pre-existing rather than introduced by this PR — disclosing rather than silently widening scope. Held up:
The hole — pre-existing, out of scope for this PR: If a source is genuinely edited after the build but its mtime is pushed backward behind the binary's, the check reports current while the content differs: Both spellings return Worth flagging operationally: the interim remediation for this false alarm — restoring mtimes to last-commit time — creates exactly that backward-mtime shape. It's only safe because it's gated on |
sonichi
left a comment
There was a problem hiding this comment.
Approving per Chi's explicit go (one-by-one). Review-verified LGTM: adds git content cross-check to the compiled-artifact stale branch, kills the false rebuild-needed alarm; 5-case tests.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Reviewed the premise and the mechanism against the source. The diagnosis is correct and the fix is the consistent one. One scoping limitation worth recording, not a blocker.
Premise verified at src/health-check.py. mark_stale_if_outdated does have three comparisons and exactly one lacks the cross-check:
- compiled-artifact branch, lines 916-926 — bare
src_mtime - bin_mtime > threshold_sec, no_file_unchanged_sincecall - process-start path, line 971 — calls
_file_unchanged_since - bridges path, line 2501 — calls
_file_unchanged_since
So "the only one of the three without that cross-check" is accurate as stated.
Fail-open direction is preserved, which is the thing I most wanted to check. _file_unchanged_since returns False on any git error (docstring: "so real stale deploys aren't hidden"). At line 971 False means do-not-suppress → report stale. Reusing it with the binary's build time as the reference keeps that polarity: a git failure still reports stale rather than silently claiming the binary is current. A fix in this area that inverted that would be worse than the false alarm, and this one doesn't.
Limitation: on a deployment host that is not a git checkout, this fix silently does nothing. _file_unchanged_since shells to /usr/bin/git log -1 --format=%ct HEAD -- <src> with cwd=REPO_DIR. Where REPO_DIR has no .git, that returns non-zero, the helper returns False, and the branch reports stale exactly as before. Measured on the host I am on:
$ git rev-parse --is-inside-work-tree
fatal: not a git repository (or any of the parent directories): .git
$ /usr/bin/git log -1 --format=%ct HEAD -- src/Sutando/main.swift
fatal: not a git repository (or any of the parent directories): .git
This is an app-bundled engine tree (REPO_DIR = Path(__file__).parent.parent), not a clone, so every _file_unchanged_since consumer already degrades to no-suppression here. That makes it a pre-existing property of the helper rather than anything this PR introduces — #253 and #255 inherit it too — and your observed failure was on a host that does sync via git, where the fix will work. I raise it only so the PR is not read as "the daily false alarm is now fixed everywhere": on non-git deployment hosts it will still fire, and closing that would need a non-git fallback (e.g. content hash recorded at build time) rather than a git query.
Not approving, because I could not execute this branch — no git checkout on this host, so I could not run tests/health-check-compiled-artifact-stale.test.py or diff the head. Everything above is read from the parent-commit source and from live commands whose output is pasted verbatim. Someone with a real clone should confirm the test fails at the parent and passes at the head before merge.
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed current head 3b8de4f. The bug exists on main: the compiled-artifact freshness path was the lone mtime-only branch. The shared content cross-check preserves fail-safe behavior on git errors and the predicate is reused by the repair gate. Independently ran all 9 focused cases, Python compile, diff check, and hardcoded-path scan; all pass. Live before/after evidence covers an idempotent checkout restamp and a genuine post-build edit. No blocking findings.
|
@cla-assistant check |
Problem
mark_stale_if_outdatedinsrc/health-check.pymakes three mtime comparisons. Two of them ask git whether the content actually changed before flagging — the process-start path (#253) and the bridges path (#255) — becausegit checkout,pull, andrebasebump mtime on files that are byte-identical. The compiled-artifact branch (binary_path is not None) arrived later in #529 and is the only one of the three without that cross-check, so any mtime bump older than the threshold reports "rebuild needed" regardless of whether the source changed.Observed 2026-07-26 on a deployment host:
upstream-syncfast-forwardedmainand restored the deployment branch with a checkout. That restore restamped 116 byte-identical files,src/Sutando/main.swiftamong them, and health-check reportedsutando-app: stale — rebuild needed. The binary was current — the source's last content change predated the build by six days. The sync runs daily, so the false alarm did too, which is the failure mode where a health channel stops being read.Fix
Reuse the existing
_file_unchanged_sincehelper, with the binary's build time as the reference point: if the source content at build time is byte-identical to the content now, the mtime bump was idempotent and the binary is current. This is the same treatment #253 and #255 already applied to the other two comparisons; the compiled-artifact branch is the last one missing it.The helper already fails safe — it returns
Falseon any git error — so an unresolvable cross-check still flags rather than hiding a genuinely stale binary.Evidence — real binary, real repo
Simulating the checkout restamp on
src/Sutando/main.swift(content unchanged since 2026-07-18; binary built 2026-07-24 11:57):Before (
src/health-check.pyat the parent commit):After (same file, same restamp, this PR applied):
Guard rail — same host, but with a genuine post-build content edit to
main.swift. It must still flag, and does:So the change suppresses the mtime-only case and nothing else.
Tests
tests/health-check-compiled-artifact-stale.test.py— 5 cases over the compiled-artifact branch: binary newer than source; idempotent mtime bump (the regression); genuine content change; cross-check unresolvable (fail safe); binary absent.The suite gates both ways:
One fixture note worth flagging for review:
_file_unchanged_sinceresolves the tree viagit reflog HEAD --before=@<ts>, so a fixture that commits "now" but dates the binary hours earlier has no reflog entry at-or-before the build — every case then fails safe and the suite passes for the wrong reason, unable to exercise the fix at all. The fixture backdates its commits viaGIT_COMMITTER_DATEso the timeline matches reality (commit, then build). This bit the first draft of the test.🤖 Generated with Claude Code