Skip to content

Commit 593e1cb

Browse files
Add gardener auto-label + Slack notification workflows
Adds two workflows that react to new issues and PRs: - `gardener-notify-event.yml` captures the triggering payload as an artifact on issue/PR open and on `devtools-gardener` label events. Uses `pull_request_target` so Dependabot- and fork-opened PRs still produce an artifact. - `gardener-notify-slack.yml` runs on `workflow_run` completion, downloads the artifact, applies the `devtools-gardener` label on first open, and posts a summary to Slack. Split into two workflows because Dependabot-triggered workflows lose write permissions and secret access; the `workflow_run` follow-up runs in the default-branch context with full permissions regardless of the upstream actor. Requires a `SLACK_GARDENER_BOT_TOKEN` secret and `GARDENER_SLACK_CHANNEL_ID` variable, plus a `devtools-gardener` label. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2f9bc85 commit 593e1cb

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Gardener - Notify Event
2+
# Tiny event capturer: stashes the triggering issue/PR payload as an artifact
3+
# for `gardener-notify-slack.yml` to pick up via workflow_run.
4+
#
5+
# Why two workflows? When Dependabot triggers a workflow, GitHub forces
6+
# GITHUB_TOKEN to read-only and hides Actions secrets — so labeling and
7+
# Slack posting from this workflow would fail on every Dependabot PR. A
8+
# workflow_run-triggered follow-up runs in the default-branch context with
9+
# full permissions and secret access, regardless of the upstream actor.
10+
#
11+
# Uses pull_request_target so fork-opened PRs still produce an artifact.
12+
# No code is checked out here; this workflow only reads the pre-parsed
13+
# event payload, so there is no pwn-request surface.
14+
on:
15+
issues:
16+
types: [opened, labeled]
17+
pull_request_target:
18+
types: [opened, labeled]
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
capture:
25+
if: github.event.action == 'opened' || github.event.label.name == 'devtools-gardener'
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Stash event payload
29+
run: cp "$GITHUB_EVENT_PATH" event.json
30+
31+
- uses: actions/upload-artifact@v4
32+
with:
33+
name: gardener-event
34+
path: event.json
35+
retention-days: 1
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
name: Gardener - Notify Slack
2+
# Runs after `Gardener - Notify Event` completes and does the real work:
3+
# applies the devtools-gardener label and posts a summary to Slack.
4+
#
5+
# The workflow_run trigger runs this job in the default-branch context with
6+
# full GITHUB_TOKEN permissions and Actions secret access — this is what
7+
# lets it succeed for Dependabot-opened PRs, where the upstream event
8+
# workflow can't label or reach secrets directly.
9+
on:
10+
workflow_run:
11+
workflows: ['Gardener - Notify Event']
12+
types: [completed]
13+
14+
permissions:
15+
contents: read
16+
issues: write
17+
pull-requests: write
18+
actions: read
19+
20+
jobs:
21+
notify:
22+
# `conclusion == success` also covers runs where the capture job was
23+
# skipped by its `if` gate (no matching label, etc.) — in that case
24+
# no artifact was uploaded, so the download step below no-ops.
25+
if: github.event.workflow_run.conclusion == 'success'
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Download event payload
29+
id: download
30+
continue-on-error: true
31+
uses: actions/download-artifact@v4
32+
with:
33+
name: gardener-event
34+
run-id: ${{ github.event.workflow_run.id }}
35+
github-token: ${{ secrets.GITHUB_TOKEN }}
36+
37+
- name: Add devtools-gardener label
38+
if: steps.download.outcome == 'success'
39+
env:
40+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41+
GH_REPO: ${{ github.repository }}
42+
run: |
43+
ACTION=$(jq -r '.action' event.json)
44+
# On `labeled` events the label is already there — skip.
45+
if [ "$ACTION" != "opened" ]; then
46+
exit 0
47+
fi
48+
NUMBER=$(jq -r '(.issue // .pull_request).number' event.json)
49+
if jq -e 'has("pull_request")' event.json > /dev/null; then
50+
gh pr edit "$NUMBER" --add-label devtools-gardener
51+
else
52+
gh issue edit "$NUMBER" --add-label devtools-gardener
53+
fi
54+
55+
- name: Post to Slack
56+
if: steps.download.outcome == 'success'
57+
continue-on-error: true
58+
env:
59+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_GARDENER_BOT_TOKEN }}
60+
SLACK_CHANNEL_ID: ${{ vars.GARDENER_SLACK_CHANNEL_ID }}
61+
run: |
62+
KIND=$(jq -r 'if has("pull_request") then "PR" else "Issue" end' event.json)
63+
# Pull the body out, truncate, then convert GitHub Markdown to
64+
# Slack mrkdwn. Links and fenced code blocks are stashed before
65+
# the HTML-escape pass so their contents survive verbatim (a `&`
66+
# inside a URL must stay raw, and code content shouldn't be
67+
# mangled). Blockquote `> ` markers are also stashed so the
68+
# `>` → `&gt;` escape doesn't break them. Everything else is
69+
# HTML-escaped so user-supplied `<`, `>`, `&` can't collide
70+
# with Slack link syntax or injected mentions like <!channel>.
71+
BODY=$(jq -r '(.issue // .pull_request).body // ""' event.json)
72+
if [ ${#BODY} -gt 1000 ]; then
73+
BODY="${BODY:0:1000}…"
74+
fi
75+
BODY=$(printf '%s' "$BODY" | perl -0777 -pe '
76+
my @u;
77+
s{\[([^\]]+)\]\(([^)]+)\)}{push @u, $2; "\x01$#u\x02$1\x03"}ge;
78+
my @c;
79+
s{^```[^\n]*\n(.*?)\n```$}{push @c, $1; "\x04$#c\x05"}gems;
80+
s/^> /\x06/gm;
81+
s/^#{1,6}\s+(.+)$/*$1*/gm;
82+
s/\*\*(.+?)\*\*/*$1*/g;
83+
s/^(\s*)- \[x\]\s+/$1✓ /gm;
84+
s/^(\s*)[-*]\s+/$1• /gm;
85+
s/&/&amp;/g;
86+
s/</&lt;/g;
87+
s/>/&gt;/g;
88+
s/\x06/> /g;
89+
s{\x01(\d+)\x02(.*?)\x03}{"<$u[$1]|$2>"}ge;
90+
s{\x04(\d+)\x05}{"```\n$c[$1]\n```"}ge;
91+
')
92+
jq \
93+
--arg channel "$SLACK_CHANNEL_ID" \
94+
--arg kind "$KIND" \
95+
--arg body "$BODY" \
96+
'
97+
def escape: gsub("&";"&amp;") | gsub("<";"&lt;") | gsub(">";"&gt;");
98+
99+
(.issue // .pull_request) as $i
100+
| ([$i.labels[]?.name | select(. != "devtools-gardener")]
101+
| map("`\(.)`") | join(" ")) as $labels
102+
| (if $kind == "PR"
103+
then " · \($i.changed_files) files, +\($i.additions)/-\($i.deletions)"
104+
+ (if $i.draft then " · draft" else "" end)
105+
else "" end) as $meta
106+
| [ "*<\($i.html_url)|\($kind) #\($i.number)>* — \(($i.title | escape))",
107+
"_opened by \($i.user.login)\($meta)_" ]
108+
+ (if $body != "" then [$body] else [] end)
109+
+ (if $labels != "" then [$labels] else [] end)
110+
| join("\n") as $msg
111+
| { channel: $channel, text: "\($kind) #\($i.number): \($i.title)",
112+
blocks: [{ type: "section", text: { type: "mrkdwn", text: $msg } }] }
113+
' event.json | curl -sf -X POST \
114+
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
115+
-H 'Content-type: application/json; charset=utf-8' \
116+
-d @- https://slack.com/api/chat.postMessage

0 commit comments

Comments
 (0)