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)