Skip to content

Commit 935309f

Browse files
Pigbibicodex
andcommitted
Harden weekly workflow boundary
Co-Authored-By: Codex <noreply@openai.com>
1 parent 9137cd7 commit 935309f

7 files changed

Lines changed: 75 additions & 30 deletions

File tree

.github/workflows/rss_source_pipeline.yml

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,18 @@ concurrency:
5454

5555
jobs:
5656
build-rss-source-events:
57+
if: >-
58+
github.repository == 'QuantStrategyLab/PoliticalEventTrackingResearch' &&
59+
github.ref == 'refs/heads/main' &&
60+
github.workflow_ref == 'QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/rss_source_pipeline.yml@refs/heads/main'
5761
runs-on: ubuntu-latest
5862
timeout-minutes: 30
5963
steps:
6064
- uses: actions/checkout@v6
65+
with:
66+
repository: QuantStrategyLab/PoliticalEventTrackingResearch
67+
ref: ${{ github.sha }}
68+
persist-credentials: false
6169
- uses: actions/setup-python@v6
6270
with:
6371
python-version: "3.11"
@@ -71,19 +79,10 @@ jobs:
7179
run: |
7280
set -euo pipefail
7381
RUN_PAYLOAD="${RUNNER_TEMP}/pert-workflow-run.json"
74-
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
75-
curl --fail --silent --show-error \
76-
-H "Accept: application/vnd.github+json" \
77-
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
78-
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" > "${RUN_PAYLOAD}"
79-
else
80-
rm -f "${RUN_PAYLOAD}"
81-
fi
82-
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
83-
PAYLOAD_ARGS=(--run-payload "${RUN_PAYLOAD}")
84-
else
85-
PAYLOAD_ARGS=()
86-
fi
82+
curl --fail --silent --show-error \
83+
-H "Accept: application/vnd.github+json" \
84+
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
85+
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" > "${RUN_PAYLOAD}"
8786
python scripts/validate_weekly_workflow.py \
8887
--event "${GITHUB_EVENT_NAME}" \
8988
--run-id "${GITHUB_RUN_ID}" \
@@ -92,7 +91,7 @@ jobs:
9291
--producer-ref "${GITHUB_SHA}" \
9392
--period-start "${PERIOD_START}" \
9493
--as-of "${AS_OF}" \
95-
"${PAYLOAD_ARGS[@]}" \
94+
--run-payload "${RUN_PAYLOAD}" \
9695
--output "${RUNNER_TEMP}/pert-period-context.json"
9796
python - <<'PY' "${RUNNER_TEMP}/pert-period-context.json" >> "${GITHUB_ENV}"
9897
import json, sys

scripts/validate_weekly_workflow.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from political_event_tracking_research.workflow_boundary import ( # noqa: E402
1616
WORKFLOW_REF,
1717
WorkflowBoundaryError,
18+
validate_manual_run,
1819
validate_manual_period,
1920
validate_scheduled_run,
2021
)
@@ -37,18 +38,23 @@ def main() -> None:
3738
try:
3839
if args.run_attempt != 1:
3940
raise WorkflowBoundaryError("run_attempt_invalid")
40-
if args.event == "schedule":
41+
if args.event in {"schedule", "workflow_dispatch"}:
4142
if args.run_payload is None:
42-
raise WorkflowBoundaryError("scheduled_run_payload_missing")
43+
raise WorkflowBoundaryError("workflow_run_payload_missing")
4344
payload = json.loads(args.run_payload.read_text(encoding="utf-8"))
44-
evidence = validate_scheduled_run(payload, run_id=args.run_id, workflow_ref=args.workflow_ref)
45-
result = {
46-
"period_start": evidence.period_start.isoformat(),
47-
"period_end_exclusive": evidence.period_end_exclusive.isoformat(),
48-
"as_of": evidence.as_of.isoformat(),
49-
"scheduled_created_at": evidence.created_at.isoformat(timespec="seconds").replace("+00:00", "Z"),
50-
"producer_ref": evidence.producer_ref,
51-
}
45+
if args.event == "schedule":
46+
evidence = validate_scheduled_run(payload, run_id=args.run_id, workflow_ref=args.workflow_ref)
47+
result = {
48+
"period_start": evidence.period_start.isoformat(),
49+
"period_end_exclusive": evidence.period_end_exclusive.isoformat(),
50+
"as_of": evidence.as_of.isoformat(),
51+
"scheduled_created_at": evidence.created_at.isoformat(timespec="seconds").replace("+00:00", "Z"),
52+
"producer_ref": evidence.producer_ref,
53+
}
54+
else:
55+
evidence = validate_manual_run(payload, run_id=args.run_id, workflow_ref=args.workflow_ref)
56+
start, as_of = validate_manual_period(args.period_start, args.as_of, run_created_at=evidence.created_at)
57+
result = {"period_start": start.isoformat(), "period_end_exclusive": (start + timedelta(days=7)).isoformat(), "as_of": as_of.isoformat(), "producer_ref": evidence.producer_ref}
5258
else:
5359
start, as_of = validate_manual_period(args.period_start, args.as_of)
5460
if args.workflow_ref != WORKFLOW_REF or type(args.producer_ref) is not str or not _SHA_RE.fullmatch(args.producer_ref):

src/political_event_tracking_research/weekly_artifact.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,14 @@ def build_weekly_artifact(*, period_start: date, as_of: date, generated_at: date
187187
raise _invalid("generated_at_invalid")
188188
if source_provenance != "official_rss_source_pipeline_v1" or run_mode not in {"scheduled", "manual"}:
189189
raise _invalid("producer_contract_invalid")
190-
raw_digest = _snapshot_digest(source_events, watchlist)
191190
period_events = _filter_events(source_events, period_start, as_of)
191+
source_snapshot_digest = _snapshot_digest(period_events, watchlist)
192192
event_count, _ = _csv_snapshot(period_events, EVENT_HEADER, "events_csv_invalid", allow_empty=True)
193193
watch_count, _ = _csv_snapshot(watchlist, WATCHLIST_HEADER, "watchlist_csv_invalid")
194194
status = _status(feed_status)
195195
snapshot_id = f"rss_source_snapshot_{as_of:%Y%m%d}_{source_run_id}"
196196
try:
197-
lock = PoliticalEventWeeklyPeriodLockV1(period_start, period_end, as_of, workflow_ref, source_run_id, 1, producer_ref, snapshot_id, raw_digest, source_provenance, (SourceSnapshotArtifact(EVENTS_NAME, _sha256(period_events), event_count), SourceSnapshotArtifact(WATCHLIST_NAME, _sha256(watchlist), watch_count)))
197+
lock = PoliticalEventWeeklyPeriodLockV1(period_start, period_end, as_of, workflow_ref, source_run_id, 1, producer_ref, snapshot_id, source_snapshot_digest, source_provenance, (SourceSnapshotArtifact(EVENTS_NAME, _sha256(period_events), event_count), SourceSnapshotArtifact(WATCHLIST_NAME, _sha256(watchlist), watch_count)))
198198
contract = WeeklySourceContract(as_of, period_start, period_end, generated_at, run_mode, producer_ref, source_provenance, (WeeklySourceArtifact(EVENTS_NAME, _sha256(period_events), event_count), WeeklySourceArtifact(WATCHLIST_NAME, _sha256(watchlist), watch_count)), status)
199199
files: dict[str, bytes] = {PERIOD_LOCK_NAME: serialize_period_lock(lock), EVENTS_NAME: period_events, WATCHLIST_NAME: watchlist, WEEKLY_NAME: serialize_weekly_contract(contract)}
200200
except (PeriodLockError, WeeklyContractError, TypeError, ValueError, OverflowError):
@@ -228,6 +228,8 @@ def parse_weekly_artifact(files: Mapping[str, bytes]) -> dict[str, bytes]:
228228
if tuple((item.path, item.sha256, item.row_count) for item in lock.source_artifacts) != expected or tuple((item.path, item.sha256, item.row_count) for item in contract.source_artifacts) != expected:
229229
raise _invalid("source_artifact_mismatch")
230230
expected_id = f"rss_source_snapshot_{contract.as_of:%Y%m%d}_{lock.source_run_id}"
231+
if lock.source_snapshot_digest != _snapshot_digest(files[EVENTS_NAME], files[WATCHLIST_NAME]):
232+
raise _invalid("source_snapshot_digest_mismatch")
231233
if lock.source_snapshot_id != expected_id or lock.source_attempt != 1 or lock.period_start != contract.period_start or lock.period_end_exclusive != contract.period_end_exclusive or lock.as_of != contract.as_of or lock.producer_ref != contract.producer_ref or lock.source_provenance != contract.source_provenance:
232234
raise _invalid("period_contract_mismatch")
233235
manifest_value = _parse_json(files[MANIFEST_NAME], "manifest_wire_invalid")

src/political_event_tracking_research/workflow_boundary.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def _parse_date(value: object, code: str) -> date:
4848
return parsed
4949

5050

51-
def validate_manual_period(period_start: object, as_of: object) -> tuple[date, date]:
51+
def validate_manual_period(period_start: object, as_of: object, *, run_created_at: datetime | None = None) -> tuple[date, date]:
5252
start = _parse_date(period_start, "manual_period_invalid")
5353
end = _parse_date(as_of, "manual_period_invalid")
5454
try:
@@ -57,6 +57,8 @@ def validate_manual_period(period_start: object, as_of: object) -> tuple[date, d
5757
raise _invalid("manual_period_invalid") from None
5858
if start.weekday() != 0 or end != expected_end - timedelta(days=1):
5959
raise _invalid("manual_period_mismatch")
60+
if run_created_at is not None and (type(run_created_at) is not datetime or run_created_at.tzinfo != timezone.utc or expected_end > run_created_at.date()):
61+
raise _invalid("manual_period_incomplete")
6062
return start, end
6163

6264

@@ -72,12 +74,12 @@ def _parse_created_at(value: object) -> datetime:
7274
return parsed
7375

7476

75-
def validate_scheduled_run(payload: Mapping[str, object], *, run_id: object, workflow_ref: object) -> ScheduledRunEvidence:
77+
def validate_workflow_run(payload: Mapping[str, object], *, run_id: object, workflow_ref: object, event: str) -> ScheduledRunEvidence:
7678
if not isinstance(payload, Mapping) or type(run_id) is not str or not _RUN_ID_RE.fullmatch(run_id) or workflow_ref != WORKFLOW_REF:
7779
raise _invalid("scheduled_run_identity_invalid")
7880
if type(payload.get("id")) is not int or payload["id"] != int(run_id):
7981
raise _invalid("scheduled_run_identity_invalid")
80-
if type(payload.get("run_attempt")) is not int or payload["run_attempt"] != 1 or payload.get("event") != "schedule":
82+
if type(payload.get("run_attempt")) is not int or payload["run_attempt"] != 1 or payload.get("event") != event:
8183
raise _invalid("scheduled_run_identity_invalid")
8284
if payload.get("path") != WORKFLOW_PATH or payload.get("head_branch") != "main":
8385
raise _invalid("scheduled_run_identity_invalid")
@@ -91,3 +93,11 @@ def validate_scheduled_run(payload: Mapping[str, object], *, run_id: object, wor
9193
current_monday = created_at.date() - timedelta(days=created_at.date().weekday())
9294
period_start = current_monday - timedelta(days=7)
9395
return ScheduledRunEvidence(period_start, current_monday, current_monday - timedelta(days=1), created_at, producer_ref)
96+
97+
98+
def validate_scheduled_run(payload: Mapping[str, object], *, run_id: object, workflow_ref: object) -> ScheduledRunEvidence:
99+
return validate_workflow_run(payload, run_id=run_id, workflow_ref=workflow_ref, event="schedule")
100+
101+
102+
def validate_manual_run(payload: Mapping[str, object], *, run_id: object, workflow_ref: object) -> ScheduledRunEvidence:
103+
return validate_workflow_run(payload, run_id=run_id, workflow_ref=workflow_ref, event="workflow_dispatch")

tests/test_weekly_artifact.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ def test_malformed_event_date_and_tamper_fail_closed() -> None:
6868
parse_weekly_artifact(tampered)
6969

7070

71+
def test_source_snapshot_digest_is_recomputed_from_artifact_bytes() -> None:
72+
files = build()
73+
lock = json.loads(files["period_lock.json"])
74+
lock["source_snapshot_digest"] = "b" * 64
75+
files["period_lock.json"] = json.dumps(lock, sort_keys=True, separators=(",", ":")).encode()
76+
with pytest.raises(WeeklyArtifactError) as error:
77+
parse_weekly_artifact(files)
78+
assert error.value.code == "source_snapshot_digest_mismatch"
79+
80+
7181
@pytest.mark.parametrize("feed", [status(complete=False), status(failed_feed_count=1, successful_feed_count=1), status(stale_feed_count=1), status(missing_feed_count=1)])
7282
def test_incomplete_feed_never_builds(feed: dict[str, object]) -> None:
7383
with pytest.raises(WeeklyArtifactError):

tests/test_workflow_boundary.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from datetime import date
3+
from datetime import date, datetime, timezone
44

55
import pytest
66

@@ -29,6 +29,8 @@ def test_manual_guard_is_pure_and_strict() -> None:
2929
validate_manual_period("", "2026-07-19")
3030
with pytest.raises(WorkflowBoundaryError):
3131
validate_manual_period("2026-07-14", "2026-07-20")
32+
with pytest.raises(WorkflowBoundaryError):
33+
validate_manual_period("2026-07-20", "2026-07-26", run_created_at=datetime(2026, 7, 20, 0, 1, tzinfo=timezone.utc))
3234

3335

3436
def test_scheduled_run_uses_api_created_at_not_local_clock() -> None:
@@ -61,3 +63,11 @@ def test_scheduled_run_wrong_repository_fails_closed() -> None:
6163
payload = {**RUN, "head_repository": {"full_name": "other/repo"}}
6264
with pytest.raises(WorkflowBoundaryError):
6365
validate_scheduled_run(payload, run_id="12345", workflow_ref="QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/rss_source_pipeline.yml@refs/heads/main")
66+
67+
68+
def test_manual_run_binds_current_run_created_at() -> None:
69+
payload = {**RUN, "event": "workflow_dispatch"}
70+
from political_event_tracking_research.workflow_boundary import validate_manual_run
71+
72+
evidence = validate_manual_run(payload, run_id="12345", workflow_ref="QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/rss_source_pipeline.yml@refs/heads/main")
73+
assert validate_manual_period("2026-07-13", "2026-07-19", run_created_at=evidence.created_at)[0] == date(2026, 7, 13)

tests/test_workflow_contract.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ def test_guard_and_legacy_upload_precede_weekly_build() -> None:
1010
assert text.index("Upload RSS source artifact") < text.index("Build completed weekly producer artifact")
1111

1212

13+
def test_privileged_job_has_server_side_main_gate_and_sha_checkout() -> None:
14+
text = WORKFLOW.read_text(encoding="utf-8")
15+
assert "github.ref == 'refs/heads/main'" in text
16+
assert "github.workflow_ref == 'QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/rss_source_pipeline.yml@refs/heads/main'" in text
17+
assert "ref: ${{ github.sha }}" in text
18+
assert "persist-credentials: false" in text
19+
20+
1321
def test_schedule_uses_actions_api_and_no_period_wall_clock() -> None:
1422
text = WORKFLOW.read_text(encoding="utf-8")
1523
assert "actions: read" in text

0 commit comments

Comments
 (0)