Skip to content

Commit b7f283d

Browse files
feat(workflows): reusable PR → Slack summary via OpenRouter
Adds the canonical org-wide workflow that watching repos call to post AI-summarized Slack notifications for opened/merged PRs. - Triggers on opened/ready_for_review/closed PR events on the caller's side; reusable workflow filters out drafts and fork PRs (latter because secrets aren't available to fork-triggered workflows). - Calls OpenRouter (default google/gemini-3.1-flash-lite) in JSON mode with a strict schema (summary, breaking_changes[], risk). Defensive parsing in case the model wraps output in markdown. - Posts Slack Block Kit to an incoming webhook: header with repo, clickable title + author, AI summary, breaking-changes section (only if non-empty), "Review PR →" button (only when not yet merged). Requires org secrets SLACK_WEBHOOK_URL and OPENROUTER_API_KEY scoped to each consuming repo. README documents the per-repo caller pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 753f83f commit b7f283d

2 files changed

Lines changed: 301 additions & 2 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
name: PR → Slack summary
2+
3+
# Reusable workflow: posts an AI-summarized Slack message for every PR
4+
# opened, marked ready-for-review, or merged. Skips drafts and fork PRs
5+
# (the latter because GitHub denies secrets to fork-triggered workflows).
6+
#
7+
# Called from each repo via:
8+
#
9+
# on:
10+
# pull_request:
11+
# types: [opened, ready_for_review, closed]
12+
# branches: [main] # add other long-lived branches as needed
13+
#
14+
# jobs:
15+
# notify:
16+
# uses: backendforth/.github/.github/workflows/pr-slack-summary.yml@main
17+
# secrets: inherit
18+
#
19+
# Required org secrets (Settings → Secrets and variables → Actions):
20+
# - SLACK_WEBHOOK_URL Incoming Webhook pointed at the target channel
21+
# - OPENROUTER_API_KEY from openrouter.ai/keys (set a credit cap there)
22+
23+
on:
24+
workflow_call:
25+
secrets:
26+
SLACK_WEBHOOK_URL:
27+
required: true
28+
OPENROUTER_API_KEY:
29+
required: true
30+
inputs:
31+
model:
32+
type: string
33+
default: "google/gemini-3.1-flash-lite"
34+
description: "OpenRouter model slug"
35+
36+
permissions:
37+
contents: read
38+
pull-requests: read
39+
40+
jobs:
41+
notify:
42+
if: |
43+
github.event.pull_request.draft == false &&
44+
github.event.pull_request.head.repo.full_name == github.repository &&
45+
(github.event.action != 'closed' || github.event.pull_request.merged == true)
46+
runs-on: ubuntu-latest
47+
steps:
48+
- name: Collect PR context
49+
id: ctx
50+
env:
51+
GH_TOKEN: ${{ github.token }}
52+
GH_REPO: ${{ github.repository }}
53+
PR_NUMBER: ${{ github.event.pull_request.number }}
54+
run: |
55+
set -euo pipefail
56+
gh pr view "$PR_NUMBER" --repo "$GH_REPO" \
57+
--json title,body,labels,files,author,baseRefName,headRefName \
58+
> pr.json
59+
60+
jq -n \
61+
--slurpfile pr pr.json \
62+
--arg repo "$GH_REPO" \
63+
--arg number "$PR_NUMBER" \
64+
'{
65+
repo: $repo,
66+
number: ($number | tonumber),
67+
title: $pr[0].title,
68+
body: (($pr[0].body // "") | .[0:8000]),
69+
labels: [$pr[0].labels[].name],
70+
files: [$pr[0].files[] | {path, additions, deletions}],
71+
author: $pr[0].author.login,
72+
base: $pr[0].baseRefName,
73+
head: $pr[0].headRefName
74+
}' > context.json
75+
76+
if [[ "${{ github.event.action }}" == "closed" ]]; then
77+
{
78+
echo "event_type=merged"
79+
echo "header_emoji=✅"
80+
echo "header_text=Merged"
81+
} >> "$GITHUB_OUTPUT"
82+
else
83+
{
84+
echo "event_type=opened"
85+
echo "header_emoji=📥"
86+
echo "header_text=New PR"
87+
} >> "$GITHUB_OUTPUT"
88+
fi
89+
90+
- name: Generate AI summary via OpenRouter
91+
id: ai
92+
env:
93+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
94+
MODEL: ${{ inputs.model }}
95+
run: |
96+
set -euo pipefail
97+
98+
SYSTEM='You analyze a GitHub pull request and emit STRICT JSON with three fields:
99+
- summary: 2-3 sentences describing intent and main changes. Plain prose, no markdown.
100+
- breaking_changes: array of bullet strings; [] if none.
101+
- risk: "low" | "medium" | "high".
102+
103+
For Dependabot PRs, scan the body for major version bumps (X.x → Y.0) and list each as a breaking change.
104+
For feature PRs, scan title and body for BREAKING CHANGE, feat!, fix!, and explicit breaking notes.
105+
106+
Be concise and concrete; no fluff. Output ONLY the JSON object, no markdown, no commentary.'
107+
108+
PAYLOAD=$(jq -n \
109+
--arg system "$SYSTEM" \
110+
--slurpfile ctx context.json \
111+
--arg model "$MODEL" \
112+
'{
113+
model: $model,
114+
response_format: { type: "json_object" },
115+
messages: [
116+
{ role: "system", content: $system },
117+
{ role: "user", content: ($ctx[0] | tostring) }
118+
]
119+
}')
120+
121+
# Retry up to 3 times on 5xx
122+
for attempt in 1 2 3; do
123+
HTTP_CODE=$(curl -sS -o response.json -w '%{http_code}' \
124+
https://openrouter.ai/api/v1/chat/completions \
125+
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
126+
-H "Content-Type: application/json" \
127+
-H "HTTP-Referer: https://github.com/backendforth" \
128+
-H "X-Title: PR Slack Summary" \
129+
-d "$PAYLOAD")
130+
131+
if [[ "$HTTP_CODE" == "200" ]]; then
132+
break
133+
fi
134+
135+
echo "OpenRouter returned HTTP $HTTP_CODE (attempt $attempt)"
136+
cat response.json || true
137+
138+
if [[ $attempt -lt 3 && "$HTTP_CODE" -ge 500 ]]; then
139+
sleep 5
140+
continue
141+
fi
142+
143+
echo "Aborting after attempt $attempt"
144+
exit 1
145+
done
146+
147+
CONTENT=$(jq -r '.choices[0].message.content' response.json)
148+
149+
# Strip accidental markdown code fences (just in case)
150+
CONTENT=$(printf '%s' "$CONTENT" | sed -E 's/^```(json)?//; s/```$//')
151+
152+
if printf '%s' "$CONTENT" | jq empty 2>/dev/null; then
153+
printf '%s' "$CONTENT" > ai.json
154+
else
155+
echo "AI response was not valid JSON; using fallback"
156+
jq -n --arg s "$CONTENT" \
157+
'{summary: $s, breaking_changes: [], risk: "low"}' > ai.json
158+
fi
159+
160+
# Defensive defaults if fields are missing
161+
jq '{
162+
summary: (.summary // "No summary available."),
163+
breaking_changes: (.breaking_changes // []),
164+
risk: (.risk // "low")
165+
}' ai.json > ai.final.json
166+
mv ai.final.json ai.json
167+
168+
- name: Build Slack Block Kit payload
169+
env:
170+
PR_TITLE: ${{ github.event.pull_request.title }}
171+
PR_URL: ${{ github.event.pull_request.html_url }}
172+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
173+
REPO_NAME: ${{ github.event.repository.name }}
174+
IS_MERGED: ${{ github.event.pull_request.merged }}
175+
HEADER_EMOJI: ${{ steps.ctx.outputs.header_emoji }}
176+
HEADER_TEXT: ${{ steps.ctx.outputs.header_text }}
177+
run: |
178+
set -euo pipefail
179+
180+
jq -n \
181+
--arg header "$HEADER_EMOJI $HEADER_TEXT · $REPO_NAME" \
182+
--arg title "$PR_TITLE" \
183+
--arg url "$PR_URL" \
184+
--arg author "$PR_AUTHOR" \
185+
--arg is_merged "$IS_MERGED" \
186+
--slurpfile ai ai.json \
187+
'
188+
($ai[0]) as $ai |
189+
($is_merged == "true") as $merged |
190+
{ blocks:
191+
([
192+
{ type: "header", text: { type: "plain_text", text: $header, emoji: true } },
193+
{ type: "section", text: { type: "mrkdwn",
194+
text: ("*<" + $url + "|" + $title + ">*\nby `" + $author + "`") } },
195+
{ type: "section", text: { type: "mrkdwn",
196+
text: ("*Zusammenfassung*\n" + $ai.summary) } }
197+
]
198+
+ (if ($ai.breaking_changes | length) > 0 then
199+
[{ type: "section", text: { type: "mrkdwn",
200+
text: ("⚠️ *Breaking changes*\n" +
201+
([$ai.breaking_changes[] | "• " + .] | join("\n"))) } }]
202+
else [] end)
203+
+ (if $merged then [] else
204+
[{ type: "actions", elements: [
205+
{ type: "button",
206+
text: { type: "plain_text", text: "Review PR →" },
207+
url: $url, style: "primary" }
208+
]}]
209+
end))
210+
}' > slack.json
211+
212+
cat slack.json
213+
214+
- name: Post to Slack
215+
uses: slackapi/slack-github-action@v2
216+
with:
217+
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
218+
webhook-type: incoming-webhook
219+
payload-file-path: ./slack.json

README.md

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,82 @@
1-
# .github
2-
Org-wide reusable workflows for backendforth repositories
1+
# backendforth/.github
2+
3+
Org-wide GitHub Actions reusable workflows. Repos under `backendforth/*` opt in
4+
by adding a thin caller workflow that points at the canonical workflow files
5+
here.
6+
7+
## Reusable workflows
8+
9+
### `pr-slack-summary.yml` — PR → Slack with AI summary
10+
11+
Posts an AI-summarized Slack message to a single channel for every PR opened,
12+
marked ready-for-review, or merged on a watched branch. Drafts and fork PRs are
13+
skipped. Powered by an OpenRouter call (default model
14+
`google/gemini-3.1-flash-lite`) feeding a Slack Block Kit message via incoming
15+
webhook.
16+
17+
#### How to enroll a repo
18+
19+
1. Make sure the two org secrets are accessible to this repo (see below).
20+
2. Drop this caller workflow into the repo at
21+
`.github/workflows/pr-slack-notify.yml`:
22+
23+
```yaml
24+
name: PR Slack notify
25+
26+
on:
27+
pull_request:
28+
types: [opened, ready_for_review, closed]
29+
branches:
30+
- main
31+
# add other long-lived branches the repo ships, e.g.:
32+
# - variant/document-level
33+
34+
jobs:
35+
notify:
36+
uses: backendforth/.github/.github/workflows/pr-slack-summary.yml@main
37+
secrets: inherit
38+
```
39+
40+
3. Open a test PR — within ~30 s a `📥 New PR · <repo>` message should appear
41+
in `#github-logs`. Merge the PR → a second `✅ Merged · <repo>` message
42+
(without the Review button) follows.
43+
44+
#### Org secrets it expects
45+
46+
Set both at the org level (Settings → Secrets and variables → Actions →
47+
Organization secrets) with repository access limited to the repos that opt in.
48+
49+
| Secret | Where it comes from | Notes |
50+
|---|---|---|
51+
| `SLACK_WEBHOOK_URL` | Slack → Apps → Incoming Webhooks → pointed at `#github-logs` | Treat like a secret — anyone with this URL can post into the channel. |
52+
| `OPENROUTER_API_KEY` | <https://openrouter.ai/keys> | Create a dedicated key named `github-actions-pr-summary` with a small credit cap (e.g. $5) — the workflow uses well under $0.10 / month across four repos. |
53+
54+
#### Optional input
55+
56+
The default model is `google/gemini-3.1-flash-lite`. Override per-repo if
57+
needed:
58+
59+
```yaml
60+
jobs:
61+
notify:
62+
uses: backendforth/.github/.github/workflows/pr-slack-summary.yml@main
63+
secrets: inherit
64+
with:
65+
model: anthropic/claude-haiku-4.5
66+
```
67+
68+
#### What the message contains
69+
70+
1. Header — repo name, opened or merged
71+
2. PR title (clickable) + author
72+
3. AI-generated 2–3 sentence summary
73+
4. Breaking-changes section (only shown when the model flags at least one)
74+
5. "Review PR →" button (only on non-merged PRs)
75+
76+
The model is prompted to flag major version bumps in Dependabot grouped PRs
77+
and `BREAKING CHANGE` / `feat!` / `fix!` markers in feature PRs.
78+
79+
## License
80+
81+
Internal infrastructure — no license file. Workflows here are intended only
82+
for `backendforth/*` repositories.

0 commit comments

Comments
 (0)