diff --git a/.github/workflows/gardener-notify-event.yml b/.github/workflows/gardener-notify-event.yml new file mode 100644 index 0000000..f03b719 --- /dev/null +++ b/.github/workflows/gardener-notify-event.yml @@ -0,0 +1,35 @@ +name: Gardener - Notify Event +# Tiny event capturer: stashes the triggering issue/PR payload as an artifact +# for `gardener-notify-slack.yml` to pick up via workflow_run. +# +# Why two workflows? When Dependabot triggers a workflow, GitHub forces +# GITHUB_TOKEN to read-only and hides Actions secrets — so labeling and +# Slack posting from this workflow would fail on every Dependabot PR. A +# workflow_run-triggered follow-up runs in the default-branch context with +# full permissions and secret access, regardless of the upstream actor. +# +# Uses pull_request_target so fork-opened PRs still produce an artifact. +# No code is checked out here; this workflow only reads the pre-parsed +# event payload, so there is no pwn-request surface. +on: + issues: + types: [opened, labeled] + pull_request_target: + types: [opened, labeled] + +permissions: + contents: read + +jobs: + capture: + if: github.event.action == 'opened' || github.event.label.name == 'devtools-gardener' + runs-on: ubuntu-latest + steps: + - name: Stash event payload + run: cp "$GITHUB_EVENT_PATH" event.json + + - uses: actions/upload-artifact@v4 + with: + name: gardener-event + path: event.json + retention-days: 1 diff --git a/.github/workflows/gardener-notify-slack.yml b/.github/workflows/gardener-notify-slack.yml new file mode 100644 index 0000000..8bcb5a8 --- /dev/null +++ b/.github/workflows/gardener-notify-slack.yml @@ -0,0 +1,116 @@ +name: Gardener - Notify Slack +# Runs after `Gardener - Notify Event` completes and does the real work: +# applies the devtools-gardener label and posts a summary to Slack. +# +# The workflow_run trigger runs this job in the default-branch context with +# full GITHUB_TOKEN permissions and Actions secret access — this is what +# lets it succeed for Dependabot-opened PRs, where the upstream event +# workflow can't label or reach secrets directly. +on: + workflow_run: + workflows: ['Gardener - Notify Event'] + types: [completed] + +permissions: + contents: read + issues: write + pull-requests: write + actions: read + +jobs: + notify: + # `conclusion == success` also covers runs where the capture job was + # skipped by its `if` gate (no matching label, etc.) — in that case + # no artifact was uploaded, so the download step below no-ops. + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - name: Download event payload + id: download + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: gardener-event + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Add devtools-gardener label + if: steps.download.outcome == 'success' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + ACTION=$(jq -r '.action' event.json) + # On `labeled` events the label is already there — skip. + if [ "$ACTION" != "opened" ]; then + exit 0 + fi + NUMBER=$(jq -r '(.issue // .pull_request).number' event.json) + if jq -e 'has("pull_request")' event.json > /dev/null; then + gh pr edit "$NUMBER" --add-label devtools-gardener + else + gh issue edit "$NUMBER" --add-label devtools-gardener + fi + + - name: Post to Slack + if: steps.download.outcome == 'success' + continue-on-error: true + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_GARDENER_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ vars.GARDENER_SLACK_CHANNEL_ID }} + run: | + KIND=$(jq -r 'if has("pull_request") then "PR" else "Issue" end' event.json) + # Pull the body out, truncate, then convert GitHub Markdown to + # Slack mrkdwn. Links and fenced code blocks are stashed before + # the HTML-escape pass so their contents survive verbatim (a `&` + # inside a URL must stay raw, and code content shouldn't be + # mangled). Blockquote `> ` markers are also stashed so the + # `>` → `>` escape doesn't break them. Everything else is + # HTML-escaped so user-supplied `<`, `>`, `&` can't collide + # with Slack link syntax or injected mentions like . + BODY=$(jq -r '(.issue // .pull_request).body // ""' event.json) + if [ ${#BODY} -gt 1000 ]; then + BODY="${BODY:0:1000}…" + fi + BODY=$(printf '%s' "$BODY" | perl -0777 -pe ' + my @u; + s{\[([^\]]+)\]\(([^)]+)\)}{push @u, $2; "\x01$#u\x02$1\x03"}ge; + my @c; + s{^```[^\n]*\n(.*?)\n```$}{push @c, $1; "\x04$#c\x05"}gems; + s/^> /\x06/gm; + s/^#{1,6}\s+(.+)$/*$1*/gm; + s/\*\*(.+?)\*\*/*$1*/g; + s/^(\s*)- \[x\]\s+/$1✓ /gm; + s/^(\s*)[-*]\s+/$1• /gm; + s/&/&/g; + s//>/g; + s/\x06/> /g; + s{\x01(\d+)\x02(.*?)\x03}{"<$u[$1]|$2>"}ge; + s{\x04(\d+)\x05}{"```\n$c[$1]\n```"}ge; + ') + jq \ + --arg channel "$SLACK_CHANNEL_ID" \ + --arg kind "$KIND" \ + --arg body "$BODY" \ + ' + def escape: gsub("&";"&") | gsub("<";"<") | gsub(">";">"); + + (.issue // .pull_request) as $i + | ([$i.labels[]?.name | select(. != "devtools-gardener")] + | map("`\(.)`") | join(" ")) as $labels + | (if $kind == "PR" + then " · \($i.changed_files) files, +\($i.additions)/-\($i.deletions)" + + (if $i.draft then " · draft" else "" end) + else "" end) as $meta + | [ "*<\($i.html_url)|\($kind) #\($i.number)>* — \(($i.title | escape))", + "_opened by \($i.user.login)\($meta)_" ] + + (if $body != "" then [$body] else [] end) + + (if $labels != "" then [$labels] else [] end) + | join("\n") as $msg + | { channel: $channel, text: "\($kind) #\($i.number): \($i.title)", + blocks: [{ type: "section", text: { type: "mrkdwn", text: $msg } }] } + ' event.json | curl -sf -X POST \ + -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ + -H 'Content-type: application/json; charset=utf-8' \ + -d @- https://slack.com/api/chat.postMessage