-
Notifications
You must be signed in to change notification settings - Fork 0
feat: publish weekly producer contract artifact #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,14 @@ on: | |
| required: false | ||
| default: "50" | ||
| type: string | ||
| period_start: | ||
| description: "Manual weekly period Monday UTC (YYYY-MM-DD)." | ||
| required: true | ||
| type: string | ||
| as_of: | ||
| description: "Manual weekly as_of Sunday UTC (YYYY-MM-DD)." | ||
| required: true | ||
| type: string | ||
| commit_outputs: | ||
| description: "Commit generated live CSV outputs back to data/live." | ||
| required: false | ||
|
|
@@ -32,7 +40,7 @@ on: | |
| - "false" | ||
| - "true" | ||
| schedule: | ||
| - cron: "15 12 * * 6" | ||
| - cron: "15 0 * * 1" | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
@@ -46,8 +54,8 @@ jobs: | |
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: actions/setup-python@v6 | ||
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | ||
| - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 | ||
| with: | ||
| python-version: "3.11" | ||
| - name: Install package | ||
|
|
@@ -75,6 +83,47 @@ jobs: | |
| --watchlist "${WATCHLIST_PATH}" \ | ||
| --events data/output/rss_source_pipeline/source_events.csv \ | ||
| --output data/output/rss_source_pipeline/source_tracker.csv | ||
| - name: Build weekly contract artifact | ||
| env: | ||
| INPUT_PERIOD_START: ${{ inputs.period_start || '' }} | ||
| INPUT_AS_OF: ${{ inputs.as_of || '' }} | ||
| PRODUCER_REF: ${{ github.sha }} | ||
| run: | | ||
| set -euo pipefail | ||
| if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then | ||
| test -n "${INPUT_PERIOD_START}" | ||
| test -n "${INPUT_AS_OF}" | ||
| PERIOD_START="${INPUT_PERIOD_START}" | ||
| AS_OF="${INPUT_AS_OF}" | ||
| RUN_MODE=manual | ||
| else | ||
| RUN_DATE="$(date -u +%F)" | ||
| read -r PERIOD_START AS_OF < <(RUN_DATE="${RUN_DATE}" python3 - <<'PY' | ||
| import os | ||
| from datetime import date, timedelta | ||
|
|
||
| today = date.fromisoformat(os.environ["RUN_DATE"]) | ||
| start = today - timedelta(days=today.weekday() + 7) | ||
| print(start.isoformat(), (start + timedelta(days=6)).isoformat()) | ||
| PY | ||
| ) | ||
| RUN_MODE=scheduled | ||
| fi | ||
| GENERATED_AT="$(date -u +%Y-%m-%dT%H:%M:%S.%6NZ)" | ||
| python scripts/write_weekly_artifact.py \ | ||
| --base-dir . \ | ||
| --output-dir data/output/rss_source_pipeline/weekly-artifact \ | ||
| --feed-status data/output/rss_source_pipeline/source_fetch_status.json \ | ||
| --period-start "${PERIOD_START}" \ | ||
| --as-of "${AS_OF}" \ | ||
| --generated-at "${GENERATED_AT}" \ | ||
| --run-mode "${RUN_MODE}" \ | ||
| --producer-ref "${PRODUCER_REF}" \ | ||
| --source-provenance official_political_event_tracking_research_v1 \ | ||
| data/output/rss_source_pipeline/source_items.csv \ | ||
| data/output/rss_source_pipeline/source_events.csv \ | ||
| data/output/rss_source_pipeline/source_tracker.csv \ | ||
| data/output/rss_source_pipeline/source_fetch_status.json | ||
|
Comment on lines
+123
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The manifest records a weekly Useful? React with 👍 / 👎. |
||
| - name: Publish live CSV outputs to repository | ||
| env: | ||
| COMMIT_OUTPUTS: ${{ github.event_name == 'schedule' && 'true' || github.event.inputs.commit_outputs || 'false' }} | ||
|
|
@@ -108,8 +157,19 @@ jobs: | |
| git push | ||
| fi | ||
| - name: Upload RSS source artifact | ||
| uses: actions/upload-artifact@v7 | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | ||
| with: | ||
| name: rss-source-pipeline | ||
| path: data/output/rss_source_pipeline/ | ||
| path: | | ||
| data/output/rss_source_pipeline/source_items.csv | ||
| data/output/rss_source_pipeline/source_events.csv | ||
| data/output/rss_source_pipeline/source_tracker.csv | ||
| data/output/rss_source_pipeline/source_fetch_status.json | ||
| if-no-files-found: error | ||
| - name: Upload weekly contract artifact | ||
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | ||
| with: | ||
| name: political-event-weekly-v1 | ||
| path: data/output/rss_source_pipeline/weekly-artifact/weekly_manifest.json | ||
| if-no-files-found: error | ||
| retention-days: 30 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Weekly producer artifact contract | ||
|
|
||
| This is the producer-side boundary for `political_event_weekly.v1`. It is a | ||
| pure local adapter: callers provide the completed UTC week, `as_of`, actual | ||
| UTC `generated_at`, producer full SHA, provenance identifier, input files, and | ||
| feed status. The adapter never infers dates, reads the wall clock, fetches | ||
| feeds, or uploads files. | ||
|
|
||
| ## Artifact | ||
|
|
||
| - Name: `political-event-weekly-v1` | ||
| - File set: exactly `weekly_manifest.json` | ||
| - Retention: 30 days, configured by the consuming artifact workflow; it is not | ||
| inferred from report content. | ||
| - Bytes: canonical `political_event_weekly.v1` manifest bytes from the merged | ||
| weekly manifest serializer. | ||
|
|
||
| The input file paths are repository-relative POSIX paths under the trusted | ||
| `base_dir`. They must be regular, non-symlink files. Their SHA-256 and CSV row | ||
| counts are read from those exact files. The feed status file must be one of | ||
| the declared inputs, and its counters must agree with every feed entry. Any | ||
| failed, stale, missing, partial, malformed, or mismatched input fails before | ||
| the output directory is created. | ||
|
|
||
| The CLI requires all period, timestamp, producer, provenance, and path inputs | ||
| explicitly. Upload availability is an external workflow gate: callers must | ||
| only upload the returned artifact after successful local readback; an upload | ||
| step that cannot accept the exact single-file artifact must fail the workflow, | ||
| not publish a success signal. | ||
|
|
||
| The RSS source workflow now supplies this boundary without changing fetching: | ||
| scheduled runs execute Monday UTC after the previous ISO week is complete and | ||
| derive that week using the documented producer rule; manual runs require both | ||
| `period_start` and `as_of`. It uploads the single manifest with a 30-day | ||
| retention setting. Other workflows and downstream consumers are unchanged. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| #!/usr/bin/env python3 | ||
| """Write a producer-owned ``political_event_weekly.v1`` manifest artifact.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| from datetime import datetime | ||
| from pathlib import Path | ||
| import sys | ||
|
|
||
| sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) | ||
|
|
||
| from political_event_tracking_research.weekly_contract import WeeklyContractError | ||
| from political_event_tracking_research.weekly_producer import write_weekly_artifact_from_files | ||
|
|
||
|
|
||
| def _generated_at(value: str) -> datetime: | ||
| if not value.endswith("Z"): | ||
| raise argparse.ArgumentTypeError("generated_at_invalid") | ||
| try: | ||
| return datetime.fromisoformat(value[:-1] + "+00:00") | ||
| except ValueError as error: | ||
| raise argparse.ArgumentTypeError("generated_at_invalid") from error | ||
|
|
||
|
|
||
| def main() -> None: | ||
| parser = argparse.ArgumentParser(description="Write a validated weekly producer artifact.") | ||
| parser.add_argument("paths", nargs="+", help="Explicit input files, relative to --base-dir.") | ||
| parser.add_argument("--base-dir", required=True) | ||
| parser.add_argument("--output-dir", required=True) | ||
| parser.add_argument("--feed-status", required=True) | ||
| parser.add_argument("--period-start", required=True) | ||
| parser.add_argument("--as-of", required=True) | ||
| parser.add_argument("--generated-at", required=True, type=_generated_at) | ||
| parser.add_argument("--run-mode", required=True, choices=("scheduled", "manual")) | ||
| parser.add_argument("--producer-ref", required=True) | ||
| parser.add_argument("--source-provenance", required=True) | ||
| args = parser.parse_args() | ||
| try: | ||
| write_weekly_artifact_from_files( | ||
| args.paths, | ||
| base_dir=args.base_dir, | ||
| output_dir=args.output_dir, | ||
| feed_status_path=args.feed_status, | ||
| period_start=args.period_start, | ||
| as_of=args.as_of, | ||
| generated_at=args.generated_at, | ||
| run_mode=args.run_mode, | ||
| producer_ref=args.producer_ref, | ||
| source_provenance=args.source_provenance, | ||
| ) | ||
| except WeeklyContractError as error: | ||
| raise SystemExit(error.code) from None | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this workflow the fetch step still uses
--continue-on-feed-error, so a single RSS outage produces usable CSVs plus a nonzerofailed_feed_count; however this new weekly artifact step runs before the existing live-output commit/upload path, and the producer exits on incomplete feed status. In scheduled runs where one feed fails, the job now stops here and skips publishing the ordinary RSS outputs that were previously refreshed from the healthy feeds, so the weekly artifact failure should be isolated or moved after the existing publication path if partial RSS runs should remain available.Useful? React with 👍 / 👎.