fix(ci): bind terminal-green evidence to PR head
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
2026-08-01 08:53:36 -05:00
parent e7b29219e1
commit 033b2ffb46
6 changed files with 82 additions and 19 deletions
@@ -62,6 +62,7 @@ A Woodpecker API token is required. To configure:
~/.config/mosaic/tools/woodpecker/ci-wait.sh -r usc/uconnect -n 3917 -n 3918
# Verify the full JSON child-step record; do not use the text summary for this gate
PR_HEAD=<full-40-hex-provider-head>
~/.config/mosaic/tools/woodpecker/pipeline-status.sh -r mosaicstack/stack -n 2188 -f json \
| ~/.config/mosaic/tools/woodpecker/verify-terminal-green.py -
| ~/.config/mosaic/tools/woodpecker/verify-terminal-green.py --expect-commit "$PR_HEAD" -
```
@@ -4,6 +4,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERIFIER="$SCRIPT_DIR/verify-terminal-green.py"
EXPECTED_COMMIT=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
@@ -27,14 +28,13 @@ PY
}
expect_exit() {
local expected="$1" label="$2" file="$3"
shift 3
local expected_exit="$1" label="$2" file="$3" expected_commit="${4:-$EXPECTED_COMMIT}"
set +e
output=$(python3 "$VERIFIER" "$file" "$@" 2>&1)
output=$(python3 "$VERIFIER" --expect-commit "$expected_commit" "$file" 2>&1)
actual=$?
set -e
if [[ "$actual" -ne "$expected" ]]; then
printf 'FAIL %s: expected exit %s, got %s\n%s\n' "$label" "$expected" "$actual" "$output" >&2
if [[ "$actual" -ne "$expected_exit" ]]; then
printf 'FAIL %s: expected exit %s, got %s\n%s\n' "$label" "$expected_exit" "$actual" "$output" >&2
exit 1
fi
printf 'PASS %s\n' "$label"
@@ -74,4 +74,23 @@ expect_exit 1 artifact-plus-real-failure "$TMP/artifact-plus-failure.json" >/dev
write_fixture "$TMP/skipped.json" success success 0 '' skipped
expect_exit 1 skipped-step "$TMP/skipped.json" >/dev/null
printf 'terminal-green contract harness: PASS (9 cases)\n'
# The scanned pipeline must be bound to an explicit, full PR-head commit.
set +e
missing_output=$(python3 "$VERIFIER" "$TMP/artifact.json" 2>&1)
missing_rc=$?
set -e
if [[ "$missing_rc" -ne 2 ]] || ! grep -q -- '--expect-commit' <<<"$missing_output"; then
printf 'FAIL missing-expected-commit: expected usage exit 2\n%s\n' "$missing_output" >&2
exit 1
fi
expect_exit 1 mismatched-expected-commit "$TMP/artifact.json" bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb >/dev/null
python3 - "$TMP/artifact.json" "$TMP/missing-record-commit.json" <<'PY'
import json, sys
record = json.load(open(sys.argv[1]))
record.pop("commit")
json.dump(record, open(sys.argv[2], "w"))
PY
expect_exit 1 missing-record-commit "$TMP/missing-record-commit.json" >/dev/null
printf 'terminal-green contract harness: PASS (12 cases)\n'
@@ -9,6 +9,7 @@ It does not fetch, retry, or re-trigger pipelines.
from __future__ import annotations
import argparse
import json
import re
import sys
@@ -51,12 +52,24 @@ def is_issue_1000_artifact(step: dict[str, Any]) -> bool:
)
def verify(record: dict[str, Any]) -> tuple[int, dict[str, Any]]:
def verify(record: dict[str, Any], expected_commit: str) -> tuple[int, dict[str, Any]]:
anomalies: list[dict[str, Any]] = []
candidates: list[dict[str, Any]] = []
steps: list[dict[str, Any]] = []
pipeline_status = record.get("status")
actual_commit = record.get("commit")
if actual_commit != expected_commit:
anomalies.append(
{
"scope": "pipeline",
"name": str(record.get("number", "unknown")),
"state": pipeline_status,
"reason": "pipeline commit does not equal the expected PR head",
"expected_commit": expected_commit,
"actual_commit": actual_commit,
}
)
if pipeline_status != "success":
anomalies.append(
{
@@ -156,7 +169,8 @@ def verify(record: dict[str, Any]) -> tuple[int, dict[str, Any]]:
"schema_version": "mosaic-terminal-green/v1",
"verdict": "terminal-green" if not anomalies else "not-terminal-green",
"pipeline_number": record.get("number"),
"commit": record.get("commit"),
"commit": actual_commit,
"expected_commit": expected_commit,
"pipeline_status": pipeline_status,
"total_steps": len(steps),
"state_counts": dict(sorted(state_counts.items())),
@@ -180,15 +194,31 @@ def verify(record: dict[str, Any]) -> tuple[int, dict[str, Any]]:
return (0 if not anomalies else 1), result
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="verify the full Woodpecker terminal-green child-step contract"
)
parser.add_argument(
"--expect-commit",
required=True,
metavar="FULL_SHA",
help="full 40-hex PR-head commit that the pipeline record must match",
)
parser.add_argument("record", nargs="?", default="-", help="pipeline JSON file or -")
arguments = parser.parse_args()
if re.fullmatch(r"[0-9a-fA-F]{40}", arguments.expect_commit) is None:
parser.error("--expect-commit must be a full 40-hex commit")
arguments.expect_commit = arguments.expect_commit.lower()
return arguments
def main() -> int:
if len(sys.argv) > 2 or (len(sys.argv) == 2 and sys.argv[1] in {"-h", "--help"}):
print(f"usage: {Path(sys.argv[0]).name} [pipeline.json|-]", file=sys.stderr)
return 0 if len(sys.argv) == 2 else 2
arguments = parse_arguments()
try:
record = load_record(sys.argv[1] if len(sys.argv) == 2 else None)
record = load_record(arguments.record)
except (OSError, ValueError, json.JSONDecodeError) as error:
return fail_usage(str(error))
code, result = verify(record)
code, result = verify(record, arguments.expect_commit)
print(json.dumps(result, indent=2, sort_keys=True))
return code