460
self.assertIn("📊 **SquadScope Week", webhook_run)
461
self.assertIn("Webhook post failed (non-critical)", webhook_run)
462
463
- def test_podcaster_handoff_runs_only_after_normal_deploy_without_blocking_deploy(self) -> None:
464
- workflow_path = Path(".github/workflows/crawl-and-publish.yml")
465
- workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8"))
466
-
467
- podcaster_job = workflow["jobs"]["podcaster-handoff"]
468
- self.assertEqual(podcaster_job["needs"], ["analyze", "generate", "deploy"])
469
- checkout_step = next(
470
- (s for s in podcaster_job["steps"] if s.get("name") == "Check out repository"), None
471
- )
472
- self.assertIsNotNone(checkout_step)
473
- self.assertTrue(_uses_action(checkout_step, "actions/checkout"))
474
- self.assertFalse(checkout_step["with"]["persist-credentials"])
475
- download_step = next(
476
- (s for s in podcaster_job["steps"] if s.get("name") == "Download analysis candidate"),
477
- None,
478
- )
479
- self.assertIsNotNone(download_step)
480
- self.assertTrue(_uses_action(download_step, "actions/download-artifact"))
481
- self.assertEqual(podcaster_job["if"], "${{ needs.analyze.outputs.run_mode == 'normal' }}")
482
- self.assertNotIn("continue-on-error", podcaster_job)
483
- self.assertNotIn("force-replace", podcaster_job["if"])
484
- self.assertNotIn("restore", podcaster_job["if"])
485
-
486
- deploy_job = workflow["jobs"]["deploy"]
463
+ def test_podcaster_handoff_triggers_post_merge_from_sync_not_crawl(self) -> None:
464
+ # Handoff must fire only AFTER the weekly article is merged to main, so it
465
+ # lives in sync-publish-to-main (post-merge), not in crawl-and-publish
466
+ # (which deploys from artifacts before the merge — the W27 stub race).
467
+ crawl = yaml.safe_load(
468
+ Path(".github/workflows/crawl-and-publish.yml").read_text(encoding="utf-8")
469
+ )
470
+ self.assertNotIn("podcaster-handoff", crawl["jobs"])
471
+ deploy_job = crawl["jobs"]["deploy"]
472
self.assertEqual(deploy_job["needs"], ["crawl", "analyze", "generate"])
488
- self.assertNotIn("podcaster-handoff", deploy_job["needs"])
473
490
- notify_step = next(
491
- (s for s in podcaster_job["steps"] if s.get("name") == "Notify Podcaster"), None
492
- )
493
- self.assertIsNotNone(notify_step)
494
- self.assertEqual(notify_step["env"]["PODCASTER_ENDPOINT"], "${{ vars.PODCASTER_ENDPOINT }}")
495
- self.assertEqual(
496
- notify_step["env"]["PODCASTER_API_KEY"], "${{ secrets.PODCASTER_API_KEY }}"
474
+ sync = yaml.safe_load(
475
+ Path(".github/workflows/sync-publish-to-main.yml").read_text(encoding="utf-8")
476
)
498
- run_script = notify_step["run"]
499
- self.assertIn("article_url_from_page_path", run_script)
477
+ steps = sync["jobs"]["sync"]["steps"]
478
+ trigger = next((s for s in steps if s.get("name") == "Trigger Podcaster after merge"), None)
479
+ self.assertIsNotNone(trigger)
480
+ self.assertEqual(trigger["if"], "${{ steps.sync.outputs.merged == 'true' }}")
481
+ self.assertEqual(trigger["env"]["PODCASTER_ENDPOINT"], "${{ vars.PODCASTER_ENDPOINT }}")
482
+ self.assertEqual(trigger["env"]["PODCASTER_API_KEY"], "${{ secrets.PODCASTER_API_KEY }}")
483
+ run_script = trigger["run"]
484
self.assertIn("scripts/publish_manifest.py assert-eligible", run_script)
501
- self.assertIn("publish manifest is not eligible", run_script)
485
self.assertIn("scripts/podcaster_handoff.py", run_script)
503
- self.assertIn('--article-path "$PAGE_PATH"', run_script)
504
- self.assertIn('--publish-run-id "$PUBLISH_RUN_ID"', run_script)
505
- self.assertIn('--publish-mode "$RUN_MODE"', run_script)
506
- self.assertIn('--manifest "$MANIFEST_FILE"', run_script)
507
- self.assertNotIn("weekly article publication remains complete", run_script)
486
+ self.assertIn("--require-merged", run_script)
487
self.assertNotIn("--force", run_script)
509
- self.assertNotIn("--dry-run", run_script)
488
self.assertNotIn("echo $PODCASTER_API_KEY", run_script)
511
- self.assertNotIn("curl", run_script)
489
490
def test_podcaster_smoke_workflow_exercises_real_weekly_payload_shape(self) -> None:
491
workflow_path = Path(".github/workflows/podcaster-handoff-smoke.yml")