2323 required : false
2424 default : " 50"
2525 type : string
26+ period_start :
27+ description : " Must equal the immediately prior complete ISO-week Monday derived from this run."
28+ required : true
29+ type : string
30+ as_of :
31+ description : " Must equal the immediately prior complete ISO-week Sunday derived from this run."
32+ required : true
33+ type : string
2634 commit_outputs :
2735 description : " Commit generated live CSV outputs back to data/live."
2836 required : false
2937 default : " false"
3038 type : choice
31- options :
32- - " false"
33- - " true"
39+ options : ["false", "true"]
3440 schedule :
35- - cron : " 15 12 * * 6 "
41+ - cron : " 15 12 * * 1 "
3642
3743permissions :
44+ actions : read
3845 contents : write
3946
4047concurrency :
4148 group : ${{ github.workflow }}-${{ github.ref_name }}
4249 cancel-in-progress : false
4350
4451jobs :
52+ validate-run-boundary :
53+ if : >-
54+ github.repository == 'QuantStrategyLab/PoliticalEventTrackingResearch' &&
55+ github.ref == 'refs/heads/main' &&
56+ github.workflow_ref == 'QuantStrategyLab/PoliticalEventTrackingResearch/.github/workflows/rss_source_pipeline.yml@refs/heads/main'
57+ runs-on : ubuntu-latest
58+ outputs :
59+ period_start : ${{ steps.guard.outputs.period_start }}
60+ period_end_exclusive : ${{ steps.guard.outputs.period_end_exclusive }}
61+ as_of : ${{ steps.guard.outputs.as_of }}
62+ producer_ref : ${{ steps.guard.outputs.producer_ref }}
63+ steps :
64+ - name : Validate run identity and period before checkout
65+ id : guard
66+ env :
67+ GH_TOKEN : ${{ github.token }}
68+ EVENT_NAME : ${{ github.event_name }}
69+ RUN_ID : ${{ github.run_id }}
70+ RUN_ATTEMPT : ${{ github.run_attempt }}
71+ WORKFLOW_REF : ${{ github.workflow_ref }}
72+ PERIOD_START : ${{ github.event.inputs.period_start || '' }}
73+ AS_OF : ${{ github.event.inputs.as_of || '' }}
74+ API_URL : ${{ github.api_url }}
75+ run : |
76+ set -euo pipefail
77+ payload="${RUNNER_TEMP}/pert-workflow-run.json"
78+ curl --fail --silent --show-error \
79+ -H "Accept: application/vnd.github+json" \
80+ -H "Authorization: Bearer ${GH_TOKEN}" \
81+ "${API_URL}/repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}" > "${payload}"
82+ python3 - "${payload}" <<'PY'
83+ import json
84+ import os
85+ import sys
86+ from datetime import datetime, timedelta, timezone
87+
88+ expected_repo = "QuantStrategyLab/PoliticalEventTrackingResearch"
89+ expected_path = ".github/workflows/rss_source_pipeline.yml"
90+ expected_ref = f"{expected_repo}/{expected_path}@refs/heads/main"
91+ payload = json.load(open(sys.argv[1], encoding="utf-8"))
92+ if os.environ["WORKFLOW_REF"] != expected_ref or os.environ["GITHUB_REF"] != "refs/heads/main":
93+ raise SystemExit("workflow_identity_invalid")
94+ if payload.get("id") != int(os.environ["RUN_ID"]) or type(payload.get("run_attempt")) is not int or payload["run_attempt"] != 1:
95+ raise SystemExit("workflow_identity_invalid")
96+ if payload.get("event") != os.environ["EVENT_NAME"] or payload.get("path") != expected_path or payload.get("head_branch") != "main":
97+ raise SystemExit("workflow_identity_invalid")
98+ if payload.get("head_repository", {}).get("full_name") != expected_repo:
99+ raise SystemExit("workflow_identity_invalid")
100+ producer_ref = payload.get("head_sha")
101+ if type(producer_ref) is not str or producer_ref != os.environ["GITHUB_SHA"] or len(producer_ref) != 40 or any(char not in "0123456789abcdef" for char in producer_ref):
102+ raise SystemExit("workflow_identity_invalid")
103+ created = datetime.fromisoformat(payload["created_at"].replace("Z", "+00:00"))
104+ if created.tzinfo != timezone.utc:
105+ raise SystemExit("workflow_created_at_invalid")
106+ current_monday = created.date() - timedelta(days=created.date().weekday())
107+ start = current_monday - timedelta(days=7)
108+ end = current_monday
109+ as_of = end - timedelta(days=1)
110+ if os.environ["EVENT_NAME"] == "workflow_dispatch" and (os.environ["PERIOD_START"], os.environ["AS_OF"]) != (start.isoformat(), as_of.isoformat()):
111+ raise SystemExit("manual_period_not_immediate_prior")
112+ for key, value in (("period_start", start.isoformat()), ("period_end_exclusive", end.isoformat()), ("as_of", as_of.isoformat()), ("producer_ref", producer_ref)):
113+ with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
114+ output.write(f"{key}={value}\n")
115+ PY
116+
45117 build-rss-source-events :
118+ needs : validate-run-boundary
119+ if : needs.validate-run-boundary.result == 'success'
46120 runs-on : ubuntu-latest
47121 timeout-minutes : 30
48122 steps :
49123 - uses : actions/checkout@v6
124+ with :
125+ repository : QuantStrategyLab/PoliticalEventTrackingResearch
126+ ref : ${{ github.sha }}
127+ persist-credentials : false
50128 - uses : actions/setup-python@v6
51129 with :
52130 python-version : " 3.11"
@@ -61,55 +139,44 @@ jobs:
61139 run : |
62140 set -euo pipefail
63141 mkdir -p data/output/rss_source_pipeline
64- python scripts/fetch_rss_sources.py \
65- --feeds "${FEEDS_PATH}" \
66- --output data/output/rss_source_pipeline/source_items.csv \
67- --max-items-per-feed "${MAX_ITEMS_PER_FEED}" \
68- --continue-on-feed-error \
69- --status-output data/output/rss_source_pipeline/source_fetch_status.json
70- python scripts/extract_source_mentions.py \
71- --raw-items data/output/rss_source_pipeline/source_items.csv \
72- --aliases "${ALIASES_PATH}" \
73- --output data/output/rss_source_pipeline/source_events.csv
74- python scripts/build_tracker.py \
75- --watchlist "${WATCHLIST_PATH}" \
76- --events data/output/rss_source_pipeline/source_events.csv \
77- --output data/output/rss_source_pipeline/source_tracker.csv
142+ python scripts/fetch_rss_sources.py --feeds "${FEEDS_PATH}" --output data/output/rss_source_pipeline/source_items.csv --max-items-per-feed "${MAX_ITEMS_PER_FEED}" --continue-on-feed-error --status-output data/output/rss_source_pipeline/source_fetch_status.json
143+ python scripts/extract_source_mentions.py --raw-items data/output/rss_source_pipeline/source_items.csv --aliases "${ALIASES_PATH}" --output data/output/rss_source_pipeline/source_events.csv
144+ python scripts/build_tracker.py --watchlist "${WATCHLIST_PATH}" --events data/output/rss_source_pipeline/source_events.csv --output data/output/rss_source_pipeline/source_tracker.csv
78145 - name : Publish live CSV outputs to repository
79146 env :
80147 COMMIT_OUTPUTS : ${{ github.event_name == 'schedule' && 'true' || github.event.inputs.commit_outputs || 'false' }}
81148 run : |
82149 set -euo pipefail
83- if [ "${COMMIT_OUTPUTS}" != "true" ]; then
84- echo "Live output commit disabled."
85- exit 0
86- fi
150+ if [ "${COMMIT_OUTPUTS}" != "true" ]; then echo "Live output commit disabled."; exit 0; fi
87151 mkdir -p data/live
88152 cp data/output/rss_source_pipeline/source_items.csv data/live/source_items.csv
89153 cp data/output/rss_source_pipeline/source_events.csv data/live/source_events.csv
90154 cp data/output/rss_source_pipeline/source_events.csv data/live/political_events.csv
91155 cp data/output/rss_source_pipeline/source_tracker.csv data/live/source_tracker.csv
92156 cp data/output/rss_source_pipeline/source_fetch_status.json data/live/source_fetch_status.json
93- python scripts/write_live_manifest.py \
94- --base-dir . \
95- --output data/live/source_manifest.json \
96- data/live/source_fetch_status.json \
97- data/live/source_items.csv \
98- data/live/source_events.csv \
99- data/live/political_events.csv \
100- data/live/source_tracker.csv
157+ python scripts/write_live_manifest.py --base-dir . --output data/live/source_manifest.json data/live/source_fetch_status.json data/live/source_items.csv data/live/source_events.csv data/live/political_events.csv data/live/source_tracker.csv
101158 git config user.name "github-actions[bot]"
102159 git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
103- git add data/live/source_items.csv data/live/source_events.csv data/live/political_events.csv data/live/source_tracker.csv data/live/source_fetch_status.json data/live/source_manifest.json
104- if git diff --cached --quiet; then
105- echo "No live RSS output changes to commit."
106- else
107- git commit -m "Update live RSS source events [skip ci]"
108- git push
109- fi
160+ git add data/live
161+ if git diff --cached --quiet; then echo "No live RSS output changes to commit."; else git commit -m "Update live RSS source events [skip ci]"; git push; fi
110162 - name : Upload RSS source artifact
111163 uses : actions/upload-artifact@v7
112164 with :
113165 name : rss-source-pipeline
114166 path : data/output/rss_source_pipeline/
115167 if-no-files-found : error
168+ - name : Build completed weekly producer artifact
169+ env :
170+ WATCHLIST_PATH : ${{ github.event.inputs.watchlist_path || 'data/live/political_watchlist.csv' }}
171+ RUN_MODE : ${{ github.event_name == 'schedule' && 'scheduled' || 'manual' }}
172+ run : |
173+ set -euo pipefail
174+ mkdir -p data/output/political-event-weekly-v1
175+ python scripts/build_weekly_artifact.py --period-start "${{ needs.validate-run-boundary.outputs.period_start }}" --as-of "${{ needs.validate-run-boundary.outputs.as_of }}" --generated-at "$(date -u +%Y-%m-%dT%H:%M:%S.000000Z)" --workflow-ref "${GITHUB_WORKFLOW_REF}" --source-run-id "${GITHUB_RUN_ID}" --producer-ref "${{ needs.validate-run-boundary.outputs.producer_ref }}" --source-events data/output/rss_source_pipeline/source_events.csv --watchlist "${WATCHLIST_PATH}" --feed-status data/output/rss_source_pipeline/source_fetch_status.json --output-dir data/output/political-event-weekly-v1 --run-mode "${RUN_MODE}"
176+ - name : Upload political weekly artifact
177+ uses : actions/upload-artifact@v7
178+ with :
179+ name : political-event-weekly-v1
180+ path : data/output/political-event-weekly-v1/
181+ if-no-files-found : error
182+ retention-days : 30
0 commit comments