security: remediate zizmor Actions alerts (#351)

Operator-reviewed; all 83 zizmor alerts fixed, template-injection cleared, comments resolved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed Jun 11, 2026 at 00:51 UTC db32b45b04e214c4a5cd6d09cef2c6a341f30198
5 files changed +151 -96
.github/workflows/crawl-and-publish.yml
+103 -67
@@ -63,8 +63,8 @@ jobs:
63 contents: write
64
65 steps:
66 - - name: Check out repository
67 - uses: actions/checkout@v4
66 + - name: Check out repository # zizmor: ignore[artipacked] crawl job pushes to the publish branch; checkout token is reused by a later git push
67 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
68 with:
69 fetch-depth: 0
70 ref: ${{ github.event.repository.default_branch }}
@@ -88,7 +88,7 @@ jobs:
88
89 - name: Find latest successful crawl cache
90 id: previous-cache-run
91 - uses: actions/github-script@v7
91 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
92 with:
93 script: |
94 const workflowId = 'crawl-and-publish.yml';
@@ -128,7 +128,7 @@ jobs:
128 - name: Restore previous crawl cache
129 if: steps.previous-cache-run.outputs.run_id != ''
130 continue-on-error: true
131 - uses: actions/download-artifact@v4
131 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
132 with:
133 github-token: ${{ secrets.GITHUB_TOKEN }}
134 run-id: ${{ steps.previous-cache-run.outputs.run_id }}
@@ -138,7 +138,7 @@ jobs:
138 - name: Download previous raw artifacts for same-day reuse
139 if: ${{ steps.previous-cache-run.outputs.run_id != '' && !inputs.rebuild_week }}
140 continue-on-error: true
141 - uses: actions/download-artifact@v4
141 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
142 with:
143 github-token: ${{ secrets.GITHUB_TOKEN }}
144 run-id: ${{ steps.previous-cache-run.outputs.run_id }}
@@ -148,7 +148,7 @@ jobs:
148 - name: Download previous snapshots for same-day reuse
149 if: ${{ steps.previous-cache-run.outputs.run_id != '' && !inputs.rebuild_week }}
150 continue-on-error: true
151 - uses: actions/download-artifact@v4
151 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
152 with:
153 github-token: ${{ secrets.GITHUB_TOKEN }}
154 run-id: ${{ steps.previous-cache-run.outputs.run_id }}
@@ -156,7 +156,7 @@ jobs:
156 path: .artifact-reuse/snapshots/
157
158 - name: Set up Python
159 - uses: actions/setup-python@v5
159 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
160 with:
161 python-version: '3.12'
162
@@ -218,7 +218,7 @@ jobs:
218
219 - name: Upload raw crawl artifact
220 if: always()
221 - uses: actions/upload-artifact@v4
221 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
222 with:
223 name: raw-data
224 path: data/raw/
@@ -226,7 +226,7 @@ jobs:
226
227 - name: Upload snapshot artifact
228 if: always()
229 - uses: actions/upload-artifact@v4
229 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
230 with:
231 name: crawl-snapshots
232 path: data/snapshots/
@@ -234,7 +234,7 @@ jobs:
234
235 - name: Upload cache artifact
236 if: always()
237 - uses: actions/upload-artifact@v4
237 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
238 with:
239 name: crawl-cache
240 path: data/cache/
@@ -302,14 +302,15 @@ jobs:
302
303 steps:
304 - name: Check out repository
305 - uses: actions/checkout@v4
305 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
306 with:
307 fetch-depth: 0
308 ref: ${{ github.event.repository.default_branch }}
309 + persist-credentials: false
310
311 - name: Download raw crawl artifact
312 if: ${{ !inputs.rebuild_week }}
312 - uses: actions/download-artifact@v4
313 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
314 with:
315 name: raw-data
316 path: data/raw/
@@ -346,12 +347,12 @@ jobs:
347 fi
348
349 - name: Set up Node
349 - uses: actions/setup-node@v4
350 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
351 with:
352 node-version: '22'
353
354 - name: Set up Python
354 - uses: actions/setup-python@v5
355 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
356 with:
357 python-version: '3.12'
358
@@ -407,10 +408,13 @@ jobs:
408
409 - name: Run correlation and press context
410 id: press-context
411 + env:
412 + IN_WEEK: ${{ steps.analysis-context.outputs.week }}
413 + IN_WEEK_FILE: ${{ steps.analysis-context.outputs.week_file }}
414 run: |
415 set -euo pipefail
412 - WEEK="${{ steps.analysis-context.outputs.week }}"
413 - WEEK_FILE="${{ steps.analysis-context.outputs.week_file }}"
416 + WEEK="$IN_WEEK"
417 + WEEK_FILE="$IN_WEEK_FILE"
418 TC_FILE="data/raw/${WEEK}-external-news.json"
419 if [ ! -f "$TC_FILE" ]; then
420 TC_FILE="data/raw/${WEEK}-techcrunch.json"
@@ -446,13 +450,19 @@ jobs:
450
451 - name: Render and preflight analysis prompt
452 id: prompt-preflight
453 + env:
454 + IN_WEEK_FILE: ${{ steps.analysis-context.outputs.week_file }}
455 + IN_WEEK: ${{ steps.analysis-context.outputs.week }}
456 + IN_OUTPUT_FILE: ${{ steps.analysis-context.outputs.candidate_output_file }}
457 + IN_CURRENT_DATETIME: ${{ steps.analysis-context.outputs.current_datetime }}
458 + IN_PRESS_FILE: ${{ steps.press-context.outputs.press_file }}
459 run: |
460 set -euo pipefail
451 - WEEK_FILE="${{ steps.analysis-context.outputs.week_file }}"
452 - WEEK="${{ steps.analysis-context.outputs.week }}"
453 - OUTPUT_FILE="${{ steps.analysis-context.outputs.candidate_output_file }}"
454 - CURRENT_DATETIME="${{ steps.analysis-context.outputs.current_datetime }}"
455 - PRESS_FILE="${{ steps.press-context.outputs.press_file }}"
461 + WEEK_FILE="$IN_WEEK_FILE"
462 + WEEK="$IN_WEEK"
463 + OUTPUT_FILE="$IN_OUTPUT_FILE"
464 + CURRENT_DATETIME="$IN_CURRENT_DATETIME"
465 + PRESS_FILE="$IN_PRESS_FILE"
466 DIAGNOSTICS_DIR="$(dirname "$OUTPUT_FILE")/diagnostics"
467 PROMPT_FILE="data/metrics/analysis-prompt-${WEEK}.md"
468 PREFLIGHT_JSON="$DIAGNOSTICS_DIR/analysis-input-manifest.json"
@@ -511,21 +521,32 @@ jobs:
521 COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GH_TOKEN }}
522 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
523 PREFLIGHT_REPORT_JSON: ${{ steps.prompt-preflight.outputs.preflight_report_json }}
524 + IN_OUTPUT_FILE: ${{ steps.analysis-context.outputs.candidate_output_file }}
525 + IN_WEEK_FILE: ${{ steps.analysis-context.outputs.week_file }}
526 + IN_WEEK: ${{ steps.analysis-context.outputs.week }}
527 + IN_CURRENT_DATETIME: ${{ steps.analysis-context.outputs.current_datetime }}
528 + IN_MANIFEST_FILE: ${{ steps.analysis-context.outputs.publish_manifest_file }}
529 + IN_PUBLISHED_SUMMARY: ${{ steps.analysis-context.outputs.published_output_file }}
530 + IN_PRESS_FILE: ${{ steps.press-context.outputs.press_file }}
531 + IN_ANALYSIS_PATH: ${{ inputs.analysis_path || 'single-pass' }}
532 + IN_RUN_MODE: ${{ steps.analysis-context.outputs.run_mode }}
533 + IN_PROMPT_FILE: ${{ steps.prompt-preflight.outputs.prompt_file }}
534 + IN_GATE_REPORT_FILE: ${{ steps.analysis-context.outputs.analysis_gate_report_file }}
535 run: |
536 set -euo pipefail
516 - OUTPUT_FILE="${{ steps.analysis-context.outputs.candidate_output_file }}"
517 - WEEK_FILE="${{ steps.analysis-context.outputs.week_file }}"
518 - WEEK="${{ steps.analysis-context.outputs.week }}"
519 - CURRENT_DATETIME="${{ steps.analysis-context.outputs.current_datetime }}"
520 - MANIFEST_FILE="${{ steps.analysis-context.outputs.publish_manifest_file }}"
521 - PUBLISHED_SUMMARY="${{ steps.analysis-context.outputs.published_output_file }}"
522 - PRESS_FILE="${{ steps.press-context.outputs.press_file }}"
523 - ANALYSIS_PATH="${{ inputs.analysis_path || 'single-pass' }}"
524 - RUN_MODE="${{ steps.analysis-context.outputs.run_mode }}"
537 + OUTPUT_FILE="$IN_OUTPUT_FILE"
538 + WEEK_FILE="$IN_WEEK_FILE"
539 + WEEK="$IN_WEEK"
540 + CURRENT_DATETIME="$IN_CURRENT_DATETIME"
541 + MANIFEST_FILE="$IN_MANIFEST_FILE"
542 + PUBLISHED_SUMMARY="$IN_PUBLISHED_SUMMARY"
543 + PRESS_FILE="$IN_PRESS_FILE"
544 + ANALYSIS_PATH="$IN_ANALYSIS_PATH"
545 + RUN_MODE="$IN_RUN_MODE"
546 ANALYSIS_STARTED=$(date +%s)
547 DIAGNOSTICS_DIR="$(dirname "$OUTPUT_FILE")/diagnostics"
548 mkdir -p data/metrics "$(dirname "$OUTPUT_FILE")" "$DIAGNOSTICS_DIR"
528 - PROMPT_FILE="${{ steps.prompt-preflight.outputs.prompt_file }}"
549 + PROMPT_FILE="$IN_PROMPT_FILE"
550
551 sanitize_agent_output() {
552 python3 scripts/sanitize_agent_output.py --path "$1"
@@ -653,7 +674,7 @@ jobs:
674 # The gate applies deterministic metadata/schema repairs and writes an auditable report.
675 if run_quality_gate copilot-cli copilot-default "$GATE_REPORT"; then
676 cp "$OUTPUT_FILE" "$CANDIDATE_SNAPSHOT" 2>/dev/null || true
656 - cp "$GATE_REPORT" "${{ steps.analysis-context.outputs.analysis_gate_report_file }}" 2>/dev/null || true
677 + cp "$GATE_REPORT" "$IN_GATE_REPORT_FILE" 2>/dev/null || true
678 GATE_PASSED=true
679 FINAL_TRANSCRIPT="$TRANSCRIPT_FILE"
680 ANALYSIS_SOURCE="copilot-cli"
@@ -703,7 +724,7 @@ jobs:
724 sanitize_agent_output "$OUTPUT_FILE"
725 NO_AI_GATE_REPORT="$DIAGNOSTICS_DIR/gate-no-ai-attempt-0.json"
726 run_quality_gate no-ai none "$NO_AI_GATE_REPORT" || true
706 - cp "$NO_AI_GATE_REPORT" "${{ steps.analysis-context.outputs.analysis_gate_report_file }}" 2>/dev/null || true
727 + cp "$NO_AI_GATE_REPORT" "$IN_GATE_REPORT_FILE" 2>/dev/null || true
728 cp "$OUTPUT_FILE" "$DIAGNOSTICS_DIR/candidate-no-ai-attempt-0.md" 2>/dev/null || true
729 ANALYSIS_SOURCE="no-ai"
730 ANALYSIS_MODEL="none"
@@ -808,11 +829,13 @@ jobs:
829
830 - name: Assert candidate is eligible for promotion
831 if: ${{ steps.analysis-context.outputs.run_mode != 'dry-run' && steps.analysis-context.outputs.run_mode != 'candidate-only' }}
811 - run: python3 scripts/publish_manifest.py assert-eligible --manifest "${{ steps.analysis-context.outputs.publish_manifest_file }}"
832 + env:
833 + IN_MANIFEST_FILE: ${{ steps.analysis-context.outputs.publish_manifest_file }}
834 + run: python3 scripts/publish_manifest.py assert-eligible --manifest "$IN_MANIFEST_FILE"
835
836 - name: Upload analyzed data
837 if: always()
815 - uses: actions/upload-artifact@v4
838 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
839 with:
840 name: analyzed-data
841 path: data/analyzed/
@@ -820,7 +843,7 @@ jobs:
843
844 - name: Upload analysis candidate
845 if: always()
823 - uses: actions/upload-artifact@v4
846 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
847 with:
848 name: analysis-candidate
849 path: data/candidates/
@@ -837,32 +860,32 @@ jobs:
860 page_path: ${{ steps.generate-content.outputs.page_path }}
861
862 steps:
840 - - name: Check out repository
841 - uses: actions/checkout@v4
863 + - name: Check out repository # zizmor: ignore[artipacked] generate job pushes to the publish branch; checkout token is reused by a later git push
864 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
865 with:
866 fetch-depth: 0
867 ref: ${{ github.event.repository.default_branch }}
868
869 - name: Download analyzed data artifact
847 - uses: actions/download-artifact@v4
870 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
871 with:
872 name: analyzed-data
873 path: data/analyzed/
874
875 - name: Download analysis candidate artifact
853 - uses: actions/download-artifact@v4
876 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
877 with:
878 name: analysis-candidate
879 path: data/candidates/
880
881 - name: Download raw crawl artifact
859 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
882 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
883 with:
884 name: raw-data
885 path: data/raw/
886
887 - name: Set up Python
865 - uses: actions/setup-python@v5
888 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
889 with:
890 python-version: '3.12'
891
@@ -1021,7 +1044,7 @@ jobs:
1044 fi
1045
1046 - name: Upload generated content artifact
1024 - uses: actions/upload-artifact@v4
1047 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
1048 with:
1049 name: generated-content
1050 path: |
@@ -1031,7 +1054,7 @@ jobs:
1054 if-no-files-found: warn
1055
1056 - name: Upload promoted analyzed artifact
1034 - uses: actions/upload-artifact@v4
1057 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
1058 with:
1059 name: promoted-analyzed-data
1060 path: data/analyzed/
@@ -1057,26 +1080,27 @@ jobs:
1080
1081 steps:
1082 - name: Check out repository
1060 - uses: actions/checkout@v4
1083 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
1084 with:
1085 submodules: recursive
1086 fetch-depth: 0
1087 ref: ${{ github.event.repository.default_branch }}
1088 + persist-credentials: false
1089
1090 - name: Download raw crawl artifact
1067 - uses: actions/download-artifact@v4
1091 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
1092 with:
1093 name: raw-data
1094 path: data/raw/
1095
1096 - name: Download analyzed data artifact
1073 - uses: actions/download-artifact@v4
1097 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
1098 with:
1099 name: promoted-analyzed-data
1100 path: data/analyzed/
1101
1102 - name: Download generated content artifact
1079 - uses: actions/download-artifact@v4
1103 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
1104 with:
1105 name: generated-content
1106 path: content/
@@ -1084,7 +1108,7 @@ jobs:
1108
1109 # Deploy builds from artifacts — no dependency on PR merges to main.
1110 - name: Configure GitHub Pages
1087 - uses: actions/configure-pages@v5
1111 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
1112
1113 - name: Install Hugo
1114 run: |
@@ -1119,13 +1143,13 @@ jobs:
1143 run: npx pagefind --site public/
1144
1145 - name: Upload Pages artifact
1122 - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
1146 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
1147 with:
1148 path: ./public
1149
1150 - name: Deploy to GitHub Pages
1151 id: deployment
1128 - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
1152 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
1153
1154 podcaster-handoff:
1155 needs: [analyze, generate, deploy]
@@ -1190,14 +1214,16 @@ jobs:
1214 discussions: write
1215
1216 steps:
1193 - - uses: actions/checkout@v4
1217 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
1218 + with:
1219 + persist-credentials: false
1220
1195 - - uses: actions/download-artifact@v4
1221 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
1222 with:
1223 name: promoted-analyzed-data
1224 path: data/analyzed/
1225
1200 - - uses: actions/download-artifact@v4
1226 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
1227 with:
1228 name: analysis-candidate
1229 path: data/candidates/
@@ -1223,6 +1249,9 @@ jobs:
1249 - name: Post to Discussions
1250 env:
1251 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1252 + REPO_OWNER: ${{ github.repository_owner }}
1253 + REPO_NAME: ${{ github.event.repository.name }}
1254 + GH_REPOSITORY: ${{ github.repository }}
1255 run: |
1256 set -e
1257
@@ -1230,13 +1259,13 @@ jobs:
1259 WEEK=$(basename "$SUMMARY" | sed 's/-summary.md//')
1260
1261 # Create discussion via GraphQL (Announcements category)
1233 - REPO_ID=$(gh api repos/${{ github.repository }} --jq '.node_id')
1262 + REPO_ID=$(gh api "repos/$GH_REPOSITORY" --jq '.node_id')
1263 if [ -z "$REPO_ID" ]; then
1264 echo "::error::Failed to get repository node_id"
1265 exit 1
1266 fi
1267
1239 - CAT_ID=$(gh api graphql -f query='{ repository(owner:"${{ github.repository_owner }}", name:"${{ github.event.repository.name }}") { discussionCategories(first:10) { nodes { id name } } } }' --jq '.data.repository.discussionCategories.nodes[] | select(.name=="Announcements") | .id' 2>&1 || echo "")
1268 + CAT_ID=$(gh api graphql -f query='{ repository(owner:"'"$REPO_OWNER"'", name:"'"$REPO_NAME"'") { discussionCategories(first:10) { nodes { id name } } } }' --jq '.data.repository.discussionCategories.nodes[] | select(.name=="Announcements") | .id' 2>/dev/null || echo "")
1269 if [ -z "$CAT_ID" ]; then
1270 echo "::warning::Announcements discussion category not found; skipping discussion creation"
1271 exit 0
@@ -1279,10 +1308,12 @@ jobs:
1308 env:
1309 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1310 GH_REPO: ${{ github.repository }}
1311 + SERVER_URL: ${{ github.server_url }}
1312 + RUN_ID: ${{ github.run_id }}
1313 run: |
1314 set -euo pipefail
1284 - RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
1285 - FAILED_JOBS=$(gh run view "${{ github.run_id }}" --json jobs --jq '[.jobs[] | select(.conclusion=="failure") | .name] | join(", ")')
1315 + RUN_URL="${SERVER_URL}/${GH_REPO}/actions/runs/${RUN_ID}"
1316 + FAILED_JOBS=$(gh run view "$RUN_ID" --json jobs --jq '[.jobs[] | select(.conclusion=="failure") | .name] | join(", ")')
1317 if [ -z "$FAILED_JOBS" ]; then
1318 FAILED_JOBS="unknown"
1319 fi
@@ -1304,7 +1335,7 @@ jobs:
1335 "- If transient (network/rate limit), close with context" \
1336 "- If real bug, assign to the right squad member")
1337 gh issue create \
1307 - --title "🔴 Crawl and publish pipeline failed (run ${{ github.run_id }})" \
1338 + --title "🔴 Crawl and publish pipeline failed (run ${RUN_ID})" \
1339 --label bug \
1340 --label squad \
1341 --body "$BODY"
@@ -1317,17 +1348,20 @@ jobs:
1348 should_reskill: ${{ steps.check.outputs.reskill }}
1349
1350 steps:
1320 - - uses: actions/checkout@v4
1351 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
1352 with:
1353 fetch-depth: 0
1354 ref: publish
1355 + persist-credentials: false
1356 continue-on-error: true
1357
1358 - name: Fallback to default branch for counter
1359 + env:
1360 + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
1361 run: |
1362 if [ ! -f .squad/run-counter.txt ]; then
1329 - git fetch origin "${{ github.event.repository.default_branch }}" 2>/dev/null || true
1330 - git checkout "origin/${{ github.event.repository.default_branch }}" -- .squad/run-counter.txt 2>/dev/null || true
1363 + git fetch origin "$DEFAULT_BRANCH" 2>/dev/null || true
1364 + git checkout "origin/$DEFAULT_BRANCH" -- .squad/run-counter.txt 2>/dev/null || true
1365 fi
1366
1367 - name: Check reskill trigger
@@ -1350,26 +1384,28 @@ jobs:
1384 contents: write
1385
1386 steps:
1353 - - uses: actions/checkout@v4
1387 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 # zizmor: ignore[artipacked] reskill job pushes to the publish branch; checkout token is reused by a later git push
1388 with:
1389 fetch-depth: 0
1390 ref: publish
1391 continue-on-error: true
1392
1393 - name: Fallback to default branch
1394 + env:
1395 + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
1396 run: |
1397 if [ ! -d .squad ]; then
1362 - git fetch origin "${{ github.event.repository.default_branch }}"
1363 - git checkout "origin/${{ github.event.repository.default_branch }}" -- . 2>/dev/null || true
1398 + git fetch origin "$DEFAULT_BRANCH"
1399 + git checkout "origin/$DEFAULT_BRANCH" -- . 2>/dev/null || true
1400 fi
1401
1402 - name: Set up Node
1367 - uses: actions/setup-node@v4
1403 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
1404 with:
1405 node-version: '22'
1406
1407 - name: Set up Python
1372 - uses: actions/setup-python@v5
1408 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
1409 with:
1410 python-version: '3.12'
1411
.github/workflows/deploy-site.yml
+20 -12
@@ -1,6 +1,6 @@
1 name: Deploy Hugo site
2
3 -on:
3 +on: # zizmor: ignore[dangerous-triggers] intentional post-crawl auto-deploy; build job is guarded by conclusion==success and actor checks
4 push:
5 branches:
6 - main
@@ -14,8 +14,6 @@ on:
14
15 permissions:
16 contents: read
17 - pages: write
18 - id-token: write
17
18 concurrency:
19 group: pages
@@ -28,26 +26,30 @@ jobs:
26 # covers the bot-written publish branch updates.
27 if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.actor != 'github-actions[bot]') || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') }}
28 runs-on: ubuntu-latest
29 + permissions:
30 + contents: read
31 + pages: write
32 env:
33 HUGO_VERSION: 0.161.1
34 HUGO_PARAMS_GA_MEASUREMENT_ID: ${{ secrets.GA_MEASUREMENT_ID }}
35 steps:
36 - name: Check out repository
36 - uses: actions/checkout@v4
37 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
38 with:
39 fetch-depth: 0
40 submodules: recursive
41 + persist-credentials: false
42
43 - name: Configure GitHub Pages
42 - uses: actions/configure-pages@v5
44 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
45
46 - name: Set up Node.js
45 - uses: actions/setup-node@v4
47 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
48 with:
49 node-version: '20'
50
51 - name: Install Hugo
50 - run: |
52 + run: | # zizmor: ignore[github-env] writes a fixed runner path (${HOME}/.local/hugo) to GITHUB_PATH, not attacker input; needed so later steps can run hugo
53 set -euo pipefail
54 RELEASE_URL="https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}"
55 TARBALL="hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz"
@@ -102,20 +104,23 @@ jobs:
104 run: npx pagefind --site public/
105
106 - name: Upload Pages artifact
105 - uses: actions/upload-pages-artifact@v3
107 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
108 with:
109 path: ./public
110
111 deploy:
112 needs: build
113 runs-on: ubuntu-latest
114 + permissions:
115 + pages: write
116 + id-token: write
117 environment:
118 name: github-pages
119 url: ${{ steps.deployment.outputs.page_url }}
120 steps:
121 - name: Deploy to GitHub Pages
122 id: deployment
118 - uses: actions/deploy-pages@v4
123 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
124
125 notify-failure:
126 needs: [build, deploy]
@@ -128,21 +133,24 @@ jobs:
133 env:
134 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
135 GH_REPO: ${{ github.repository }}
136 + SERVER_URL: ${{ github.server_url }}
137 + RUN_ID: ${{ github.run_id }}
138 run: |
139 + RUN_URL="${SERVER_URL}/${GH_REPO}/actions/runs/${RUN_ID}"
140 EXISTING=$(gh issue list --label 'deploy-failure' --state open --json number --jq length)
141 if [ "$EXISTING" -gt 0 ]; then
142 ISSUE_NUM=$(gh issue list --label 'deploy-failure' --state open --json number --jq '.[0].number')
135 - gh issue comment "$ISSUE_NUM" --body "Deploy failed again: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
143 + gh issue comment "$ISSUE_NUM" --body "Deploy failed again: ${RUN_URL}"
144 echo "Updated existing issue #$ISSUE_NUM"
145 else
146 BODY=$(printf '%s\n\n%s\n\n%s\n%s\n%s' \
147 "The Deploy Hugo site workflow failed." \
140 - "**Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
148 + "**Run:** ${RUN_URL}" \
149 "Please triage:" \
150 "- If transient (rate limit, network), close with comment" \
151 "- If real bug, assign to the right squad member")
152 gh issue create \
145 - --title "🔴 Deploy Hugo site failed (run ${{ github.run_id }})" \
153 + --title "🔴 Deploy Hugo site failed (run ${RUN_ID})" \
154 --label 'squad,deploy-failure' \
155 --body "$BODY"
156 fi
.github/workflows/restore-publish-backup.yml
+4 -3
@@ -20,13 +20,14 @@ jobs:
20 runs-on: ubuntu-latest
21 steps:
22 - name: Check out workflow source
23 - uses: actions/checkout@v4
23 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
24 with:
25 ref: ${{ github.sha }}
26 path: workflow-source
27 + persist-credentials: false
28
28 - - name: Check out publish
29 - uses: actions/checkout@v4
29 + - name: Check out publish # zizmor: ignore[artipacked] this job force-pushes the restored backup to publish; checkout token is reused by git push
30 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
31 with:
32 ref: publish
33 fetch-depth: 0
.github/workflows/sync-publish-to-main.yml
+3 -3
@@ -1,6 +1,6 @@
1 name: Sync publish data to main
2
3 -on:
3 +on: # zizmor: ignore[dangerous-triggers] intentional chaining off crawl-and-publish; sync job guarded by workflow_run.conclusion==success
4 workflow_run:
5 workflows: ["Crawl and publish weekly data"]
6 types: [completed]
@@ -21,8 +21,8 @@ jobs:
21 runs-on: ubuntu-latest
22 if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
23 steps:
24 - - name: Check out main
25 - uses: actions/checkout@v4
24 + - name: Check out main # zizmor: ignore[artipacked] sync job force-pushes the sync branch; checkout token is reused by git push
25 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
26 with:
27 ref: main
28 fetch-depth: 0
tests/test_pipeline.py
+21 -11
@@ -15,6 +15,19 @@ import scripts.crawl as crawl
15 import scripts.generate_content as generate_content
16
17
18 +def _uses_action(step: dict, action: str) -> bool:
19 + """Return True if a step uses ``action``, ignoring the version/SHA ref.
20 +
21 + Tolerates SHA-pinned references such as
22 + ``actions/download-artifact@<40-hex-sha> # v4`` by comparing only the
23 + ``owner/repo`` portion before the ``@``.
24 + """
25 + uses = step.get("uses")
26 + if not isinstance(uses, str):
27 + return False
28 + return uses.split("@", 1)[0] == action
29 +
30 +
31 class _FakeHTTPResponse(io.BytesIO):
32 def __enter__(self):
33 return self
@@ -385,7 +398,7 @@ class WorkflowConfigTests(unittest.TestCase):
398
399 notify_job = workflow["jobs"]["notify"]
400 self.assertEqual(notify_job["needs"], ["analyze", "generate", "deploy"])
388 - analyzed_download = next((s for s in notify_job["steps"] if s.get("uses") == "actions/download-artifact@v4" and s.get("with", {}).get("path") == "data/analyzed/"), None)
401 + analyzed_download = next((s for s in notify_job["steps"] if _uses_action(s, "actions/download-artifact") and s.get("with", {}).get("path") == "data/analyzed/"), None)
402 self.assertIsNotNone(analyzed_download)
403 self.assertEqual(analyzed_download["with"]["name"], "promoted-analyzed-data")
404
@@ -417,16 +430,12 @@ class WorkflowConfigTests(unittest.TestCase):
430 podcaster_job = workflow["jobs"]["podcaster-handoff"]
431 self.assertEqual(podcaster_job["needs"], ["analyze", "generate", "deploy"])
432 checkout_step = next((s for s in podcaster_job["steps"] if s.get("name") == "Check out repository"), None)
420 - self.assertEqual(
421 - checkout_step["uses"],
422 - "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd",
423 - )
433 + self.assertIsNotNone(checkout_step)
434 + self.assertTrue(_uses_action(checkout_step, "actions/checkout"))
435 self.assertFalse(checkout_step["with"]["persist-credentials"])
436 download_step = next((s for s in podcaster_job["steps"] if s.get("name") == "Download analysis candidate"), None)
426 - self.assertEqual(
427 - download_step["uses"],
428 - "actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093",
429 - )
437 + self.assertIsNotNone(download_step)
438 + self.assertTrue(_uses_action(download_step, "actions/download-artifact"))
439 self.assertEqual(podcaster_job["if"], "${{ needs.analyze.outputs.run_mode == 'normal' }}")
440 self.assertTrue(podcaster_job["continue-on-error"])
441 self.assertNotIn("force-replace", podcaster_job["if"])
@@ -507,7 +516,7 @@ class WorkflowConfigTests(unittest.TestCase):
516 s
517 for s in generate["steps"]
518 if s.get("name") == "Download raw crawl artifact"
510 - and s.get("uses") == "actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093"
519 + and _uses_action(s, "actions/download-artifact")
520 and s.get("with", {}).get("name") == "raw-data"
521 and s.get("with", {}).get("path") == "data/raw/"
522 ),
@@ -564,7 +573,8 @@ class WorkflowConfigTests(unittest.TestCase):
573 self.assertIsNotNone(create_issue_step)
574 self.assertEqual(create_issue_step["env"]["GITHUB_TOKEN"], "${{ secrets.GITHUB_TOKEN }}")
575 create_issue_run = create_issue_step["run"]
567 - self.assertIn('gh run view "${{ github.run_id }}" --json jobs', create_issue_run)
576 + self.assertIn('gh run view "$RUN_ID" --json jobs', create_issue_run)
577 + self.assertEqual(create_issue_step["env"]["RUN_ID"], "${{ github.run_id }}")
578 self.assertIn('gh issue list --state open --search', create_issue_run)
579 self.assertIn('gh issue comment "$ISSUE_NUM"', create_issue_run)
580 self.assertIn('gh issue create', create_issue_run)