Skip to content

Commit b589508

Browse files
authored
Merge pull request #33 from cld2labs/dev
feat(security): refine Dependabot notifications
2 parents edbf826 + 83337a9 commit b589508

3 files changed

Lines changed: 214 additions & 82 deletions

File tree

.github/workflows/code-scans.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
pull_request:
88
types: [opened, synchronize, reopened, ready_for_review]
99
schedule:
10-
- cron: '0 6 * * 1'
10+
- cron: '0 12 * * 5'
1111

1212
concurrency:
1313
group: sdle-${{ github.event.pull_request.number || github.ref }}

.github/workflows/dependabot-auto-merge.yml

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,43 @@ jobs:
1818
with:
1919
github-token: "${{ secrets.GITHUB_TOKEN }}"
2020

21+
- name: Classify update type
22+
id: classify
23+
env:
24+
FM_UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
25+
PR_TITLE: ${{ github.event.pull_request.title }}
26+
run: |
27+
UPDATE_TYPE="${FM_UPDATE_TYPE}"
28+
29+
if [[ -z "$UPDATE_TYPE" || "$UPDATE_TYPE" == "null" ]]; then
30+
if [[ "$PR_TITLE" =~ ([0-9]+)\.([0-9]+)\.([0-9]+).*to.*([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
31+
OLD_MAJOR="${BASH_REMATCH[1]}"
32+
OLD_MINOR="${BASH_REMATCH[2]}"
33+
OLD_PATCH="${BASH_REMATCH[3]}"
34+
NEW_MAJOR="${BASH_REMATCH[4]}"
35+
NEW_MINOR="${BASH_REMATCH[5]}"
36+
NEW_PATCH="${BASH_REMATCH[6]}"
37+
38+
if [[ "$OLD_MAJOR" != "$NEW_MAJOR" ]]; then
39+
UPDATE_TYPE="version-update:semver-major"
40+
elif [[ "$OLD_MINOR" != "$NEW_MINOR" ]]; then
41+
UPDATE_TYPE="version-update:semver-minor"
42+
elif [[ "$OLD_PATCH" != "$NEW_PATCH" ]]; then
43+
UPDATE_TYPE="version-update:semver-patch"
44+
else
45+
UPDATE_TYPE="unknown"
46+
fi
47+
else
48+
UPDATE_TYPE="unknown"
49+
fi
50+
fi
51+
52+
echo "update-type=$UPDATE_TYPE" >> "$GITHUB_OUTPUT"
53+
2154
- name: Enable auto-merge for minor and patch updates
2255
if: |
23-
steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
24-
steps.metadata.outputs.update-type == 'version-update:semver-minor'
56+
steps.classify.outputs.update-type == 'version-update:semver-patch' ||
57+
steps.classify.outputs.update-type == 'version-update:semver-minor'
2558
run: gh pr merge --auto --squash "$PR_URL"
2659
env:
2760
PR_URL: ${{ github.event.pull_request.html_url }}

.github/workflows/dependabot-gchat-notify.yml

Lines changed: 178 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ name: Notify Google Chat on Dependabot Events
22

33
on:
44
pull_request_target:
5-
types: [opened, closed]
5+
types: [opened]
6+
pull_request:
7+
types: [closed]
8+
push:
9+
branches: [main]
610

711
permissions:
812
contents: read
@@ -12,113 +16,208 @@ jobs:
1216
notify:
1317
name: Post Dependabot event to Google Chat
1418
runs-on: ubuntu-latest
15-
if: github.actor == 'dependabot[bot]'
19+
# Trigger conditions:
20+
# - opened/closed events: only when the PR was authored by dependabot[bot]
21+
# - push events: only when the head commit looks like a Dependabot squash
22+
# (commit message starts with chore(deps- which is our configured prefix)
23+
if: |
24+
(github.event_name != 'push' && github.event.pull_request.user.login == 'dependabot[bot]') ||
25+
(github.event_name == 'push' && startsWith(github.event.head_commit.message, 'chore(deps-'))
1626
steps:
17-
- name: Fetch Dependabot metadata
18-
id: meta
27+
- name: Resolve PR for push event
28+
id: pr_lookup
29+
if: github.event_name == 'push'
30+
env:
31+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32+
COMMIT_SHA: ${{ github.event.head_commit.id }}
33+
run: |
34+
# Find the PR that produced this squash commit on main.
35+
PR_JSON=$(gh api "repos/${{ github.repository }}/commits/${COMMIT_SHA}/pulls" 2>/dev/null | jq '[.[] | select(.user.login == "dependabot[bot]")] | .[0] // empty')
36+
if [[ -z "$PR_JSON" || "$PR_JSON" == "null" ]]; then
37+
echo "No Dependabot PR associated with this commit; skipping."
38+
echo "skip=true" >> "$GITHUB_OUTPUT"
39+
exit 0
40+
fi
41+
echo "skip=false" >> "$GITHUB_OUTPUT"
42+
echo "pr_number=$(echo "$PR_JSON" | jq -r '.number')" >> "$GITHUB_OUTPUT"
43+
echo "pr_url=$(echo "$PR_JSON" | jq -r '.html_url')" >> "$GITHUB_OUTPUT"
44+
echo "pr_title=$(echo "$PR_JSON" | jq -r '.title')" >> "$GITHUB_OUTPUT"
45+
echo "pr_head_ref=$(echo "$PR_JSON" | jq -r '.head.ref')" >> "$GITHUB_OUTPUT"
46+
47+
- name: Stop early if push had no Dependabot PR
48+
if: github.event_name == 'push' && steps.pr_lookup.outputs.skip == 'true'
49+
run: exit 0
50+
51+
- name: Fetch Dependabot metadata (PR events)
52+
id: meta_pr
53+
if: github.event_name != 'push'
54+
uses: dependabot/fetch-metadata@v2
55+
with:
56+
github-token: "${{ secrets.GITHUB_TOKEN }}"
57+
58+
- name: Fetch Dependabot metadata (push events)
59+
id: meta_push
60+
if: github.event_name == 'push' && steps.pr_lookup.outputs.skip != 'true'
1961
uses: dependabot/fetch-metadata@v2
2062
with:
2163
github-token: "${{ secrets.GITHUB_TOKEN }}"
64+
pr-number: ${{ steps.pr_lookup.outputs.pr_number }}
2265

23-
- name: Classify event
66+
- name: Classify event and update type
2467
id: classify
68+
env:
69+
FM_UPDATE_TYPE: ${{ steps.meta_pr.outputs.update-type || steps.meta_push.outputs.update-type }}
70+
PR_TITLE: ${{ github.event.pull_request.title || steps.pr_lookup.outputs.pr_title }}
71+
ACTION: ${{ github.event.action }}
72+
MERGED: ${{ github.event.pull_request.merged }}
73+
GH_EVENT: ${{ github.event_name }}
2574
run: |
26-
EVENT="unknown"
27-
if [[ "${{ github.event.action }}" == "opened" ]]; then
28-
if [[ "${{ steps.meta.outputs.update-type }}" == "version-update:semver-major" ]]; then
29-
EVENT="opened_major"
75+
UPDATE_TYPE="${FM_UPDATE_TYPE}"
76+
if [[ -z "$UPDATE_TYPE" || "$UPDATE_TYPE" == "null" ]]; then
77+
if [[ "$PR_TITLE" =~ ([0-9]+)\.([0-9]+)\.([0-9]+).*to.*([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
78+
OM="${BASH_REMATCH[1]}"; Om="${BASH_REMATCH[2]}"
79+
NM="${BASH_REMATCH[4]}"; Nm="${BASH_REMATCH[5]}"
80+
if [[ "$OM" != "$NM" ]]; then UPDATE_TYPE="version-update:semver-major"
81+
elif [[ "$Om" != "$Nm" ]]; then UPDATE_TYPE="version-update:semver-minor"
82+
else UPDATE_TYPE="version-update:semver-patch"; fi
3083
else
31-
EVENT="opened_minor_patch"
84+
UPDATE_TYPE="unknown"
3285
fi
33-
elif [[ "${{ github.event.action }}" == "closed" && "${{ github.event.pull_request.merged }}" == "true" ]]; then
86+
fi
87+
88+
EVENT="skip"
89+
if [[ "$GH_EVENT" == "pull_request_target" && "$ACTION" == "opened" ]]; then
90+
if [[ "$UPDATE_TYPE" == "version-update:semver-major" ]]; then EVENT="opened_major"
91+
else EVENT="opened_minor_patch"; fi
92+
elif [[ "$GH_EVENT" == "pull_request" && "$ACTION" == "closed" && "$MERGED" == "true" ]]; then
93+
EVENT="merged"
94+
elif [[ "$GH_EVENT" == "push" ]]; then
3495
EVENT="merged"
35-
else
36-
echo "Not a tracked event. Skipping notification."
37-
echo "event=skip" >> "$GITHUB_OUTPUT"
38-
exit 0
3996
fi
97+
98+
case "$UPDATE_TYPE" in
99+
version-update:semver-patch) UPDATE_LABEL="patch update";;
100+
version-update:semver-minor) UPDATE_LABEL="minor update";;
101+
version-update:semver-major) UPDATE_LABEL="major update (may contain breaking changes)";;
102+
*) UPDATE_LABEL="update";;
103+
esac
104+
40105
echo "event=$EVENT" >> "$GITHUB_OUTPUT"
106+
echo "update-type=$UPDATE_TYPE" >> "$GITHUB_OUTPUT"
107+
echo "update-label=$UPDATE_LABEL" >> "$GITHUB_OUTPUT"
41108
42-
- name: Send Google Chat card
109+
- name: Send Google Chat card (threaded per repo)
43110
if: steps.classify.outputs.event != 'skip'
44111
env:
45112
WEBHOOK_URL: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
46113
EVENT: ${{ steps.classify.outputs.event }}
47-
UPDATE_TYPE: ${{ steps.meta.outputs.update-type }}
48-
DEP_NAMES: ${{ steps.meta.outputs.dependency-names }}
49-
ECOSYSTEM: ${{ steps.meta.outputs.package-ecosystem }}
50-
DIRECTORY: ${{ steps.meta.outputs.directory }}
51-
PREVIOUS_VERSION: ${{ steps.meta.outputs.previous-version }}
52-
NEW_VERSION: ${{ steps.meta.outputs.new-version }}
53-
CVSS: ${{ steps.meta.outputs.cvss || '0' }}
54-
DEP_GROUP: ${{ steps.meta.outputs.dependency-group }}
55-
PR_URL: ${{ github.event.pull_request.html_url }}
56-
PR_NUMBER: ${{ github.event.pull_request.number }}
57-
PR_TITLE: ${{ github.event.pull_request.title }}
114+
UPDATE_LABEL: ${{ steps.classify.outputs.update-label }}
115+
DEP_NAMES: ${{ steps.meta_pr.outputs.dependency-names || steps.meta_push.outputs.dependency-names }}
116+
ECOSYSTEM: ${{ steps.meta_pr.outputs.package-ecosystem || steps.meta_push.outputs.package-ecosystem }}
117+
DIRECTORY: ${{ steps.meta_pr.outputs.directory || steps.meta_push.outputs.directory }}
118+
PREVIOUS_VERSION: ${{ steps.meta_pr.outputs.previous-version || steps.meta_push.outputs.previous-version }}
119+
NEW_VERSION: ${{ steps.meta_pr.outputs.new-version || steps.meta_push.outputs.new-version }}
120+
DEP_GROUP: ${{ steps.meta_pr.outputs.dependency-group || steps.meta_push.outputs.dependency-group }}
121+
CVSS: ${{ steps.meta_pr.outputs.cvss || steps.meta_push.outputs.cvss || '0' }}
122+
PR_URL: ${{ github.event.pull_request.html_url || steps.pr_lookup.outputs.pr_url }}
123+
PR_NUMBER: ${{ github.event.pull_request.number || steps.pr_lookup.outputs.pr_number }}
124+
PR_TITLE: ${{ github.event.pull_request.title || steps.pr_lookup.outputs.pr_title }}
58125
REPO: ${{ github.repository }}
59-
MERGED_BY: ${{ github.event.pull_request.merged_by.login }}
126+
MERGED_BY: ${{ github.event.pull_request.merged_by.login || github.event.head_commit.author.username || 'app/github-actions' }}
60127
run: |
128+
case "$ECOSYSTEM" in
129+
npm_and_yarn) ECO_LABEL="npm";;
130+
pip) ECO_LABEL="pip";;
131+
github_actions) ECO_LABEL="GitHub Actions";;
132+
docker) ECO_LABEL="Docker";;
133+
*) ECO_LABEL="$ECOSYSTEM";;
134+
esac
135+
ECO_DISPLAY="$ECO_LABEL"
136+
if [[ -n "$DIRECTORY" && "$DIRECTORY" != "/" ]]; then
137+
ECO_DISPLAY="$ECO_LABEL ($DIRECTORY)"
138+
fi
139+
if [[ -n "$PREVIOUS_VERSION" && -n "$NEW_VERSION" ]]; then
140+
VERSION_TEXT="$PREVIOUS_VERSION -> $NEW_VERSION"
141+
else
142+
VERSION_TEXT="see PR for versions"
143+
fi
144+
if [[ -n "$DEP_GROUP" ]]; then
145+
PKG_COUNT=$(echo "$DEP_NAMES" | tr ',' '\n' | wc -l | tr -d ' ')
146+
PKG_SUMMARY="$DEP_GROUP group ($PKG_COUNT packages)"
147+
else
148+
PKG_SUMMARY="$DEP_NAMES"
149+
fi
61150
case "$EVENT" in
62151
opened_minor_patch)
63-
TITLE="Dependabot PR opened — auto-merge enabled"
64-
SUBTITLE="${REPO} · #${PR_NUMBER}"
65-
STATUS_TEXT="Auto-merge enabled. Will merge once CI passes."
66-
STATUS_LABEL="Status"
67-
ACTION_BUTTON_TEXT="View PR"
152+
HEADER_TITLE="Safe update - auto-merging after CI"
153+
STATUS_LABEL="Action Required"
154+
STATUS_TEXT="No action needed. Auto-merge enabled; will merge once CI passes."
155+
BUTTON_TEXT="View PR"
68156
;;
69157
opened_major)
70-
TITLE="MAJOR version bump — human review required"
71-
SUBTITLE="${REPO} · #${PR_NUMBER}"
72-
STATUS_TEXT="This is a MAJOR version upgrade and may contain breaking changes. Please review and merge manually if safe."
158+
HEADER_TITLE="MAJOR version bump - human review required"
73159
STATUS_LABEL="Action Required"
74-
ACTION_BUTTON_TEXT="Review PR"
160+
STATUS_TEXT="This is a MAJOR version upgrade and may contain breaking changes. Please review and merge manually if safe."
161+
BUTTON_TEXT="Review PR"
75162
;;
76163
merged)
77-
TITLE="Dependabot PR merged"
78-
SUBTITLE="${REPO} · #${PR_NUMBER}"
79-
STATUS_TEXT="Merged into default branch${MERGED_BY:+ by ${MERGED_BY}}."
80-
STATUS_LABEL="Status"
81-
ACTION_BUTTON_TEXT="View PR"
164+
HEADER_TITLE="Merged into main"
165+
STATUS_LABEL="Merged By"
166+
if [[ -n "$MERGED_BY" && "$MERGED_BY" != "null" ]]; then
167+
STATUS_TEXT="$MERGED_BY"
168+
else
169+
STATUS_TEXT="dependabot[bot] (auto-merge)"
170+
fi
171+
BUTTON_TEXT="View PR"
82172
;;
83173
esac
174+
SUBTITLE="$REPO - PR #$PR_NUMBER"
175+
CARD_ID="dependabot-$EVENT-$(date +%s)"
176+
THREAD_KEY=$(echo "$REPO" | tr '[:upper:]' '[:lower:]' | tr '/' '-')
177+
THREADED_URL="${WEBHOOK_URL}&threadKey=${THREAD_KEY}&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD"
84178
85-
VERSION_TEXT="${PREVIOUS_VERSION:-?} → ${NEW_VERSION:-?}"
86-
[[ "$VERSION_TEXT" == "? → ?" ]] && VERSION_TEXT="see PR for versions"
87-
88-
GROUP_LINE=""
89-
if [[ -n "$DEP_GROUP" ]]; then
90-
GROUP_LINE=",{\"decoratedText\": {\"topLabel\": \"Group\", \"text\": \"${DEP_GROUP}\"}}"
91-
fi
179+
jq -n \
180+
--arg cardId "$CARD_ID" \
181+
--arg title "$HEADER_TITLE" \
182+
--arg subtitle "$SUBTITLE" \
183+
--arg statusLabel "$STATUS_LABEL" \
184+
--arg statusText "$STATUS_TEXT" \
185+
--arg prTitle "$PR_TITLE" \
186+
--arg pkgSummary "$PKG_SUMMARY" \
187+
--arg versionText "$VERSION_TEXT" \
188+
--arg updateLabel "$UPDATE_LABEL" \
189+
--arg ecoDisplay "$ECO_DISPLAY" \
190+
--arg buttonText "$BUTTON_TEXT" \
191+
--arg prUrl "$PR_URL" \
192+
--arg cvss "$CVSS" \
193+
'{
194+
cardsV2: [{
195+
cardId: $cardId,
196+
card: {
197+
header: {
198+
title: $title,
199+
subtitle: $subtitle,
200+
imageUrl: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
201+
imageType: "CIRCLE"
202+
},
203+
sections: [{
204+
widgets: (
205+
[
206+
{decoratedText: {topLabel: $statusLabel, text: $statusText, wrapText: true}},
207+
{decoratedText: {topLabel: "PR Title", text: $prTitle, wrapText: true}},
208+
{decoratedText: {topLabel: "Package(s)", text: $pkgSummary, wrapText: true}},
209+
{decoratedText: {topLabel: "Version", text: $versionText}},
210+
{decoratedText: {topLabel: "Update Type", text: $updateLabel}},
211+
{decoratedText: {topLabel: "Ecosystem", text: $ecoDisplay}}
212+
]
213+
+ (if ($cvss | IN("0","0.0","")) then [] else [{decoratedText: {topLabel: "CVSS Score (Common Vulnerability Scoring System, 0 to 10 scale)", text: $cvss}}] end)
214+
+ [{buttonList: {buttons: [{text: $buttonText, onClick: {openLink: {url: $prUrl}}}]}}]
215+
)
216+
}]
217+
}
218+
}]
219+
}' > payload.json
92220
93-
cat > payload.json <<EOF
94-
{
95-
"cardsV2": [{
96-
"cardId": "dependabot-${EVENT}-$(date +%s)",
97-
"card": {
98-
"header": {
99-
"title": "${TITLE}",
100-
"subtitle": "${SUBTITLE}",
101-
"imageUrl": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
102-
"imageType": "CIRCLE"
103-
},
104-
"sections": [{
105-
"widgets": [
106-
{"decoratedText": {"topLabel": "${STATUS_LABEL}", "text": "${STATUS_TEXT}", "wrapText": true}},
107-
{"decoratedText": {"topLabel": "PR Title", "text": "${PR_TITLE}", "wrapText": true}},
108-
{"decoratedText": {"topLabel": "Package(s)", "text": "${DEP_NAMES}", "wrapText": true}},
109-
{"decoratedText": {"topLabel": "Version", "text": "${VERSION_TEXT}"}},
110-
{"decoratedText": {"topLabel": "Update Type", "text": "${UPDATE_TYPE}"}},
111-
{"decoratedText": {"topLabel": "Ecosystem", "text": "${ECOSYSTEM} (${DIRECTORY})"}}${GROUP_LINE},
112-
{"decoratedText": {"topLabel": "CVSS Score", "text": "${CVSS}"}},
113-
{"buttonList": {"buttons": [
114-
{"text": "${ACTION_BUTTON_TEXT}", "onClick": {"openLink": {"url": "${PR_URL}"}}}
115-
]}}
116-
]
117-
}]
118-
}
119-
}]
120-
}
121-
EOF
122-
curl -sS -X POST "$WEBHOOK_URL" \
221+
curl -sS -X POST "$THREADED_URL" \
123222
-H "Content-Type: application/json; charset=UTF-8" \
124223
-d @payload.json

0 commit comments

Comments
 (0)