Skip to content

Commit 5281d9a

Browse files
committed
ci: auto-merge dependency bot PRs
1 parent 9f39a25 commit 5281d9a

3 files changed

Lines changed: 206 additions & 6 deletions

File tree

.github/workflows/README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,27 @@ This directory contains GitHub Actions workflows for continuous integration and
101101

102102
**Triggers:** Push/PR to default branch
103103

104-
### 4. `stale.yml` - Issue Management
104+
### 4. `dependency-auto-merge.yml` - Dependency Bot Auto-Merge
105+
106+
**Purpose:** Automatically approves and merges clean dependency update PRs from Dependabot and Renovate.
107+
108+
**Triggers:**
109+
- Pull request target events for dependency bot PR approval
110+
- Successful `BTrace CI/CD` or `CodeQL` workflow completion for merge checks
111+
112+
**Behavior:**
113+
- Only acts on PRs authored by `dependabot[bot]` or `renovate[bot]`
114+
- Does not check out PR code
115+
- Requires both `BTrace CI/CD` and `CodeQL` to pass for the exact PR head commit
116+
- Verifies all latest commit checks/statuses, excluding this automation itself, are successful, skipped, or neutral before merging
117+
118+
### 5. `stale.yml` - Issue Management
105119

106120
**Purpose:** Automatically marks stale issues and PRs.
107121

108122
**Schedule:** Daily at midnight
109123

110-
### 5. `release.yml` - Release Management
124+
### 6. `release.yml` - Release Management
111125

112126
**Purpose:** Handles the complete release process with a manual checkpoint for Maven Central.
113127

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
name: Dependency Bot Auto-Merge
2+
3+
on:
4+
pull_request_target:
5+
types: [ opened, reopened, synchronize, ready_for_review ]
6+
workflow_run:
7+
workflows:
8+
- BTrace CI/CD
9+
- CodeQL
10+
types: [ completed ]
11+
12+
permissions:
13+
actions: read
14+
checks: read
15+
contents: write
16+
pull-requests: write
17+
statuses: read
18+
19+
defaults:
20+
run:
21+
shell: bash
22+
23+
env:
24+
REQUIRED_CI_WORKFLOWS: |
25+
BTrace CI/CD
26+
CodeQL
27+
28+
jobs:
29+
approve:
30+
name: Approve dependency bot PR
31+
if: >
32+
github.event_name == 'pull_request_target' &&
33+
!github.event.pull_request.draft &&
34+
contains(fromJSON('["dependabot[bot]","renovate[bot]"]'), github.event.pull_request.user.login)
35+
runs-on: ubuntu-latest
36+
steps:
37+
- name: Approve PR
38+
env:
39+
GH_TOKEN: ${{ secrets.DEPENDENCY_AUTOMERGE_TOKEN || github.token }}
40+
PR_URL: ${{ github.event.pull_request.html_url }}
41+
run: |
42+
set -euo pipefail
43+
gh pr review "${PR_URL}" \
44+
--approve \
45+
--body "Auto-approved dependency bot PR." || \
46+
echo "::notice::Approval was skipped; it may already be approved."
47+
48+
merge:
49+
name: Merge dependency bot PR
50+
if: >
51+
github.event_name == 'workflow_run' &&
52+
github.event.workflow_run.event == 'pull_request' &&
53+
github.event.workflow_run.conclusion == 'success'
54+
runs-on: ubuntu-latest
55+
steps:
56+
- name: Merge clean dependency bot PRs
57+
env:
58+
GH_TOKEN: ${{ secrets.DEPENDENCY_AUTOMERGE_TOKEN || github.token }}
59+
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
60+
PULL_REQUESTS: ${{ toJson(github.event.workflow_run.pull_requests) }}
61+
REPOSITORY: ${{ github.repository }}
62+
run: |
63+
set -euo pipefail
64+
65+
is_dependency_bot() {
66+
case "$1" in
67+
dependabot\[bot\]|renovate\[bot\]) return 0 ;;
68+
*) return 1 ;;
69+
esac
70+
}
71+
72+
required_workflows_passed() {
73+
local sha="$1"
74+
local runs_json workflow status conclusion
75+
76+
runs_json="$(gh api "repos/${REPOSITORY}/actions/runs" \
77+
--method GET \
78+
-f event=pull_request \
79+
-f head_sha="${sha}" \
80+
-F per_page=100)"
81+
82+
while IFS= read -r workflow; do
83+
[[ -z "${workflow}" ]] && continue
84+
85+
status="$(jq -r --arg workflow "${workflow}" '
86+
[.workflow_runs[] | select(.name == $workflow)]
87+
| sort_by(.created_at)
88+
| last
89+
| .status // ""
90+
' <<< "${runs_json}")"
91+
92+
conclusion="$(jq -r --arg workflow "${workflow}" '
93+
[.workflow_runs[] | select(.name == $workflow)]
94+
| sort_by(.created_at)
95+
| last
96+
| .conclusion // ""
97+
' <<< "${runs_json}")"
98+
99+
if [[ "${status}" != "completed" || "${conclusion}" != "success" ]]; then
100+
echo "::notice::Waiting for ${workflow} on ${sha} (status=${status:-missing}, conclusion=${conclusion:-missing})."
101+
return 1
102+
fi
103+
done <<< "${REQUIRED_CI_WORKFLOWS}"
104+
}
105+
106+
clean_commit_checks() {
107+
local sha="$1"
108+
local checks_json statuses_json bad_checks bad_statuses
109+
110+
checks_json="$(gh api "repos/${REPOSITORY}/commits/${sha}/check-runs" \
111+
--method GET \
112+
-f filter=latest \
113+
-F per_page=100)"
114+
115+
bad_checks="$(jq -r '
116+
.check_runs[]
117+
| select(
118+
(.name == "Approve dependency bot PR" or .name == "Merge dependency bot PR") | not
119+
)
120+
| select(
121+
.status != "completed" or
122+
((.conclusion == "success" or .conclusion == "skipped" or .conclusion == "neutral") | not)
123+
)
124+
| "- \(.name): \(.status)/\(.conclusion // "none")"
125+
' <<< "${checks_json}")"
126+
127+
statuses_json="$(gh api "repos/${REPOSITORY}/commits/${sha}/status")"
128+
129+
bad_statuses="$(jq -r '
130+
.statuses[]
131+
| select((.context | contains("Dependency Bot Auto-Merge")) | not)
132+
| select(.state != "success")
133+
| "- \(.context): \(.state)"
134+
' <<< "${statuses_json}")"
135+
136+
if [[ -n "${bad_checks}" || -n "${bad_statuses}" ]]; then
137+
echo "::notice::CI is not clean for ${sha}."
138+
[[ -n "${bad_checks}" ]] && printf '%s\n' "${bad_checks}"
139+
[[ -n "${bad_statuses}" ]] && printf '%s\n' "${bad_statuses}"
140+
return 1
141+
fi
142+
}
143+
144+
mapfile -t pr_numbers < <(jq -r '.[].number' <<< "${PULL_REQUESTS}")
145+
146+
if [[ "${#pr_numbers[@]}" -eq 0 ]]; then
147+
echo "::notice::No pull requests were attached to this workflow run."
148+
exit 0
149+
fi
150+
151+
for pr_number in "${pr_numbers[@]}"; do
152+
pr_json="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}")"
153+
author="$(jq -r '.user.login' <<< "${pr_json}")"
154+
draft="$(jq -r '.draft' <<< "${pr_json}")"
155+
head_sha="$(jq -r '.head.sha' <<< "${pr_json}")"
156+
pr_state="$(jq -r '.state' <<< "${pr_json}")"
157+
pr_url="$(jq -r '.html_url' <<< "${pr_json}")"
158+
159+
if ! is_dependency_bot "${author}"; then
160+
echo "::notice::Skipping PR #${pr_number}: author ${author} is not a dependency bot."
161+
continue
162+
fi
163+
164+
if [[ "${pr_state}" != "open" || "${draft}" == "true" ]]; then
165+
echo "::notice::Skipping PR #${pr_number}: state=${pr_state}, draft=${draft}."
166+
continue
167+
fi
168+
169+
if [[ "${head_sha}" != "${HEAD_SHA}" ]]; then
170+
echo "::notice::Skipping PR #${pr_number}: workflow ran on ${HEAD_SHA}, current head is ${head_sha}."
171+
continue
172+
fi
173+
174+
if ! required_workflows_passed "${head_sha}"; then
175+
continue
176+
fi
177+
178+
if ! clean_commit_checks "${head_sha}"; then
179+
continue
180+
fi
181+
182+
gh pr review "${pr_url}" \
183+
--approve \
184+
--body "Auto-approved dependency bot PR after CI passed." || \
185+
echo "::notice::Approval for PR #${pr_number} was skipped; it may already be approved."
186+
187+
gh pr merge "${pr_url}" --squash --delete-branch
188+
done

renovate.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@
77
],
88
"schedule": ["before 9am on monday"],
99
"labels": ["dependencies"],
10+
"automerge": true,
11+
"automergeType": "pr",
1012
"packageRules": [
1113
{
1214
"matchManagers": ["github-actions"],
1315
"addLabels": ["github-actions"]
14-
},
15-
{
16-
"matchUpdateTypes": ["minor", "patch"],
17-
"automerge": true
1816
}
1917
]
2018
}

0 commit comments

Comments
 (0)