Add webhook notification integration for team channels (#52)

* feat: add webhook notifications Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Security: Store webhook URLs as secrets instead of variables - Changed workflow to use secrets.WEBHOOK_URL instead of vars.WEBHOOK_URL (lines 500-502 of .github/workflows/crawl-and-publish.yml) This ensures webhook credentials are properly masked in logs. - Updated operator guide to reflect that WEBHOOK_URL should be configured as an Actions secret using 'gh secret set' (docs/operator-guide.md). - Updated test assertions to validate secret-based configuration (tests/test_pipeline.py lines 227-228) Addresses PR #52 review comments: webhook URLs are credentials and should be stored securely as Actions secrets, not as repository variables. 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 May 18, 2026 at 17:18 UTC 4fcf9e32a9d1331f7e9457445580f4558db1aec4
9 files changed +61 -4
.github/workflows/crawl-and-publish.yml
+18 -1
@@ -447,7 +447,7 @@ jobs:
447
448 notify:
449 if: github.event_name == 'schedule' || github.event.inputs.publish_release == 'true'
450 - needs: [generate, deploy]
450 + needs: [analyze, generate, deploy]
451 runs-on: ubuntu-latest
452 permissions:
453 contents: write
@@ -496,6 +496,23 @@ jobs:
496
497 gh api graphql -f query="mutation { createDiscussion(input: {repositoryId: \"$REPO_ID\", categoryId: \"$CAT_ID\", title: \"Week $WEEK — Tech Trends Summary\", body: $BODY}) { discussion { url } } }"
498
499 + - name: Post to webhook
500 + if: secrets.WEBHOOK_URL != ''
501 + env:
502 + WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
503 + run: |
504 + SUMMARY=$(ls -t data/analyzed/*-summary.md | head -1)
505 + WEEK=$(basename "$SUMMARY" | sed 's/-summary.md//')
506 + SITE_URL="https://jmservera.github.io/SquadScope/weekly/$(echo $WEEK | tr '-' '/' | sed 's/W/w/')/"
507 +
508 + # Post JSON payload (compatible with Discord/Slack webhooks)
509 + curl -s -X POST "$WEBHOOK_URL" \
510 + -H "Content-Type: application/json" \
511 + -d "{
512 + \"content\": \"📊 **SquadScope Week $WEEK** — New tech trends summary published!\n$SITE_URL\",
513 + \"username\": \"SquadScope\"
514 + }" || echo "Webhook post failed (non-critical)"
515 +
516
517 reskill-check:
518 needs: [crawl]
docs/operator-guide.md
+21 -3
@@ -76,14 +76,32 @@ Verify the secret exists:
76 gh secret list -R YOUR_USERNAME/SquadScope
77 ```
78
79 -### Step 4: Enable GitHub Pages
79 +### Step 4: Configure optional webhook notifications
80 +
81 +To notify a team channel whenever a weekly summary is published, add a repository secret named `WEBHOOK_URL` (a secret, not a variable, because webhook URLs are credentials that should be masked and protected):
82 +
83 +```bash
84 +gh secret set WEBHOOK_URL --body "https://example.com/webhook" -R YOUR_USERNAME/SquadScope
85 +```
86 +
87 +You can also add it in the GitHub UI under **Settings → Secrets and variables → Actions → Secrets**.
88 +
89 +Supported endpoints:
90 +
91 +- **Discord:** Create a webhook in the target channel's **Edit Channel → Integrations → Webhooks** settings, then paste that webhook URL into `WEBHOOK_URL`.
92 +- **Slack:** Create an **Incoming Webhook** app for the target channel, then paste that webhook URL into `WEBHOOK_URL`.
93 +- **Custom endpoint:** Any endpoint that accepts an HTTP `POST` with a JSON body containing `content` and `username` fields.
94 +
95 +If `WEBHOOK_URL` is unset, the workflow skips the webhook step automatically.
96 +
97 +### Step 5: Enable GitHub Pages
98
99 1. Navigate to repo **Settings → Pages**
100 2. Set **Source** to "GitHub Actions"
101 3. (Optional) Configure custom domain if desired
102 4. Save
103
86 -### Step 5: Test local build
104 +### Step 6: Test local build
105
106 Ensure the Hugo build works locally:
107
@@ -444,7 +462,7 @@ Once SquadScope is running smoothly:
462 1. **Monitor quality:** Review weekly analyses for patterns and trends
463 2. **Iterate prompts:** Based on reskill recommendations, refine analysis quality
464 3. **Extend sources:** Add additional data sources (HackerNews, Reddit, etc.) via MCP tools
447 -4. **Add notifications:** Configure GitHub Releases or webhook integrations
465 +4. **Add notifications:** Configure GitHub Releases, Discord/Slack webhooks, or custom webhook integrations
466 5. **Topic channels:** Explore multi-topic feature (see `docs/PRD-topic-channels.md`)
467
468 SquadScope is designed to improve itself. Trust the system, monitor the trends, and enjoy curated tech news delivered every week.
scripts/__pycache__/__init__.cpython-312.pyc
Binary files /dev/null and b/scripts/__pycache__/__init__.cpython-312.pyc differ
scripts/__pycache__/analysis_gate.cpython-312.pyc
Binary files /dev/null and b/scripts/__pycache__/analysis_gate.cpython-312.pyc differ
scripts/__pycache__/analyze_fallback.cpython-312.pyc
Binary files /dev/null and b/scripts/__pycache__/analyze_fallback.cpython-312.pyc differ
scripts/__pycache__/crawl.cpython-312.pyc
Binary files /dev/null and b/scripts/__pycache__/crawl.cpython-312.pyc differ
scripts/__pycache__/generate_content.cpython-312.pyc
Binary files /dev/null and b/scripts/__pycache__/generate_content.cpython-312.pyc differ
tests/__pycache__/test_pipeline.cpython-312-pytest-9.0.2.pyc
Binary files /dev/null and b/tests/__pycache__/test_pipeline.cpython-312-pytest-9.0.2.pyc differ
tests/test_pipeline.py
+22
@@ -215,6 +215,28 @@ class WorkflowConfigTests(unittest.TestCase):
215 self.assertIn("content/monthly/", upload_step["with"]["path"])
216 self.assertIn("content/yearly/", upload_step["with"]["path"])
217
218 + def test_notify_workflow_posts_optional_webhook(self) -> None:
219 + workflow_path = Path(".github/workflows/crawl-and-publish.yml")
220 + workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8"))
221 +
222 + notify_job = workflow["jobs"]["notify"]
223 + self.assertEqual(notify_job["needs"], ["analyze", "generate", "deploy"])
224 +
225 + webhook_step = next((s for s in notify_job["steps"] if s.get("name") == "Post to webhook"), None)
226 + self.assertIsNotNone(webhook_step)
227 + self.assertEqual(webhook_step["if"], "secrets.WEBHOOK_URL != ''")
228 + self.assertEqual(webhook_step["env"]["WEBHOOK_URL"], "${{ secrets.WEBHOOK_URL }}")
229 +
230 + release_step = next((s for s in notify_job["steps"] if s.get("name") == "Create GitHub Release"), None)
231 + self.assertIsNotNone(release_step)
232 + self.assertEqual(release_step["env"]["SUMMARY_FILE"], "${{ needs.analyze.outputs.summary_file }}")
233 +
234 + webhook_run = webhook_step["run"]
235 + self.assertIn("curl -s -X POST \"$WEBHOOK_URL\"", webhook_run)
236 + self.assertIn("https://jmservera.github.io/SquadScope/weekly/", webhook_run)
237 + self.assertIn('\\"content\\": \\"📊 **SquadScope Week $WEEK**', webhook_run)
238 + self.assertIn("Webhook post failed (non-critical)", webhook_run)
239 +
240
241 class PipelineIntegrationTests(unittest.TestCase):
242 def test_crawl_script_produces_valid_json_output_schema(self) -> None: