main
yml 191 lines 8.07 KB
Raw
1 name: Podcaster handoff smoke test
2
3 on:
4 workflow_dispatch:
5 # checkov:skip=CKV_GHA_7:Manual smoke-test workflow (not a release build). Inputs identify the article/week to validate the handoff payload; they do not produce or alter published build artifacts. Inputs are required to target a specific article.
6 inputs:
7 week:
8 description: 'Week slug to validate, e.g. 2026-W23.'
9 required: true
10 type: string
11 article_url:
12 description: 'Published SquadScope article URL to send to Podcaster.'
13 required: true
14 type: string
15 article_path:
16 description: 'Published article path, e.g. content/weekly/2026/W23.md.'
17 required: true
18 type: string
19 article_sha256:
20 description: 'Optional 64-character article SHA-256 override; when omitted the checked-out article file is hashed.'
21 required: false
22 default: ''
23 type: string
24
25 permissions:
26 contents: read
27
28 concurrency:
29 group: ${{ github.workflow }}-${{ github.ref }}
30 cancel-in-progress: false
31
32 jobs:
33 smoke:
34 runs-on: ubuntu-latest
35 permissions:
36 contents: read
37
38 steps:
39 - name: Check out repository
40 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
41 with:
42 persist-credentials: false
43
44 - name: Set up Python
45 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
46 with:
47 python-version: '3.12'
48
49 - name: Smoke test Podcaster dry run
50 env:
51 PODCASTER_ENDPOINT: ${{ vars.PODCASTER_ENDPOINT }}
52 PODCASTER_API_KEY: ${{ secrets.PODCASTER_API_KEY }}
53 WEEK: ${{ inputs.week }}
54 ARTICLE_URL: ${{ inputs.article_url }}
55 ARTICLE_PATH: ${{ inputs.article_path }}
56 ARTICLE_SHA256: ${{ inputs.article_sha256 || '' }}
57 PUBLISH_RUN_ID: ${{ github.run_id }}
58 run: |
59 set -euo pipefail
60 if [ -z "$PODCASTER_ENDPOINT" ] || [ -z "$PODCASTER_API_KEY" ]; then
61 echo "::error::Podcaster smoke test requires PODCASTER_ENDPOINT variable and PODCASTER_API_KEY secret."
62 exit 1
63 fi
64 if [ ! -f "$ARTICLE_PATH" ]; then
65 echo "::error::article_path must exist in the checked-out repository so the smoke test exercises the real article-content handoff path."
66 exit 1
67 fi
68 mkdir -p .podcaster-smoke
69 python3 - <<'PY' "$WEEK" "$ARTICLE_PATH" "$ARTICLE_SHA256" .podcaster-smoke/publish-manifest.json
70 import hashlib
71 import json
72 import sys
73 from pathlib import Path
74
75 week, article_path, article_sha, manifest_path = sys.argv[1:]
76 article = Path(article_path)
77 if not article.is_file():
78 raise SystemExit(f"article_path does not exist: {article_path}")
79 article_bytes = article.read_bytes()
80 actual_article_sha = hashlib.sha256(article_bytes).hexdigest()
81 if article_sha:
82 if len(article_sha) != 64 or article_sha.lower() != article_sha or any(ch not in "0123456789abcdef" for ch in article_sha):
83 raise SystemExit("article_sha256 must be lowercase 64-character hex when provided.")
84 if article_sha != actual_article_sha:
85 raise SystemExit("article_sha256 must match ARTICLE_PATH contents when provided.")
86 else:
87 article_sha = actual_article_sha
88 raw_payload = {"week": week, "source": "github", "article_path": article_path}
89 raw_bytes = (json.dumps(raw_payload, sort_keys=True) + "\n").encode("utf-8")
90 raw_sha = hashlib.sha256(raw_bytes).hexdigest()
91 article_size = len(article_bytes)
92
93 def digest(label: str) -> str:
94 return hashlib.sha256(f"{week}:{label}".encode("utf-8")).hexdigest()
95
96 manifest = {
97 "week": week,
98 "run_mode": "normal",
99 "candidate": {"summary_sha256": article_sha},
100 "analysis": {"ai_status": "ai"},
101 "promotion": {"eligible": True, "decision": "promote"},
102 "source_artifacts": [
103 {
104 "role": "raw_github",
105 "path": f"data/raw/{week}.json",
106 "name": f"{week}-raw-github",
107 "sha256": raw_sha,
108 "generated_at": "2026-06-08T10:15:00Z",
109 "crawled_at": "2026-06-08T10:12:00Z",
110 "source_status": "fresh",
111 "exists": True,
112 "size_bytes": len(raw_bytes),
113 "freshness": {"status": "fresh", "reasons": []},
114 "provenance": {
115 "path": f"data/raw/{week}.json",
116 "sha256": raw_sha,
117 },
118 "same_day_reuse": {"status": "reused", "source": "smoke"},
119 "sources_requested": ["github"],
120 "sources_succeeded": ["github"],
121 "sources_failed": [],
122 },
123 {
124 "role": "published_summary",
125 "path": article_path,
126 "href": f"https://example.com/{week}/source-index.json",
127 "sha256": article_sha,
128 "generated_at": "2026-06-08T11:20:00Z",
129 "exists": True,
130 "size_bytes": article_size,
131 "provenance": {
132 "path": article_path,
133 "sha256": article_sha,
134 },
135 "source_reuse_summary": {"reused": False},
136 },
137 {
138 "role": "operator_packet",
139 "name": "weekly-publishing-packet",
140 "uri": f"https://example.com/{week}/publishing-packet.json",
141 "artifact_checksum": digest("publishing-packet"),
142 "schema_checksum": digest("publishing-packet-schema"),
143 "source_config_checksum": digest("publishing-packet-config"),
144 "source_artifact_provenance": {"source": "smoke-manifest"},
145 "week": week,
146 },
147 ],
148 }
149 Path(manifest_path).write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
150 PY
151 python3 - <<'PY' "$WEEK" "$ARTICLE_URL" "$ARTICLE_PATH" "$PUBLISH_RUN_ID" .podcaster-smoke/publish-manifest.json
152 import sys
153 from pathlib import Path
154
155 from scripts.podcaster_handoff import build_payload
156
157 week, article_url, article_path, publish_run_id, manifest_path = sys.argv[1:]
158 payload = build_payload(
159 week=week,
160 article_url=article_url,
161 article_path=article_path,
162 publish_run_id=publish_run_id,
163 publish_mode="normal",
164 manifest_path=Path(manifest_path),
165 podcast_config_path=Path("config/podcast.json"),
166 podcaster_dry_run=True,
167 )
168 required_fields = (
169 "source_artifacts",
170 "podcast_config",
171 "script_directions",
172 "spotify_publish",
173 "article_content",
174 "article_title",
175 "dry_run",
176 )
177 missing = [field for field in required_fields if field not in payload]
178 if missing:
179 raise SystemExit(f"smoke payload is missing expected real-handoff fields: {', '.join(missing)}")
180 if not payload["source_artifacts"]:
181 raise SystemExit("smoke payload must include representative source_artifacts")
182 PY
183 python3 scripts/podcaster_handoff.py \
184 --week "$WEEK" \
185 --article-url "$ARTICLE_URL" \
186 --article-path "$ARTICLE_PATH" \
187 --publish-run-id "$PUBLISH_RUN_ID" \
188 --publish-mode normal \
189 --manifest .podcaster-smoke/publish-manifest.json \
190 --podcast-config config/podcast.json \
191 --podcaster-dry-run