11name : Consumer Pin Verification
22
3- # Optional gate: verify independent consumers still pin immutable OVK v1.2.1
4- # (or a configured tag) and never uses: ./
3+ # Verify independent consumers pin OVK to an exact 40-hex candidate SHA (WP-14).
4+ # Historical tags like v1.2.1 cannot evidence this candidate. uses: ./ is forbidden.
55
66on :
77 workflow_dispatch :
88 inputs :
9- ovk_tag :
10- description : Immutable OVK tag consumers must pin
9+ ovk_candidate_sha :
10+ description : Exact 40-hex OVK candidate SHA consumers must pin
1111 required : true
12- default : v1.2.1
12+ type : string
1313 fastapi_ref :
1414 description : Ref for ovk-consumer-fastapi-terraform
1515 required : true
@@ -34,54 +34,118 @@ jobs:
3434 - repo : fraware/ovk-consumer-express-actions
3535 ref : ${{ inputs.express_ref }}
3636 steps :
37+ - name : Validate candidate SHA shape
38+ env :
39+ OVK_SHA : ${{ inputs.ovk_candidate_sha }}
40+ run : |
41+ python - <<'PY'
42+ import os, re, sys
43+ sha = os.environ["OVK_SHA"].strip().lower()
44+ if not re.fullmatch(r"[0-9a-f]{40}", sha):
45+ sys.exit(f"ovk_candidate_sha must be exact 40-hex, got {sha!r}")
46+ print(f"candidate sha ok: {sha}")
47+ PY
48+
3749 - name : Checkout consumer
38- uses : actions/checkout@v4
50+ uses : actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
3951 with :
4052 repository : ${{ matrix.repo }}
4153 ref : ${{ matrix.ref }}
4254 path : consumer
43- - uses : actions/setup-python@v5
55+
56+ - uses : actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
4457 with :
4558 python-version : " 3.12"
46- - name : Assert immutable Action pin
59+
60+ - name : Assert immutable Action SHA pin (no uses: ./)
4761 env :
48- OVK_TAG : ${{ inputs.ovk_tag }}
62+ OVK_SHA : ${{ inputs.ovk_candidate_sha }}
4963 working-directory : consumer
5064 run : |
5165 set -euo pipefail
52- echo "Checking ${{ matrix.repo }}@${{ matrix.ref }} for OVK pin ${OVK_TAG }"
66+ echo "Checking ${{ matrix.repo }}@${{ matrix.ref }} for OVK pin ${OVK_SHA }"
5367 python - <<'PY'
5468 import os, re, sys
5569 from pathlib import Path
56- tag = os.environ["OVK_TAG"]
70+ sha = os.environ["OVK_SHA"].strip().lower()
5771 workflows = list(Path(".github/workflows").glob("*.yml")) + list(Path(".github/workflows").glob("*.yaml"))
5872 if not workflows:
5973 sys.exit("no workflow files")
6074 failures = []
75+ pin = f"fraware/open-verification-kernel@{sha}"
76+ texts = []
6177 for path in workflows:
6278 text = path.read_text(encoding="utf-8")
79+ texts.append(text)
6380 for line in text.splitlines():
6481 m = re.match(r"\s*uses:\s*(\S+)", line)
6582 if not m:
6683 continue
6784 ref = m.group(1).strip().strip("'\"")
6885 if re.match(r"^\./", ref):
6986 failures.append(f"{path.name}: forbidden uses: ./")
70- if "open-verification-kernel@" in ref and ref.endswith(("@main", "@master", "@HEAD")):
71- failures.append(f"{path.name}: mutable pin {ref}")
72- if f"fraware/open-verification-kernel@{tag}" not in text and "open-verification-kernel@" in text:
73- # Allow workflows that do not reference OVK; require pin when present.
74- pass
75- texts = "\n".join(p.read_text(encoding="utf-8") for p in workflows)
76- if f"fraware/open-verification-kernel@{tag}" not in texts:
77- failures.append(f"missing required pin fraware/open-verification-kernel@{tag}")
87+ if "open-verification-kernel@" in ref:
88+ if ref.endswith(("@main", "@master", "@HEAD")):
89+ failures.append(f"{path.name}: mutable pin {ref}")
90+ # Tags like @v1.2.1 are insufficient for candidate evidence.
91+ if re.search(r"@v?\d+\.\d+", ref) and sha not in ref:
92+ failures.append(f"{path.name}: tag pin cannot evidence candidate SHA: {ref}")
93+ if not ref.lower().endswith("@" + sha):
94+ failures.append(f"{path.name}: expected exact SHA pin {pin}, got {ref}")
95+ joined = "\n".join(texts)
96+ if pin not in joined.lower() and f"open-verification-kernel@{sha}" not in joined.lower():
97+ # Also allow ownerless path forms that still end with @sha
98+ if f"@{sha}" not in joined.lower() or "open-verification-kernel@" not in joined.lower():
99+ failures.append(f"missing required exact pin .../open-verification-kernel@{sha}")
78100 if failures:
79101 print("FAIL")
80102 for item in failures:
81103 print(item)
82104 raise SystemExit(1)
83- print(f"Pin verification OK for tag {tag }")
105+ print(f"Pin verification OK for candidate SHA {sha }")
84106 PY
85107 if [ -f scripts/assert_ovk_pin.py ]; then
86- python scripts/assert_ovk_pin.py
108+ python scripts/assert_ovk_pin.py --candidate-sha "$OVK_SHA"
87109 fi
110+
111+ - name : Capture ledger-ready consumer evidence
112+ env :
113+ OVK_SHA : ${{ inputs.ovk_candidate_sha }}
114+ CONSUMER_REPO : ${{ matrix.repo }}
115+ CONSUMER_REF : ${{ matrix.ref }}
116+ working-directory : consumer
117+ run : |
118+ mkdir -p ../.verification
119+ python - <<'PY'
120+ import hashlib, json, os, pathlib, re
121+ sha = os.environ["OVK_SHA"].strip().lower()
122+ repo = os.environ["CONSUMER_REPO"]
123+ ref = os.environ["CONSUMER_REF"]
124+ workflows = list(pathlib.Path(".github/workflows").glob("*.yml")) + list(
125+ pathlib.Path(".github/workflows").glob("*.yaml")
126+ )
127+ scenario_digests = []
128+ for path in workflows:
129+ scenario_digests.append(
130+ {"path": path.as_posix(), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
131+ )
132+ payload = {
133+ "schema_version": "ovk.consumer_pin_evidence.v1",
134+ "consumer_repository": repo,
135+ "consumer_ref": ref,
136+ "ovk_candidate_sha": sha,
137+ "pin": f"fraware/open-verification-kernel@{sha}",
138+ "workflow_digests": scenario_digests,
139+ "note": "Ledger-ready shape for release_ledger.consumers[]; requires live consumer checkout.",
140+ }
141+ out = pathlib.Path("../.verification") / f"consumer-pin-{repo.replace('/', '_')}.json"
142+ out.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
143+ print(out)
144+ PY
145+
146+ - name : Upload consumer pin evidence
147+ uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
148+ with :
149+ name : consumer-pin-${{ strategy.job-index }}
150+ path : .verification/consumer-pin-*.json
151+ if-no-files-found : warn
0 commit comments