Skip to content

Commit 6227d65

Browse files
Pigbibicodex
andcommitted
Add retry-safe weekly lock acquisition preflight
Co-Authored-By: Codex <noreply@openai.com>
1 parent d20719f commit 6227d65

4 files changed

Lines changed: 429 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: PERT Weekly Period Lock Acquisition Preflight
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
actions: read
11+
artifact-metadata: write
12+
13+
jobs:
14+
period-lock-acquisition:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 10
17+
env:
18+
ARTIFACT_NAME: pert-weekly-period-lock-${{ github.run_id }}
19+
PYTHONPATH: src
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.2.2
23+
- name: Set up Python
24+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
25+
with:
26+
python-version: "3.11"
27+
- name: Reject unsupported rerun attempts
28+
run: test "${GITHUB_RUN_ATTEMPT}" -le 2
29+
- name: Build and read back original period lock (attempt 1)
30+
if: ${{ github.run_attempt == 1 }}
31+
run: |
32+
set -euo pipefail
33+
mkdir -p "${RUNNER_TEMP}/pert-period-lock/bundle"
34+
python scripts/period_lock_acquisition_preflight.py build \
35+
--output-dir "${RUNNER_TEMP}/pert-period-lock/bundle" \
36+
--run-id "${GITHUB_RUN_ID}" \
37+
--artifact-name "${ARTIFACT_NAME}"
38+
- name: Upload immutable original period lock bundle (attempt 1)
39+
if: ${{ github.run_attempt == 1 }}
40+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
41+
with:
42+
name: ${{ env.ARTIFACT_NAME }}
43+
path: ${{ runner.temp }}/pert-period-lock/bundle
44+
if-no-files-found: error
45+
retention-days: 30
46+
- name: Download original period lock from this run (attempt 2)
47+
if: ${{ github.run_attempt == 2 }}
48+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
49+
with:
50+
name: ${{ env.ARTIFACT_NAME }}
51+
path: ${{ runner.temp }}/pert-period-lock/downloaded
52+
run-id: ${{ github.run_id }}
53+
github-token: ${{ github.token }}
54+
- name: Exact verify of original period lock (attempt 2)
55+
if: ${{ github.run_attempt == 2 }}
56+
run: |
57+
set -euo pipefail
58+
python scripts/period_lock_acquisition_preflight.py verify \
59+
--bundle-dir "${RUNNER_TEMP}/pert-period-lock/downloaded" \
60+
--run-id "${GITHUB_RUN_ID}" \
61+
--artifact-name "${ARTIFACT_NAME}"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Weekly period-lock acquisition preflight
2+
3+
This isolated workflow is a test-only proof of same-run GitHub Actions artifact
4+
acquisition. It is not the RSS/source production pipeline and does not publish
5+
producer output.
6+
7+
- Attempt 1 builds one deterministic representative `pert.weekly.period_lock.v1`
8+
lock and one immutable input snapshot. The period and snapshot are fixed
9+
fixture values; no wall-clock fallback is allowed.
10+
- The artifact name is `pert-weekly-period-lock-<run_id>`, and the bundle has
11+
exactly `period_lock.json`, `input_snapshot.json`, and `bundle_manifest.json`.
12+
The manifest binds the name, original run id, source attempt `1`, and each
13+
member digest. Retention is 30 days and overwrite is not used.
14+
- Attempt 2 is only the same run's controlled rerun. It downloads that exact
15+
artifact using the same `github.run_id`, then verifies canonical bytes,
16+
source run/attempt, artifact name, snapshot identity, member set, and all
17+
digests. It never derives a new period or uploads a replacement.
18+
19+
The workflow has no contents write, secrets, or OIDC permission. `actions:read`
20+
is used for the readback path and `artifact-metadata:write` is retained for the
21+
artifact upload boundary. This preflight must fail closed if the original-run
22+
artifact is missing, duplicated, replaced, or cannot be read with the declared
23+
permissions.
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""Isolated GitHub Actions preflight for same-run weekly lock acquisition.
2+
3+
This is test-only harness code. It uses a fixed representative snapshot and
4+
never derives a period from the wall clock.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import argparse
10+
import hashlib
11+
import json
12+
import re
13+
from pathlib import Path
14+
from typing import Any
15+
16+
from political_event_tracking_research.weekly_period_lock import (
17+
LOCK_VERSION,
18+
PeriodLockError,
19+
parse_period_lock,
20+
parse_period_lock_bytes,
21+
serialize_period_lock,
22+
)
23+
24+
BUNDLE_VERSION = "pert.weekly.period_lock_acquisition.v1"
25+
BUNDLE_FILES = ("bundle_manifest.json", "input_snapshot.json", "period_lock.json")
26+
SNAPSHOT_VERSION = "pert.weekly.input_snapshot.v1"
27+
_RUN_ID_RE = re.compile(r"^[1-9][0-9]*$")
28+
29+
30+
def expected_artifact_name(run_id: str) -> str:
31+
_validate_run_id(run_id)
32+
return f"pert-weekly-period-lock-{run_id}"
33+
34+
35+
def _validate_run_id(run_id: object) -> str:
36+
if type(run_id) is not str or not _RUN_ID_RE.fullmatch(run_id):
37+
raise ValueError("period_lock_run_invalid")
38+
return run_id
39+
40+
41+
def _canonical_json(value: object, error_code: str) -> bytes:
42+
try:
43+
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")
44+
except (TypeError, ValueError, UnicodeError, OverflowError, RecursionError):
45+
raise ValueError(error_code) from None
46+
47+
48+
def _parse_canonical_json(raw: bytes, error_code: str) -> dict[str, Any]:
49+
if type(raw) is not bytes:
50+
raise ValueError(error_code)
51+
52+
def pairs(items: list[tuple[str, object]]) -> dict[str, object]:
53+
result: dict[str, object] = {}
54+
for key, value in items:
55+
if key in result:
56+
raise ValueError(error_code)
57+
result[key] = value
58+
return result
59+
60+
def reject_constant(_: str) -> None:
61+
raise ValueError(error_code)
62+
63+
try:
64+
value = json.loads(raw.decode("utf-8"), object_pairs_hook=pairs, parse_constant=reject_constant)
65+
except ValueError:
66+
raise ValueError(error_code) from None
67+
if not isinstance(value, dict) or _canonical_json(value, error_code) != raw:
68+
raise ValueError(error_code)
69+
return value
70+
71+
72+
def _sha256(raw: bytes) -> str:
73+
return hashlib.sha256(raw).hexdigest()
74+
75+
76+
def _fixture_lock_payload(run_id: str) -> dict[str, object]:
77+
return {
78+
"lock_version": LOCK_VERSION,
79+
"calendar": "utc_iso_week_monday_sunday",
80+
"period_start": "2026-07-06",
81+
"period_end_exclusive": "2026-07-13",
82+
"as_of": "2026-07-12",
83+
"workflow_ref": "QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/pert_weekly_period_lock_acquisition.yml@refs/heads/main",
84+
"source_run_id": run_id,
85+
"source_attempt": 1,
86+
"producer_ref": "a" * 40,
87+
"source_snapshot_id": "pert_weekly_input_snapshot_20260712",
88+
"source_snapshot_digest": "b" * 64,
89+
"source_provenance": "official_political_event_tracking_research_v1",
90+
"source_artifacts": [
91+
{"path": "data/live/source_events.csv", "sha256": "c" * 64, "row_count": 11},
92+
{"path": "data/live/source_manifest.json", "sha256": "d" * 64, "row_count": 1},
93+
],
94+
}
95+
96+
97+
def _fixture_snapshot(lock_payload: dict[str, object]) -> dict[str, object]:
98+
return {
99+
"snapshot_version": SNAPSHOT_VERSION,
100+
"source_run_id": lock_payload["source_run_id"],
101+
"source_snapshot_id": lock_payload["source_snapshot_id"],
102+
"source_snapshot_digest": lock_payload["source_snapshot_digest"],
103+
"source_artifacts": lock_payload["source_artifacts"],
104+
}
105+
106+
107+
def _assert_empty_output_dir(output_dir: Path) -> None:
108+
if output_dir.is_symlink() or not output_dir.is_dir():
109+
raise ValueError("period_lock_output_dir_invalid")
110+
try:
111+
if any(output_dir.iterdir()):
112+
raise ValueError("period_lock_output_not_empty")
113+
except OSError:
114+
raise ValueError("period_lock_output_dir_invalid") from None
115+
116+
117+
def build_bundle(output_dir: Path, run_id: str, artifact_name: str | None = None) -> dict[str, object]:
118+
run_id = _validate_run_id(run_id)
119+
expected_name = expected_artifact_name(run_id)
120+
if artifact_name is not None and artifact_name != expected_name:
121+
raise ValueError("period_lock_artifact_name_mismatch")
122+
_assert_empty_output_dir(output_dir)
123+
124+
lock_payload = _fixture_lock_payload(run_id)
125+
lock = parse_period_lock(lock_payload)
126+
lock_bytes = serialize_period_lock(lock)
127+
snapshot = _fixture_snapshot(lock_payload)
128+
snapshot_bytes = _canonical_json(snapshot, "period_lock_snapshot_invalid")
129+
manifest = {
130+
"bundle_version": BUNDLE_VERSION,
131+
"artifact_name": expected_name,
132+
"source_run_id": run_id,
133+
"source_attempt": 1,
134+
"lock_version": LOCK_VERSION,
135+
"snapshot_version": SNAPSHOT_VERSION,
136+
"files": [
137+
{"name": "input_snapshot.json", "sha256": _sha256(snapshot_bytes)},
138+
{"name": "period_lock.json", "sha256": _sha256(lock_bytes)},
139+
],
140+
}
141+
manifest_bytes = _canonical_json(manifest, "period_lock_manifest_invalid")
142+
payloads = {
143+
"period_lock.json": lock_bytes,
144+
"input_snapshot.json": snapshot_bytes,
145+
"bundle_manifest.json": manifest_bytes,
146+
}
147+
try:
148+
for name, raw in payloads.items():
149+
with (output_dir / name).open("xb") as handle:
150+
handle.write(raw)
151+
except (OSError, ValueError):
152+
raise ValueError("period_lock_bundle_write_invalid") from None
153+
return verify_bundle(output_dir, run_id, expected_name)
154+
155+
156+
def _read_exact_bundle(output_dir: Path) -> dict[str, bytes]:
157+
if output_dir.is_symlink() or not output_dir.is_dir():
158+
raise ValueError("period_lock_bundle_shape_invalid")
159+
try:
160+
names = {path.name for path in output_dir.iterdir()}
161+
except OSError:
162+
raise ValueError("period_lock_bundle_shape_invalid") from None
163+
if names != set(BUNDLE_FILES):
164+
raise ValueError("period_lock_bundle_shape_invalid")
165+
result: dict[str, bytes] = {}
166+
for name in BUNDLE_FILES:
167+
path = output_dir / name
168+
try:
169+
if path.is_symlink() or not path.is_file():
170+
raise ValueError("period_lock_bundle_member_invalid")
171+
result[name] = path.read_bytes()
172+
except (OSError, ValueError):
173+
raise ValueError("period_lock_bundle_member_invalid") from None
174+
return result
175+
176+
177+
def verify_bundle(output_dir: Path, expected_run_id: str, expected_artifact: str) -> dict[str, object]:
178+
expected_run_id = _validate_run_id(expected_run_id)
179+
if expected_artifact != expected_artifact_name(expected_run_id):
180+
raise ValueError("period_lock_artifact_name_mismatch")
181+
payloads = _read_exact_bundle(output_dir)
182+
try:
183+
lock = parse_period_lock_bytes(payloads["period_lock.json"])
184+
except PeriodLockError:
185+
raise ValueError("period_lock_invalid") from None
186+
if serialize_period_lock(lock) != payloads["period_lock.json"] or lock.source_run_id != expected_run_id or lock.source_attempt != 1:
187+
raise ValueError("period_lock_run_mismatch")
188+
snapshot = _parse_canonical_json(payloads["input_snapshot.json"], "period_lock_snapshot_invalid")
189+
expected_snapshot = _fixture_snapshot(_fixture_lock_payload(expected_run_id))
190+
if snapshot != expected_snapshot:
191+
raise ValueError("period_lock_snapshot_mismatch")
192+
manifest = _parse_canonical_json(payloads["bundle_manifest.json"], "period_lock_manifest_invalid")
193+
if set(manifest) != {"bundle_version", "artifact_name", "source_run_id", "source_attempt", "lock_version", "snapshot_version", "files"}:
194+
raise ValueError("period_lock_manifest_invalid")
195+
if (
196+
manifest["bundle_version"] != BUNDLE_VERSION
197+
or manifest["artifact_name"] != expected_artifact
198+
or manifest["source_run_id"] != expected_run_id
199+
or type(manifest["source_attempt"]) is not int
200+
or manifest["source_attempt"] != 1
201+
or manifest["lock_version"] != LOCK_VERSION
202+
or manifest["snapshot_version"] != SNAPSHOT_VERSION
203+
):
204+
raise ValueError("period_lock_manifest_mismatch")
205+
files = manifest["files"]
206+
if not isinstance(files, list) or {item.get("name") for item in files if isinstance(item, dict)} != {"input_snapshot.json", "period_lock.json"}:
207+
raise ValueError("period_lock_manifest_files_invalid")
208+
for item in files:
209+
if not isinstance(item, dict) or set(item) != {"name", "sha256"} or item["name"] not in payloads:
210+
raise ValueError("period_lock_manifest_files_invalid")
211+
if item["sha256"] != _sha256(payloads[item["name"]]):
212+
raise ValueError("period_lock_artifact_digest_mismatch")
213+
return {
214+
"bundle_version": BUNDLE_VERSION,
215+
"artifact_name": expected_artifact,
216+
"source_run_id": expected_run_id,
217+
"source_attempt": 1,
218+
"files": {name: _sha256(raw) for name, raw in sorted(payloads.items())},
219+
}
220+
221+
222+
def main() -> int:
223+
parser = argparse.ArgumentParser()
224+
subparsers = parser.add_subparsers(dest="command", required=True)
225+
build = subparsers.add_parser("build")
226+
build.add_argument("--output-dir", type=Path, required=True)
227+
build.add_argument("--run-id", required=True)
228+
build.add_argument("--artifact-name")
229+
verify = subparsers.add_parser("verify")
230+
verify.add_argument("--bundle-dir", type=Path, required=True)
231+
verify.add_argument("--run-id", required=True)
232+
verify.add_argument("--artifact-name", required=True)
233+
args = parser.parse_args()
234+
evidence = (
235+
build_bundle(args.output_dir, args.run_id, args.artifact_name)
236+
if args.command == "build"
237+
else verify_bundle(args.bundle_dir, args.run_id, args.artifact_name)
238+
)
239+
print(json.dumps(evidence, sort_keys=True, separators=(",", ":")))
240+
return 0
241+
242+
243+
if __name__ == "__main__":
244+
raise SystemExit(main())

0 commit comments

Comments
 (0)