From 07240cf45c0f2eace3b99bf084d22c696383b134 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 14 Aug 2026 04:40:07 +0200 Subject: [PATCH] fix(ci): publish VT evidence under the marker the gate actually writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v0.10.4 release (run 31755436916) failed at the very last step of `verify`, after all 69 other jobs had passed — full test matrix, all eight builds, smoke and soak: publish-vt-evidence: wrong evidence marker in binaries/vt-results.tsv The results format was bumped to `cbm-virustotal-results-v2` in #1596, but publish-vt-evidence.sh still demanded `-v1`. It was the only straggler: the gate, the selector, the notes renderer and all three contract fixtures were already on v2. It survived because that PR had removed the script's only caller, leaving it dead code that nothing exercised. Restoring the full-surface scan restored the caller too, and the stale expectation surfaced in the worst possible place — at the end of a real release rather than in a dry-run, since `verify` does not run in dry-runs. Also adds the guard that would have caught it: every marker publish-vt-evidence.sh validates must be one the gate actually writes. Revert-checked — reintroducing v1 fails with "publisher expects marker never written by the gate: cbm-virustotal-results-v1". The scan itself passed. This is purely the evidence-publishing step. Signed-off-by: Martin Vogel --- scripts/ci/publish-vt-evidence.sh | 2 +- tests/test_vt_gate_policy_contract.sh | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/scripts/ci/publish-vt-evidence.sh b/scripts/ci/publish-vt-evidence.sh index c079012b..3706f5bf 100644 --- a/scripts/ci/publish-vt-evidence.sh +++ b/scripts/ci/publish-vt-evidence.sh @@ -40,7 +40,7 @@ publish_copy() { publish_copy "$VT_ASSOCIATIONS" cbm-release-scan-associations-v3 virustotal-associations.tsv publish_copy "$VT_EXPECTED_SCAN_SET" cbm-release-scan-set-v2 virustotal-scan-set.tsv -publish_copy "$VT_RESULTS_PATH" cbm-virustotal-results-v1 virustotal-results.tsv +publish_copy "$VT_RESULTS_PATH" cbm-virustotal-results-v2 virustotal-results.tsv for name in virustotal-associations.tsv virustotal-results.tsv virustotal-scan-set.tsv; do if command -v sha256sum >/dev/null 2>&1; then diff --git a/tests/test_vt_gate_policy_contract.sh b/tests/test_vt_gate_policy_contract.sh index 97bdd1b6..2f7e505e 100644 --- a/tests/test_vt_gate_policy_contract.sh +++ b/tests/test_vt_gate_policy_contract.sh @@ -32,6 +32,31 @@ grep -Fq 'Microsoft `!ml` tolerance' "$ROOT/README.md" || \ grep -Fq 'Policy identifier: `cbm-vt-candidate-selection-v1`' "$ROOT/SECURITY.md" || \ fail "SECURITY.md must name the versioned candidate-selection policy" +# Every marker publish-vt-evidence.sh validates must be one the gate actually +# writes. These drifted silently: the results format went to v2 while the +# publisher still demanded v1, and nothing caught it because the publisher had +# no caller for a while. Restoring the caller failed a real release at the very +# last step, after the full test matrix, both builds, smoke and soak had passed. +python3 - "$ROOT" <<'MARKERS' || fail "evidence markers disagree between writer and publisher" +import pathlib, re, sys +root = pathlib.Path(sys.argv[1]) +publisher = (root / "scripts/ci/publish-vt-evidence.sh").read_text(encoding="utf-8") +writers = "\n".join( + (root / name).read_text(encoding="utf-8") + for name in ("scripts/ci/check-virustotal.sh", "scripts/ci/append-vt-notes.sh") +) +expected = re.findall(r"^publish_copy\s+\S+\s+(\S+)", publisher, re.M) +if not expected: + print("no publish_copy markers found - has the publisher been restructured?", file=sys.stderr) + raise SystemExit(1) +missing = [m for m in expected if m not in writers] +if missing: + for m in missing: + print(f"publisher expects marker never written by the gate: {m}", file=sys.stderr) + raise SystemExit(1) +print(f"OK: all {len(expected)} published evidence markers match what the gate writes") +MARKERS + # Tripwire for the REVERTED endpoint-verification mechanism specifically. The # current `!ml` tolerance is a policy branch inside this gate, not a callout to # an external verification service, and must never become one.