-
Notifications
You must be signed in to change notification settings - Fork 3
241 lines (231 loc) · 12.1 KB
/
Copy pathautogen-docs-notify.yml
File metadata and controls
241 lines (231 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
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@700e7f8316990de46bed556429765647af760efc # v1.0.176
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