Fix generate step failure and issue creation (#221)

* docs(squad): log Ralph round 3 — PR fixes & merges #217 #216 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix generate step failure and issue creation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: add actions:read permission to notify-failure, use set -euo pipefail - Add actions:read so gh run view can read job conclusions - Replace $GITHUB_RUN_ID with ${{ github.run_id }} for consistency - Add set -euo pipefail to catch masked errors - Update tests to match Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed Jun 1, 2026 at 13:40 UTC f12a08a98f3cff6dfd6e50e65a4506534c620dd2
7 files changed +135 -1
.github/workflows/crawl-and-publish.yml
+56 -1
@@ -609,7 +609,19 @@ jobs:
609 git checkout -f -B "$DATA_BRANCH" "origin/$DEFAULT_BRANCH"
610 fi
611 mkdir -p content/weekly content/monthly content/yearly
612 - # Required: use exact page_path from generate-content step; hard-fail if missing
612 + # generate_content.py returns an absolute path; normalize it before restoring
613 + # the freshly generated page onto the publish branch checkout.
614 + case "$PAGE_PATH" in
615 + "$GITHUB_WORKSPACE"/*) PAGE_PATH="${PAGE_PATH#"$GITHUB_WORKSPACE"/}" ;;
616 + esac
617 + PAGE_PATH="${PAGE_PATH#/}"
618 + case "$PAGE_PATH" in
619 + content/weekly/*) ;;
620 + *)
621 + echo "::error::Expected PAGE_PATH under content/weekly/, got: $PAGE_PATH"
622 + exit 1
623 + ;;
624 + esac
625 RELATIVE_FROM_WEEKLY="${PAGE_PATH#content/weekly/}"
626 mkdir -p "$(dirname "$PAGE_PATH")"
627 cp "content-weekly-backup/${RELATIVE_FROM_WEEKLY}" "$PAGE_PATH"
@@ -782,6 +794,49 @@ jobs:
794 -H "Content-Type: application/json" \
795 -d "$PAYLOAD" || echo "Webhook post failed (non-critical)"
796
797 + notify-failure:
798 + needs: [crawl, analyze, generate, deploy, notify]
799 + if: ${{ always() && contains(needs.*.result, 'failure') }}
800 + runs-on: ubuntu-latest
801 + permissions:
802 + actions: read
803 + issues: write
804 +
805 + steps:
806 + - name: Create or update failure issue
807 + env:
808 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
809 + GH_REPO: ${{ github.repository }}
810 + run: |
811 + set -euo pipefail
812 + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
813 + FAILED_JOBS=$(gh run view "${{ github.run_id }}" --json jobs --jq '[.jobs[] | select(.conclusion=="failure") | .name] | join(", ")')
814 + if [ -z "$FAILED_JOBS" ]; then
815 + FAILED_JOBS="unknown"
816 + fi
817 +
818 + EXISTING=$(gh issue list --state open --search 'in:title "Crawl and publish pipeline failed"' --json number --jq length)
819 + if [ "$EXISTING" -gt 0 ]; then
820 + ISSUE_NUM=$(gh issue list --state open --search 'in:title "Crawl and publish pipeline failed"' --json number --jq '.[0].number')
821 + COMMENT_BODY=$(printf '%s\n\n%s' \
822 + "Pipeline failed again: ${RUN_URL}" \
823 + "Failed jobs: ${FAILED_JOBS}")
824 + gh issue comment "$ISSUE_NUM" --body "$COMMENT_BODY"
825 + echo "Updated existing issue #$ISSUE_NUM"
826 + else
827 + BODY=$(printf '%s\n\n%s\n%s\n\n%s\n%s\n%s' \
828 + "The Crawl and publish weekly data workflow failed." \
829 + "**Run:** ${RUN_URL}" \
830 + "**Failed jobs:** ${FAILED_JOBS}" \
831 + "Please triage:" \
832 + "- If transient (network/rate limit), close with context" \
833 + "- If real bug, assign to the right squad member")
834 + gh issue create \
835 + --title "🔴 Crawl and publish pipeline failed (run ${{ github.run_id }})" \
836 + --label bug \
837 + --label squad \
838 + --body "$BODY"
839 + fi
840
841 reskill-check:
842 needs: [crawl]
.squad/agents/amy/history.md
+9
@@ -29,3 +29,12 @@
29 - PR #219 opened with clean Hugo build
30 - Mobile UX improved for topics navigation
31 - Responsive design verified on small screens
32 +
33 +## Round 2026-06-01T12:41
34 +
35 +### PR #219 Merged
36 +- Review comments resolved: front matter `.Description` used, h2→h3 for card titles
37 +- Accessibility improvement: hidden topic chips on screens ≤768px
38 +- Commit e39720c
39 +- Merged (squash)
40 +- Issue #216 closed
.squad/agents/fry/history.md
+11
@@ -9,6 +9,7 @@
9 - End-to-end checks matter more than isolated unit confidence when artifacts move across crawl, analyze, and publish stages.
10 - Raw crawl output can be publishable with curation, but trend filters still need skepticism about exploit noise and weak momentum data.
11 - 2026-06-01: Issue #217 came from the analysis stage: Copilot retries could fail the tightened gate with generic week-title output or missing files, so the prompt now injects concrete week/year values, the workflow falls back to GitHub Models after Copilot gate failures, and `scripts/analyze_fallback.py` now works when invoked exactly as CI runs it.
12 +- 2026-06-01: Issue #220 came from the generate stage: `generate_content.py` emits an absolute `page_path`, but the publish-branch commit step treated it as `content/weekly/...`, so the restore `cp` doubled the path and failed. The crawl workflow also had no failure-issue job, so generate/deploy failures were silent unless someone checked Actions manually.
13
14 ## Round 2026-06-01T12:19
15
@@ -16,3 +17,13 @@
17 - PR #218 opened with all tests passing
18 - Fallback mechanism now includes GitHub Models support
19 - Copilot prompt validation hardened
20 +
21 +## Round 2026-06-01T12:41
22 +
23 +### PR #218 Merged
24 +- Review comment resolved: removed outer quotes from YAML title hint
25 +- Added regression test
26 +- 542 tests passing
27 +- Commit 9d15b18
28 +- Merged (squash)
29 +- Issue #217 closed
.squad/agents/ralph/history.md
+10
@@ -6,3 +6,13 @@
6 ## Learnings
7 - Project memory is most useful when repeated guidance is compressed into stable summaries instead of copied into every agent file.
8 - Reskill cycles are the right time to turn shared context into reusable skills.
9 +
10 +## Round 2026-06-01T12:41
11 +
12 +### Coordination & Merge Cycle
13 +- Resolved all review conversations via GraphQL API
14 +- Merged PR #218 (squash): Fry's test & prompt fix
15 +- Resolved merge conflict on PR #218 branch
16 +- Merged PR #219 (squash): Amy's accessibility & UX fix
17 +- Issues #216, #217 now closed
18 +- Decision inbox consolidated: Fry's quality-gate fallback decision merged into decisions.md
.squad/decisions.md
+15
@@ -6,6 +6,21 @@ Applies to future weekly summaries and any generator work that consumes `data/an
6
7 ---
8
9 +# Fry: quality gate fallback hardening
10 +
11 +Date: 2026-06-01
12 +
13 +## Context
14 +Issue #217 showed the weekly analysis job can fail even when crawl data is healthy because Copilot sometimes returns a generic placeholder title or no output file at all after retries.
15 +
16 +## Decision
17 +Keep Copilot CLI as the primary analysis generator, but if its output still fails the quality gate after retries, immediately fall back to `scripts/analyze_fallback.py` via GitHub Models. Also render the prompt with concrete `week`, `year`, and title guidance so the model is less likely to echo placeholder frontmatter.
18 +
19 +## Rationale
20 +This keeps the higher-quality primary path, but removes CI flakiness from transient Copilot failures and from prompt placeholders leaking into the final markdown.
21 +
22 +---
23 +
24 # Directive: Prevent Recrawl on Previous-Week Rebuilds
25
26 **Date:** 2026-05-25T15:55:00+02:00
.squad/decisions/inbox/fry-generate-step.md new
+12
@@ -0,0 +1,12 @@
1 +# Fry — generate-step failure handling
2 +
3 +Date: 2026-06-01
4 +
5 +## Context
6 +Issue #220 showed the crawl-and-publish workflow could finish crawl and analysis successfully, then fail in the generate handoff because the generated weekly page path was absolute while the publish-branch restore logic assumed a repository-relative path. The same workflow also lacked a failure-to-issue bridge, so repeated pipeline failures did not automatically open or update a GitHub issue.
7 +
8 +## Decision
9 +Normalize `page_path` to a repo-relative `content/weekly/...` path inside the generate commit step before copying weekly output onto the publish branch. Add a dedicated `notify-failure` job that always evaluates after the pipeline jobs and creates or updates a GitHub issue whenever any crawl/analyze/generate/deploy/notify job fails.
10 +
11 +## Rationale
12 +The path normalization fixes the actual handoff bug without changing `scripts/generate_content.py`, which already returns an absolute file path used elsewhere in tests. A separate failure notifier makes regressions visible even when later jobs are skipped, which is the exact reliability gap that hid the recent failures.
tests/test_pipeline.py
+22
@@ -260,6 +260,9 @@ class WorkflowConfigTests(unittest.TestCase):
260 self.assertIn("git add content/weekly/", commit_run)
261 self.assertIn("content/monthly/", commit_run)
262 self.assertIn("content/yearly/", commit_run)
263 + self.assertIn("GITHUB_WORKSPACE", commit_run)
264 + self.assertIn('case "$PAGE_PATH" in', commit_run)
265 + self.assertIn("Expected PAGE_PATH under content/weekly/", commit_run)
266
267 upload_step = next((s for s in generate_job["steps"] if s.get("name") == "Upload generated content artifact"), None)
268 self.assertIsNotNone(upload_step)
@@ -290,6 +293,25 @@ class WorkflowConfigTests(unittest.TestCase):
293 self.assertIn("📊 **SquadScope Week", webhook_run)
294 self.assertIn("Webhook post failed (non-critical)", webhook_run)
295
296 + def test_notify_failure_job_creates_or_updates_issue(self) -> None:
297 + workflow_path = Path(".github/workflows/crawl-and-publish.yml")
298 + workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8"))
299 +
300 + notify_failure_job = workflow["jobs"]["notify-failure"]
301 + self.assertEqual(notify_failure_job["needs"], ["crawl", "analyze", "generate", "deploy", "notify"])
302 + self.assertEqual(notify_failure_job["if"], "${{ always() && contains(needs.*.result, 'failure') }}")
303 + self.assertEqual(notify_failure_job["permissions"], {"actions": "read", "issues": "write"})
304 +
305 + create_issue_step = next((s for s in notify_failure_job["steps"] if s.get("name") == "Create or update failure issue"), None)
306 + self.assertIsNotNone(create_issue_step)
307 + self.assertEqual(create_issue_step["env"]["GITHUB_TOKEN"], "${{ secrets.GITHUB_TOKEN }}")
308 + create_issue_run = create_issue_step["run"]
309 + self.assertIn('gh run view "${{ github.run_id }}" --json jobs', create_issue_run)
310 + self.assertIn('gh issue list --state open --search', create_issue_run)
311 + self.assertIn('gh issue comment "$ISSUE_NUM"', create_issue_run)
312 + self.assertIn('gh issue create', create_issue_run)
313 + self.assertIn('Crawl and publish pipeline failed', create_issue_run)
314 +
315
316 class PipelineIntegrationTests(unittest.TestCase):
317 def test_crawl_script_produces_valid_json_output_schema(self) -> None: