Skip to content

Commit ce74771

Browse files
committed
ci: add dependabot weekly summary workflow
1 parent 759214e commit ce74771

1 file changed

Lines changed: 151 additions & 0 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
name: Dependabot Weekly Summary
2+
3+
on:
4+
schedule:
5+
- cron: "0 8 * * 1" # Mon 08:00 UTC
6+
workflow_dispatch:
7+
8+
# Single-purpose monitoring workflow; serialise on workflow name only - we never
9+
# want two concurrent summary runs racing to post the same digest.
10+
concurrency:
11+
group: ${{ github.workflow }}
12+
cancel-in-progress: false
13+
14+
permissions:
15+
contents: read # gh CLI baseline
16+
pull-requests: read # gh pr list (open dependabot PRs)
17+
actions: read # gh run list / view (parse latest dependabot run logs)
18+
19+
jobs:
20+
summary:
21+
name: Post weekly Dependabot summary
22+
runs-on: ubuntu-latest
23+
environment: dependabot-summary
24+
steps:
25+
- name: Fetch alerts and compute summaries
26+
id: alerts
27+
env:
28+
GH_TOKEN: ${{ secrets.DEPENDABOT_ALERTS_TOKEN }}
29+
REPO: ${{ github.repository }}
30+
run: |
31+
if ! gh api -X GET "/repos/$REPO/dependabot/alerts" --paginate > pages.json 2> err.txt; then
32+
echo "total=?" >> "$GITHUB_OUTPUT"
33+
ERR=$(head -c 200 err.txt | tr '\n' ' ')
34+
echo "by_severity=:x: _failed to fetch alerts: ${ERR}_" >> "$GITHUB_OUTPUT"
35+
echo "actions=:x: _alerts unavailable_" >> "$GITHUB_OUTPUT"
36+
exit 0
37+
fi
38+
jq -s '[.[][] | select(.state == "open")]' pages.json > open.json
39+
40+
TOTAL=$(jq 'length' open.json)
41+
echo "total=$TOTAL" >> "$GITHUB_OUTPUT"
42+
43+
if [ "$TOTAL" = "0" ]; then
44+
echo "by_severity=:white_check_mark: No open alerts." >> "$GITHUB_OUTPUT"
45+
echo "actions=_None_" >> "$GITHUB_OUTPUT"
46+
exit 0
47+
fi
48+
49+
# Severity breakdown - single-line output with \n escapes for JSON safety
50+
BY_SEV=$(jq -r '
51+
group_by(.security_advisory.severity)
52+
| map({sev: .[0].security_advisory.severity,
53+
count: length,
54+
weight: ({"critical":0,"high":1,"medium":2,"low":3}[.[0].security_advisory.severity])})
55+
| sort_by(.weight)
56+
| map("• *\(.count)* \(.sev)")
57+
| join("\\n")
58+
' open.json)
59+
echo "by_severity=$BY_SEV" >> "$GITHUB_OUTPUT"
60+
61+
# Actions: alerts with <7d to TTR (P0=7d, P1=30d, P2=90d, P3=no deadline)
62+
# Grouped by (package, severity); shows earliest deadline per group.
63+
ACTIONS=$(jq -r '
64+
[.[]
65+
| (.security_advisory.severity) as $sev
66+
| ({"critical":7,"high":30,"medium":90,"low":null}[$sev]) as $ttr
67+
| select($ttr != null)
68+
| ((now - (.created_at | fromdateiso8601)) / 86400 | floor) as $age
69+
| {pkg: .dependency.package.name, sev: $sev, remaining: ($ttr - $age)}
70+
]
71+
| group_by([.pkg, .sev])
72+
| map({pkg: .[0].pkg, sev: .[0].sev, count: length, min_remaining: ([.[].remaining] | min)})
73+
| map(select(.min_remaining < 7))
74+
| sort_by(.min_remaining)
75+
| if length == 0 then "_None_"
76+
else (map(
77+
"• *\(.pkg)* (\(.sev))" +
78+
(if .count > 1 then " ×\(.count)" else "" end) + " - " +
79+
(if .min_remaining < 0 then "*OVERDUE* by \(-.min_remaining)d"
80+
else "\(.min_remaining)d remaining" end)
81+
) | join("\\n"))
82+
end
83+
' open.json)
84+
echo "actions=$ACTIONS" >> "$GITHUB_OUTPUT"
85+
86+
- name: Fetch open dependabot PRs
87+
id: prs
88+
env:
89+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
90+
REPO: ${{ github.repository }}
91+
REPO_URL: https://github.com/${{ github.repository }}
92+
run: |
93+
if ! PR_JSON=$(gh pr list --repo "$REPO" --state open --author "app/dependabot" --json number,title 2> err.txt); then
94+
ERR=$(head -c 200 err.txt | tr '\n' ' ')
95+
echo "list=:x: _failed to fetch PRs: ${ERR}_" >> "$GITHUB_OUTPUT"
96+
exit 0
97+
fi
98+
LIST=$(echo "$PR_JSON" | jq -r --arg url "$REPO_URL" '
99+
if length == 0 then "_None_"
100+
else (map("• <\($url)/pull/\(.number)|#\(.number)> \(.title)") | join("\\n"))
101+
end
102+
')
103+
echo "list=$LIST" >> "$GITHUB_OUTPUT"
104+
105+
- name: Find latest npm dependabot run
106+
id: latest
107+
env:
108+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
109+
REPO: ${{ github.repository }}
110+
run: |
111+
RUN_ID=$(gh run list --repo "$REPO" --workflow "Dependabot Updates" --status success --limit 30 --json databaseId,name --jq '[.[] | select(.name | startswith("npm_and_yarn"))][0].databaseId')
112+
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
113+
114+
- name: Extract stuck deps (only if actions pending)
115+
id: stuck
116+
env:
117+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118+
REPO: ${{ github.repository }}
119+
RUN_ID: ${{ steps.latest.outputs.run_id }}
120+
ACTIONS: ${{ steps.alerts.outputs.actions }}
121+
run: |
122+
# Skip the stuck section entirely when nothing in the actions list
123+
# - keeps the digest tidy when there's nothing to actually act on.
124+
if [ "$ACTIONS" = "_None_" ]; then
125+
echo "section=" >> "$GITHUB_OUTPUT"
126+
exit 0
127+
fi
128+
HEADER="\\n\\n*Couldn't auto-fix (need manual \`pnpm.overrides\`):*\\n"
129+
if [ -z "$RUN_ID" ]; then
130+
echo "section=${HEADER}_(no recent npm run found)_" >> "$GITHUB_OUTPUT"
131+
exit 0
132+
fi
133+
gh run view "$RUN_ID" --repo "$REPO" --log > log.txt 2>&1 || true
134+
STUCK=$(grep -oE "No update possible for [^[:space:]]+ [0-9][^[:space:]]*" log.txt | sed 's/No update possible for //' | sort -u || true)
135+
if [ -z "$STUCK" ]; then
136+
echo "section=${HEADER}_None_" >> "$GITHUB_OUTPUT"
137+
exit 0
138+
fi
139+
LIST=$(echo "$STUCK" | awk 'NR>1{printf "\\n"} {printf "• *%s* %s", $1, $2}')
140+
echo "section=${HEADER}${LIST}" >> "$GITHUB_OUTPUT"
141+
142+
- name: Post Slack summary
143+
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
144+
with:
145+
method: chat.postMessage
146+
token: ${{ secrets.SLACK_BOT_TOKEN }}
147+
payload: |
148+
{
149+
"channel": "${{ vars.SLACK_CHANNEL_ID }}",
150+
"text": ":calendar: *Weekly Dependabot summary* - `${{ github.repository }}`\n\n*Open alerts (${{ steps.alerts.outputs.total }}):*\n${{ steps.alerts.outputs.by_severity }}\n\n*Open Dependabot PRs:*\n${{ steps.prs.outputs.list }}\n\n*Actions needed (<7d remaining):*\n${{ steps.alerts.outputs.actions }}${{ steps.stuck.outputs.section }}\n\n<https://github.com/${{ github.repository }}/security/dependabot|Dependabot alerts>"
151+
}

0 commit comments

Comments
 (0)