12
type: boolean
13
14
permissions:
15
- actions: read
16
- contents: write
17
- pages: write
18
- id-token: write
15
+ contents: read
16
17
concurrency:
18
group: weekly-crawl
25
jobs:
26
crawl:
27
runs-on: ubuntu-latest
28
+ permissions:
29
+ actions: read
30
+ contents: write
31
32
steps:
33
- name: Check out repository
274
275
- name: Install Copilot CLI
276
id: install-copilot
277
- continue-on-error: true
277
run: npm install -g @github/copilot
278
279
- name: Run analysis
303
cat "$PRESS_FILE" >> "$PROMPT_FILE"
304
fi
305
307
- # Intentionally rely on Copilot CLI's default model so CI follows the platform-supported default.
308
- if command -v copilot >/dev/null 2>&1 && copilot \
309
- --agent squad \
310
- -p "Farnsworth, read the file at ${PROMPT_FILE} — it contains the weekly data and analysis instructions. Follow them exactly and write the analysis to ${OUTPUT_FILE}." \
311
- -s \
312
- --no-ask-user \
313
- --allow-tool=read \
314
- --allow-tool=write \
315
- --allow-tool=glob \
316
- --allow-tool=grep \
317
- --share=data/metrics/copilot-transcript.md \
318
- > /dev/null; then
319
- ANALYSIS_SOURCE="copilot-cli"
320
- ANALYSIS_MODEL="copilot-default"
321
- elif python3 scripts/analyze_fallback.py \
322
- --raw-json "$WEEK_FILE" \
323
- --output "$OUTPUT_FILE" \
324
- --current-datetime "$CURRENT_DATETIME" \
325
- --press-context "$PRESS_FILE" 2>/dev/null; then
326
- echo "Copilot CLI unavailable or failed; used GitHub Models API."
327
- ANALYSIS_SOURCE="github-models"
328
- ANALYSIS_MODEL="${GITHUB_MODELS_MODEL:-openai/gpt-4o}"
329
- else
330
- echo "Both Copilot CLI and GitHub Models unavailable; using no-AI data summary."
331
- python3 scripts/analyze_fallback.py \
306
+ if ! command -v copilot >/dev/null 2>&1; then
307
+ echo "::error::Copilot CLI unavailable; rerun crawl-and-publish when Copilot is available."
308
+ exit 1
309
+ fi
310
+
311
+ # Retry loop: LLM output can be non-deterministically truncated,
312
+ # so retry up to 3 attempts if the quality gate rejects the article.
313
+ MAX_RETRIES=2
314
+ ATTEMPT=0
315
+ GATE_PASSED=false
316
+
317
+ while [ "$GATE_PASSED" = "false" ] && [ "$ATTEMPT" -le "$MAX_RETRIES" ]; do
318
+ if [ "$ATTEMPT" -gt 0 ]; then
319
+ echo "::warning::Quality gate failed on attempt $ATTEMPT, retrying (attempt $((ATTEMPT + 1))/$((MAX_RETRIES + 1)))..."
320
+ rm -f "$OUTPUT_FILE"
321
+ # Reset any .squad changes from failed attempt
322
+ git checkout -- .squad 2>/dev/null || true
323
+ fi
324
+
325
+ echo "::notice::Running Copilot analysis attempt $((ATTEMPT + 1))/$((MAX_RETRIES + 1))"
326
+ TRANSCRIPT_FILE="data/metrics/copilot-transcript-attempt-${ATTEMPT}.md"
327
+ rm -f "$TRANSCRIPT_FILE"
328
+
329
+ # Intentionally rely on Copilot CLI's default model so CI follows the platform-supported default.
330
+ if ! copilot \
331
+ --agent squad \
332
+ -p "Farnsworth, read the file at ${PROMPT_FILE} — it contains the weekly data and analysis instructions. Follow them exactly and write the analysis to ${OUTPUT_FILE}." \
333
+ -s \
334
+ --no-ask-user \
335
+ --allow-tool=read \
336
+ --allow-tool=write \
337
+ --allow-tool=glob \
338
+ --allow-tool=grep \
339
+ --share="$TRANSCRIPT_FILE" \
340
+ > /dev/null; then
341
+ echo "::warning::Copilot CLI failed on attempt $((ATTEMPT + 1))"
342
+ ATTEMPT=$((ATTEMPT + 1))
343
+ continue
344
+ fi
345
+
346
+ if ! sanitize_agent_output "$OUTPUT_FILE"; then
347
+ echo "::warning::Sanitization failed on attempt $((ATTEMPT + 1))"
348
+ ATTEMPT=$((ATTEMPT + 1))
349
+ continue
350
+ fi
351
+
352
+ # Inline quality gate check (suppress step summary to avoid noise)
353
+ if GITHUB_STEP_SUMMARY="" python3 scripts/analysis_gate.py \
354
+ --analysis-file "$OUTPUT_FILE" \
355
--raw-json "$WEEK_FILE" \
333
- --output "$OUTPUT_FILE" \
356
--current-datetime "$CURRENT_DATETIME" \
335
- --press-context "$PRESS_FILE" \
336
- --no-ai
337
- ANALYSIS_SOURCE="no-ai"
338
- ANALYSIS_MODEL="none"
357
+ --source copilot-cli 2>&1; then
358
+ GATE_PASSED=true
359
+ FINAL_TRANSCRIPT="$TRANSCRIPT_FILE"
360
+ fi
361
+
362
+ ATTEMPT=$((ATTEMPT + 1))
363
+ done
364
+
365
+ if [ "$GATE_PASSED" = "false" ]; then
366
+ echo "::error::Analysis quality gate failed after $((MAX_RETRIES + 1)) attempts. The LLM did not produce a conforming article."
367
+ exit 1
368
fi
369
341
- sanitize_agent_output "$OUTPUT_FILE"
370
+ ANALYSIS_SOURCE="copilot-cli"
371
+ ANALYSIS_MODEL="copilot-default"
372
+
373
+ # Copy final transcript to canonical location
374
+ cp "$FINAL_TRANSCRIPT" data/metrics/copilot-transcript.md 2>/dev/null || true
375
376
TRANSCRIPT_ARGS=""
377
if [ -f "data/metrics/copilot-transcript.md" ]; then
388
--output-file "$OUTPUT_FILE" \
389
$TRANSCRIPT_ARGS
390
rm -f "$PROMPT_FILE"
391
+ # Clean up per-attempt transcripts
392
+ rm -f data/metrics/copilot-transcript-attempt-*.md
393
echo "analysis_source=$ANALYSIS_SOURCE" >> "$GITHUB_OUTPUT"
394
395
- name: quality-check
632
runs-on: ubuntu-latest
633
permissions:
634
contents: write
635
+ discussions: write
636
637
steps:
638
- uses: actions/checkout@v4
685
run: |
686
SUMMARY=$(ls -t data/analyzed/*-summary.md | head -1)
687
WEEK=$(basename "$SUMMARY" | sed 's/-summary.md//')
652
- SITE_URL="https://jmservera.github.io/SquadScope/weekly/$(echo $WEEK | tr '-' '/' | sed 's/W/w/')/"
653
-
654
- # Post JSON payload (compatible with Discord/Slack webhooks)
688
+ SITE_URL="https://jmservera.github.io/SquadScope/weekly/$(echo "$WEEK" | tr '-' '/' | sed 's/W/w/')/"
689
+
690
+ # Build JSON payload with jq to prevent injection via WEEK or SITE_URL
691
+ PAYLOAD=$(jq -n \
692
+ --arg content "📊 **SquadScope Week ${WEEK}** — New tech trends summary published!\n${SITE_URL}" \
693
+ --arg username "SquadScope" \
694
+ '{content: $content, username: $username}')
695
+
696
curl -s -X POST "$WEBHOOK_URL" \
697
-H "Content-Type: application/json" \
657
- -d "{
658
- \"content\": \"📊 **SquadScope Week $WEEK** — New tech trends summary published!\n$SITE_URL\",
659
- \"username\": \"SquadScope\"
660
- }" || echo "Webhook post failed (non-critical)"
698
+ -d "$PAYLOAD" || echo "Webhook post failed (non-critical)"
699
700
701
reskill-check: