Skip to content

Autogen Docs Slack Notify #18

Autogen Docs Slack Notify

Autogen Docs Slack Notify #18

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