-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
206 lines (193 loc) · 8.36 KB
/
dependabot-weekly-summary.yml
File metadata and controls
206 lines (193 loc) · 8.36 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
name: Dependabot Weekly Summary
on:
schedule:
- cron: "0 8 * * 1" # Mon 08:00 UTC
workflow_dispatch:
# Single-purpose monitoring workflow; serialise on workflow name only - we never
# want two concurrent summary runs racing to post the same digest.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
permissions:
contents: read # gh CLI baseline
pull-requests: read # gh pr list (open dependabot PRs)
actions: read # gh run list / view (parse latest dependabot run logs)
jobs:
summary:
name: Post weekly Dependabot summary
runs-on: ubuntu-latest
environment: dependabot-summary
env:
# Severities surface in the actions list when their remaining TTR drops
# below this many days. Override via repo/env var ACTION_THRESHOLD_DAYS.
THRESHOLD_DAYS: ${{ vars.ACTION_THRESHOLD_DAYS || '7' }}
steps:
- name: Fetch alerts and compute summaries
id: alerts
env:
GH_TOKEN: ${{ secrets.DEPENDABOT_ALERTS_TOKEN }}
REPO: ${{ github.repository }}
run: |
if ! gh api -X GET "/repos/$REPO/dependabot/alerts" --paginate > pages.json 2> err.txt; then
echo "total=?" >> "$GITHUB_OUTPUT"
ERR=$(head -c 200 err.txt | tr '\n' ' ')
echo "by_severity=:x: _failed to fetch alerts: ${ERR}_" >> "$GITHUB_OUTPUT"
echo "actions=:x: _alerts unavailable_" >> "$GITHUB_OUTPUT"
exit 0
fi
jq -s '[.[][] | select(.state == "open")]' pages.json > open.json
TOTAL=$(jq 'length' open.json)
echo "total=$TOTAL" >> "$GITHUB_OUTPUT"
if [ "$TOTAL" = "0" ]; then
echo "by_severity=:white_check_mark: No open alerts." >> "$GITHUB_OUTPUT"
echo "actions=_None_" >> "$GITHUB_OUTPUT"
exit 0
fi
# Severity breakdown - real newlines so jq --arg in the payload
# builder encodes them as proper \n in JSON (Slack renders as breaks).
BY_SEV=$(jq -r '
group_by(.security_advisory.severity)
| map({sev: .[0].security_advisory.severity,
count: length,
weight: ({"critical":0,"high":1,"medium":2,"low":3}[.[0].security_advisory.severity])})
| sort_by(.weight)
| map("• *\(.count)* \(.sev)")
| join("\n")
' open.json)
{
echo "by_severity<<EOF"
echo "$BY_SEV"
echo "EOF"
} >> "$GITHUB_OUTPUT"
# Actions: alerts within THRESHOLD_DAYS of their TTR (P0=7d, P1=30d, P2=90d, P3=no deadline)
# Grouped by (package, severity); shows earliest deadline per group.
ACTIONS=$(jq -r --argjson threshold "$THRESHOLD_DAYS" '
[.[]
| (.security_advisory.severity) as $sev
| ({"critical":7,"high":30,"medium":90,"low":null}[$sev]) as $ttr
| select($ttr != null)
| ((now - (.created_at | fromdateiso8601)) / 86400 | floor) as $age
| {pkg: .dependency.package.name, sev: $sev, remaining: ($ttr - $age)}
]
| group_by([.pkg, .sev])
| map({pkg: .[0].pkg, sev: .[0].sev, count: length, min_remaining: ([.[].remaining] | min)})
| map(select(.min_remaining < $threshold))
| sort_by(.min_remaining)
| if length == 0 then "_None_"
else (map(
"• *\(.pkg)* (\(.sev))" +
(if .count > 1 then " ×\(.count)" else "" end) + " - " +
(if .min_remaining < 0 then "*OVERDUE* by \(-.min_remaining)d"
else "\(.min_remaining)d remaining" end)
) | join("\n"))
end
' open.json)
{
echo "actions<<EOF"
echo "$ACTIONS"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Fetch open dependabot PRs
id: prs
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
REPO_URL: https://github.com/${{ github.repository }}
run: |
if ! PR_JSON=$(gh pr list --repo "$REPO" --state open --author "app/dependabot" --json number,title 2> err.txt); then
ERR=$(head -c 200 err.txt | tr '\n' ' ')
echo "list=:x: _failed to fetch PRs: ${ERR}_" >> "$GITHUB_OUTPUT"
exit 0
fi
LIST=$(echo "$PR_JSON" | jq -r --arg url "$REPO_URL" '
if length == 0 then "_None_"
else (map("• <\($url)/pull/\(.number)|#\(.number)> \(.title)") | join("\n"))
end
')
{
echo "list<<EOF"
echo "$LIST"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Find latest npm dependabot run
id: latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
run: |
# Repos without a dependabot.yml have no "Dependabot Updates" workflow;
# treat the lookup failure as "no recent run found" rather than failing.
if ! RUN_ID=$(gh run list --repo "$REPO" --workflow "Dependabot Updates" --status success --limit 30 --json databaseId,name --jq 'first(.[] | select(.name | startswith("npm_and_yarn")) | .databaseId) // empty' 2>/dev/null); then
RUN_ID=""
fi
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
- name: Extract stuck deps (only if actions pending)
id: stuck
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
RUN_ID: ${{ steps.latest.outputs.run_id }}
ACTIONS: ${{ steps.alerts.outputs.actions }}
run: |
# Skip the stuck section entirely when nothing in the actions list
# - keeps the digest tidy when there's nothing to actually act on.
if [ "$ACTIONS" = "_None_" ]; then
echo "section=" >> "$GITHUB_OUTPUT"
exit 0
fi
HEADER=$'\n\n*Couldn\'t auto-fix (need manual `pnpm.overrides`):*\n'
if [ -z "$RUN_ID" ]; then
{
echo "section<<EOF"
echo "${HEADER}_(no recent npm run found)_"
echo "EOF"
} >> "$GITHUB_OUTPUT"
exit 0
fi
gh run view "$RUN_ID" --repo "$REPO" --log > log.txt 2>&1 || true
STUCK=$(grep -oE "No update possible for [^[:space:]]+ [0-9][^[:space:]]*" log.txt | sed 's/No update possible for //' | sort -u || true)
if [ -z "$STUCK" ]; then
{
echo "section<<EOF"
echo "${HEADER}_None_"
echo "EOF"
} >> "$GITHUB_OUTPUT"
exit 0
fi
LIST=$(echo "$STUCK" | awk 'NR>1{printf "\n"} {printf "• *%s* %s", $1, $2}')
{
echo "section<<EOF"
echo "${HEADER}${LIST}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Build Slack payload
env:
REPO: ${{ github.repository }}
CHANNEL: ${{ vars.SLACK_CHANNEL_ID }}
TOTAL: ${{ steps.alerts.outputs.total }}
BY_SEVERITY: ${{ steps.alerts.outputs.by_severity }}
PRS_LIST: ${{ steps.prs.outputs.list }}
ACTIONS: ${{ steps.alerts.outputs.actions }}
STUCK: ${{ steps.stuck.outputs.section }}
run: |
# Build payload via jq so PR titles or error strings containing
# quotes/backslashes/newlines can't break the JSON.
jq -n \
--arg channel "$CHANNEL" \
--arg repo "$REPO" \
--arg total "$TOTAL" \
--arg by_severity "$BY_SEVERITY" \
--arg prs_list "$PRS_LIST" \
--arg actions "$ACTIONS" \
--arg stuck "$STUCK" \
--arg threshold "$THRESHOLD_DAYS" \
'{
channel: $channel,
text: ":calendar: *Weekly Dependabot summary* - `\($repo)`\n\n*Open alerts (\($total)):*\n\($by_severity)\n\n*Open Dependabot PRs:*\n\($prs_list)\n\n*Actions needed (<\($threshold)d remaining):*\n\($actions)\($stuck)\n\n<https://github.com/\($repo)/security/dependabot|Dependabot alerts>"
}' > payload.json
- name: Post Slack summary
uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload-file-path: payload.json