Autogen Docs Slack Notify #18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Autogen Docs Slack Notify | |
| # Notifies the team's #documentation Slack channel when the Upstream | |
| # Release Docs workflow completes successfully on an `autogen-docs` PR. | |
| # | |
| # Triggers on `workflow_run` (not `pull_request_target`) because the | |
| # augment workflow uses GITHUB_TOKEN to flip the PR draft -> ready, and | |
| # GitHub suppresses events emitted by GITHUB_TOKEN to prevent loops. | |
| # The `autogen-docs` label + `renovate[bot]` author guard lives in the | |
| # "Resolve PR" step rather than the job `if:` because those fields are | |
| # not on the `workflow_run` payload and must be fetched via `gh pr view`. | |
| # | |
| # SECURITY MODEL | |
| # -------------- | |
| # `workflow_run` always executes the workflow definition from the | |
| # DEFAULT BRANCH (main), regardless of which branch the triggering PR | |
| # is on -- the same guarantee `pull_request_target` provided. This | |
| # workflow NEVER executes any PR-supplied code; Claude only reads PR | |
| # metadata via the `gh` CLI. | |
| # | |
| # The work is split into two steps so the prompt-injectable Claude | |
| # session can never touch the Slack token or make arbitrary outbound | |
| # network calls: | |
| # STEP 1 (Claude / claude-code-action): composes the message CONTENT | |
| # ONLY and writes it to a JSON file. Its tools are limited to | |
| # `Bash(gh:*)` and `Write` -- there is NO curl, and the Slack | |
| # token is NOT in this step's environment. So even a prompt | |
| # injection embedded in PR content cannot exfiltrate the Slack | |
| # token (it is absent) nor reach an arbitrary host (no curl, no | |
| # general Bash). | |
| # STEP 2 (deterministic `run:` step): a small stdlib-only Python | |
| # script (`.github/scripts/post_autogen_docs_slack.py`) reads that | |
| # JSON, resolves reviewer GitHub handles -> Slack ids, and posts | |
| # ONE message to Slack. Only this step holds `SLACK_BOT_TOKEN`, | |
| # and it only ever contacts slack.com. | |
| # | |
| # --------------------------------------------------------------------- | |
| # Before this can run: | |
| # - Add a NEW repository secret `SLACK_BOT_TOKEN` (a Slack bot token, | |
| # xoxb-...) with these scopes: | |
| # * chat:write - post the message | |
| # * users:read - list users to map GitHub handle -> Slack id | |
| # * users.profile:read - read custom profile fields (the GitHub field) | |
| # * users:read.email - only if email-based matching is used as a fallback | |
| # - The Slack bot must be INVITED to the #documentation channel | |
| # (channel id C06SZA9HBHU) or chat.postMessage will fail with | |
| # `not_in_channel`. | |
| # - Reviewer @-tagging in Slack depends on each reviewer's GitHub | |
| # handle being present in their Slack profile (a "GitHub" custom | |
| # profile field, pushed via Okta). When no match is found the | |
| # message falls back to the plain GitHub @handle as text rather | |
| # than guessing a Slack id. | |
| # | |
| # Channel ids are not secret, so the #documentation channel id is set | |
| # directly as an env var below (DOCS_SLACK_CHANNEL_ID), not as a secret. | |
| # --------------------------------------------------------------------- | |
| on: | |
| workflow_run: | |
| workflows: ['Upstream Release Docs'] | |
| types: [completed] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to notify about (manual retry)' | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| # Required for Claude to read CI results / Actions context on PRs. | |
| actions: read | |
| jobs: | |
| notify: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # For workflow_run: only when the augment job succeeded on a | |
| # pull_request event (skips workflow_dispatch retries and failures). | |
| # workflow_dispatch is gated to the default branch so the workflow | |
| # definition in use is always the trusted main-branch version. | |
| if: | | |
| (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || | |
| (github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.event == 'pull_request') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| MESSAGE_FILE: ${{ github.workspace }}/.autogen-docs-slack-message.json | |
| steps: | |
| # Resolve the PR number (from the workflow_run payload or the | |
| # workflow_dispatch input), verify it carries the autogen-docs | |
| # label and was authored by Renovate, then surface number + url | |
| # as step outputs. For workflow_dispatch the caller is a human | |
| # choosing a specific PR; the label/author guard is skipped. | |
| # Sets skip=true and exits cleanly when the PR is out of scope. | |
| - name: Resolve PR and check eligibility | |
| id: pr | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| PR_NUMBER="${{ inputs.pr_number }}" | |
| else | |
| PR_NUMBER="${{ github.event.workflow_run.pull_requests[0].number }}" | |
| if [ -z "$PR_NUMBER" ]; then | |
| echo "No PR associated with this workflow run; skipping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| fi | |
| PR_JSON=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \ | |
| --json labels,author,url) | |
| if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then | |
| HAS_LABEL=$(echo "$PR_JSON" \ | |
| | jq -r '[.labels[].name] | contains(["autogen-docs"])') | |
| AUTHOR=$(echo "$PR_JSON" | jq -r '.author.login') | |
| if [ "$HAS_LABEL" != "true" ] || \ | |
| ([ "$AUTHOR" != "app/renovate" ] && [ "$AUTHOR" != "renovate[bot]" ]); then | |
| echo "PR #$PR_NUMBER is not an autogen-docs renovate PR; skipping." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| fi | |
| PR_URL=$(echo "$PR_JSON" | jq -r '.url') | |
| echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | |
| echo "url=$PR_URL" >> "$GITHUB_OUTPUT" | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| # claude-code-action unconditionally restores trusted .claude/.mcp.json | |
| # config from the base branch via `git fetch`/`git checkout` before | |
| # running Claude, even in agent mode -- it requires a git working tree | |
| # to exist. workflow_run always executes from the default branch, so | |
| # the checkout below lands trusted code in the workspace. | |
| - name: Checkout base branch | |
| if: steps.pr.outputs.skip != 'true' | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # STEP 1 — Claude composes content only. Tools limited to gh + Write. | |
| # No Slack token in this step's env; no curl tool. So even a prompt | |
| # injection from PR content cannot exfiltrate the Slack token (absent) | |
| # nor make arbitrary network calls (no curl/Bash beyond gh). | |
| - name: Compose reviewer summary | |
| if: steps.pr.outputs.skip != 'true' | |
| uses: anthropics/claude-code-action@51705da45eecce209d4700538bf8377d5b5fc695 # v1.0.152 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| # Skips claude-code-action's OIDC -> GitHub App token exchange | |
| # (which needs id-token: write) and uses the job's own minimally | |
| # scoped GITHUB_TOKEN instead. Unrelated to anthropic_api_key, | |
| # which is the separate Anthropic-auth path. | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| additional_permissions: | | |
| actions: read | |
| # workflow_run actor is github-actions[bot]; the PR is authored | |
| # by renovate[bot]. Both are listed so claude-code-action does | |
| # not refuse to run on a bot-initiated context. | |
| allowed_bots: 'renovate,github-actions' | |
| claude_args: | | |
| --model claude-opus-4-7 | |
| --max-turns 30 | |
| --allowedTools "Bash(gh:*),Write" | |
| # This step's only job is composing a JSON summary from | |
| # `gh pr view` output -- it has no use for this repo's own | |
| # CLAUDE.md/.claude project config, and loading it just | |
| # invites Claude to wander into unrelated workflows that | |
| # the tool allowlist above denies. | |
| --setting-sources user | |
| prompt: | | |
| You are running in GitHub Actions with no interactive user. | |
| Follow these steps exactly and do NOT ask clarifying | |
| questions -- proceed best-effort at every decision point. | |
| Your ONLY job is to COMPOSE the CONTENT of a reviewer | |
| summary about a documentation PR and WRITE it to a JSON | |
| file, then exit. You do NOT post anything. You do NOT call | |
| curl. You have no Slack token and no network access beyond | |
| the `gh` CLI. A later, separate, deterministic step does the | |
| actual Slack posting. | |
| STEP 1 — Read the PR. | |
| Run this exact command (the values below are already | |
| filled in -- do not substitute shell variables, and do | |
| not quote the numbers or the repo name): | |
| gh pr view ${{ steps.pr.outputs.number }} --repo ${{ github.repository }} --json title,body,reviewRequests,files,url | |
| From the JSON, extract: | |
| - the PR title | |
| - the PR body. The body contains a marker-delimited | |
| section written by the Upstream Release Docs workflow, | |
| between `<!-- upstream-release-docs:start -->` and | |
| `<!-- upstream-release-docs:end -->`. Inside it is an | |
| "At a glance" table / summary. PREFER reusing that | |
| summary's content for your bullets; do not pad beyond it. | |
| - the list of changed files (for a fallback sense of scope | |
| only — do not enumerate every file). | |
| - the list of REQUESTED REVIEWERS (reviewRequests[].login | |
| are GitHub logins). | |
| STEP 2 — Write the message content as JSON. | |
| Use the **Write** tool to write a file at exactly this path: | |
| ${{ github.workspace }}/.autogen-docs-slack-message.json | |
| The file MUST be valid JSON with EXACTLY this shape: | |
| { | |
| "headline": "<one line naming the upstream project/version, plain text>", | |
| "summary_bullets": ["...", "..."], | |
| "reviewers": [ | |
| {"login": "<github login>", "note": "<what THIS reviewer should check>"} | |
| ], | |
| "pr_url": "${{ steps.pr.outputs.url }}" | |
| } | |
| Rules: | |
| - "headline" is plain text only (NO Slack/markdown link | |
| syntax, NO ids). The poster step turns it into a link. | |
| - "summary_bullets" is a tight list of 2–4 strings, | |
| reusing the PR's "At a glance" content. No padding, no | |
| raw file lists, no internal ids. | |
| - "reviewers" has one entry per REQUESTED reviewer. "login" | |
| is the plain GitHub login (no "@"). "note" is the | |
| specific thing that reviewer should check. When deciding | |
| each note, keep this shared expectation in mind and let | |
| it shape the notes: the goal is for everyone involved in | |
| the release to review and approve within 2 business days; | |
| a given reviewer's contribution to the release may not | |
| have produced any user- or docs-facing changes, which is | |
| expected and fine, and in that case their approval simply | |
| confirms nothing was missed in the generated docs. | |
| - "pr_url" must be copied exactly as filled in above. | |
| Do NOT include any Slack ids, Slack mrkdwn, `<@...>` tags, or | |
| `<url|text>` links — only the plain content above. Do NOT | |
| post anything. Do NOT call curl. After writing the file, | |
| print a brief confirmation and finish. | |
| # STEP 2 — Deterministic Slack post. Only this step holds the Slack | |
| # token, and it only contacts slack.com. Resolves reviewer GitHub | |
| # handles -> Slack ids here (not in the Claude step). | |
| - name: Post summary to Slack | |
| if: steps.pr.outputs.skip != 'true' | |
| env: | |
| SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} | |
| DOCS_SLACK_CHANNEL_ID: C06SZA9HBHU | |
| PR_URL: ${{ steps.pr.outputs.url }} | |
| run: | | |
| python3 .github/scripts/post_autogen_docs_slack.py |