Skip to content

Commit 80fe2a8

Browse files
Add Dependabot auto-merge workflows (ROSA-745)
- Auto-merge patch/minor/digest after CI; majors manual - pull_request_target with validated API responses - branch-protection-check for config/workflow presence Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e13fe45 commit 80fe2a8

2 files changed

Lines changed: 325 additions & 0 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: Branch Protection Check
2+
3+
on:
4+
schedule:
5+
- cron: '0 9 * * 1' # Every Monday at 9 AM UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
verify-config:
13+
name: Verify Dependabot and auto-merge setup
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
19+
- name: Validate Dependabot and workflow configuration
20+
run: |
21+
set -euo pipefail
22+
pip install --quiet pyyaml
23+
python3 <<'PY'
24+
import sys
25+
from pathlib import Path
26+
27+
import yaml
28+
29+
def fail(msg: str) -> None:
30+
print(f"❌ {msg}")
31+
sys.exit(1)
32+
33+
dependabot_path = Path(".github/dependabot.yml")
34+
if not dependabot_path.is_file():
35+
fail("Dependabot configuration missing (.github/dependabot.yml)")
36+
37+
with dependabot_path.open() as f:
38+
cfg = yaml.safe_load(f)
39+
if not isinstance(cfg, dict):
40+
fail("dependabot.yml must be a YAML mapping")
41+
if cfg.get("version") != 2:
42+
fail("dependabot.yml: version must be 2")
43+
updates = cfg.get("updates")
44+
if not isinstance(updates, list) or not updates:
45+
fail("dependabot.yml: updates must be a non-empty list")
46+
for i, entry in enumerate(updates):
47+
if not isinstance(entry, dict):
48+
fail(f"dependabot.yml: updates[{i}] must be a mapping")
49+
if not entry.get("package-ecosystem"):
50+
fail(f"dependabot.yml: updates[{i}] missing package-ecosystem")
51+
if "directory" not in entry:
52+
fail(f"dependabot.yml: updates[{i}] missing directory")
53+
54+
print("✅ dependabot.yml structure is valid")
55+
for entry in updates:
56+
print(f" - {entry.get('package-ecosystem')} ({entry.get('directory')})")
57+
58+
workflow_path = Path(".github/workflows/dependabot-auto-merge.yml")
59+
if not workflow_path.is_file():
60+
fail("dependabot-auto-merge workflow missing")
61+
62+
with workflow_path.open() as f:
63+
wf = yaml.safe_load(f)
64+
if not isinstance(wf, dict):
65+
fail("dependabot-auto-merge.yml must be a YAML mapping")
66+
on = wf.get("on")
67+
if not isinstance(on, dict) or "pull_request_target" not in on:
68+
fail("dependabot-auto-merge.yml must use pull_request_target trigger")
69+
jobs = wf.get("jobs")
70+
if not isinstance(jobs, dict) or "auto-merge" not in jobs:
71+
fail("dependabot-auto-merge.yml must define jobs.auto-merge")
72+
job = jobs["auto-merge"]
73+
if not isinstance(job, dict):
74+
fail("jobs.auto-merge must be a mapping")
75+
steps = job.get("steps")
76+
if not isinstance(steps, list) or not steps:
77+
fail("jobs.auto-merge must define steps")
78+
uses = [
79+
s.get("uses", "")
80+
for s in steps
81+
if isinstance(s, dict)
82+
]
83+
if not any("dependabot/fetch-metadata" in u for u in uses):
84+
fail("jobs.auto-merge must include dependabot/fetch-metadata")
85+
86+
print("✅ dependabot-auto-merge.yml structure is valid")
87+
print("")
88+
print("ℹ️ dependabot-auto-merge.yml enables merge via GraphQL")
89+
print(" enablePullRequestAutoMerge; the PR merges only after existing")
90+
print(" required status checks pass (e.g. ci/prow/*). No extra CI job.")
91+
PY
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
name: Dependabot Auto-Merge
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
7+
permissions:
8+
contents: write
9+
pull-requests: write
10+
checks: read
11+
actions: read
12+
13+
jobs:
14+
auto-merge:
15+
runs-on: ubuntu-latest
16+
if: github.event.pull_request.user.login == 'dependabot[bot]' && github.repository_owner == 'openshift'
17+
steps:
18+
- name: Fetch Dependabot Metadata
19+
id: metadata
20+
uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2
21+
with:
22+
github-token: "${{ secrets.GITHUB_TOKEN }}"
23+
24+
- name: Enable Auto-Merge for Safe Updates
25+
id: enable-auto-merge
26+
if: |
27+
steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
28+
steps.metadata.outputs.update-type == 'version-update:semver-minor'
29+
env:
30+
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
31+
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
32+
PREVIOUS_VERSION: ${{ steps.metadata.outputs.previous-version }}
33+
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
34+
REPOSITORY: ${{ github.repository }}
35+
PR_NUMBER: ${{ github.event.pull_request.number }}
36+
run: |
37+
set -euo pipefail
38+
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
39+
export GH_TOKEN
40+
41+
comment_count() {
42+
local marker="$1"
43+
local http_code
44+
http_code=$(curl -sS -w "%{http_code}" -o /tmp/comments-list.json \
45+
-H "Accept: application/vnd.github+json" \
46+
-H "Authorization: Bearer $GH_TOKEN" \
47+
"https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments")
48+
if [[ "$http_code" != "200" ]]; then
49+
echo "::warning::Could not list PR comments (HTTP ${http_code})" >&2
50+
echo "1"
51+
return
52+
fi
53+
jq --arg m "$marker" '[.[] | select(.body | contains($m))] | length' /tmp/comments-list.json
54+
}
55+
56+
post_issue_comment() {
57+
local body="$1"
58+
local http_code
59+
http_code=$(curl -sS -w "%{http_code}" -o /tmp/comment-response.json \
60+
-X POST \
61+
-H "Accept: application/vnd.github+json" \
62+
-H "Authorization: Bearer $GH_TOKEN" \
63+
"https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \
64+
-d "$(jq -n --arg body "$body" '{body: $body}')")
65+
if [[ ! "$http_code" =~ ^2 ]]; then
66+
echo "❌ Failed to post PR comment. HTTP status: ${http_code}"
67+
cat /tmp/comment-response.json
68+
echo "::warning::PR comment could not be posted"
69+
return 1
70+
fi
71+
}
72+
73+
graphql_auto_merge_ok() {
74+
local http_code="$1"
75+
[[ "$http_code" == "200" ]] || return 1
76+
jq -e '(.errors // []) | length == 0' /tmp/response.json >/dev/null 2>&1 || return 1
77+
jq -e '.data.enablePullRequestAutoMerge.pullRequest != null' /tmp/response.json >/dev/null 2>&1
78+
}
79+
80+
graphql_error_summary() {
81+
jq -c '(.errors // []) | if length > 0 then . else .data end' /tmp/response.json 2>/dev/null || cat /tmp/response.json
82+
}
83+
84+
echo "Enabling auto-merge for ${UPDATE_TYPE} update"
85+
echo "Dependency: ${DEPENDENCY_NAMES}"
86+
87+
pr_http_code=$(curl -sS -w "%{http_code}" -o /tmp/pr-response.json \
88+
-H "Accept: application/vnd.github+json" \
89+
-H "Authorization: Bearer $GH_TOKEN" \
90+
"https://api.github.com/repos/${REPOSITORY}/pulls/${PR_NUMBER}")
91+
92+
if [[ "$pr_http_code" != "200" ]]; then
93+
echo "❌ Failed to fetch PR metadata. HTTP status: ${pr_http_code}"
94+
cat /tmp/pr-response.json
95+
echo "auto_merge_enabled=false" >> "$GITHUB_OUTPUT"
96+
exit 1
97+
fi
98+
99+
PR_NODE_ID=$(jq -r '.node_id' /tmp/pr-response.json)
100+
if [[ -z "$PR_NODE_ID" || "$PR_NODE_ID" == "null" ]]; then
101+
echo "❌ Failed to parse PR node ID from response"
102+
cat /tmp/pr-response.json
103+
echo "auto_merge_enabled=false" >> "$GITHUB_OUTPUT"
104+
exit 1
105+
fi
106+
107+
http_code=$(curl -sS -w "%{http_code}" -o /tmp/response.json \
108+
-X POST \
109+
-H "Accept: application/vnd.github+json" \
110+
-H "Authorization: Bearer $GH_TOKEN" \
111+
"https://api.github.com/graphql" \
112+
-d "{\"query\":\"mutation { enablePullRequestAutoMerge(input: { pullRequestId: \\\"$PR_NODE_ID\\\", mergeMethod: SQUASH }) { pullRequest { autoMergeRequest { enabledAt } } } }\"}")
113+
114+
if graphql_auto_merge_ok "$http_code"; then
115+
echo "✅ Auto-merge enabled successfully via GraphQL"
116+
cat /tmp/response.json
117+
echo "auto_merge_enabled=true" >> "$GITHUB_OUTPUT"
118+
else
119+
api_detail=$(graphql_error_summary)
120+
echo "❌ Failed to enable auto-merge. HTTP status: ${http_code}"
121+
echo "Response body:"
122+
cat /tmp/response.json
123+
echo "auto_merge_enabled=false" >> "$GITHUB_OUTPUT"
124+
echo "::warning::Could not enable auto-merge. PR may need manual review."
125+
if [[ "$(comment_count 'Dependabot Auto-Merge Status')" -eq 0 ]]; then
126+
failure_body=$(jq -rn \
127+
--arg ut "$UPDATE_TYPE" \
128+
--arg deps "$DEPENDENCY_NAMES" \
129+
--arg prev "$PREVIOUS_VERSION" \
130+
--arg new "$NEW_VERSION" \
131+
--arg api "$api_detail" \
132+
'@text "🤖 **Dependabot Auto-Merge Status**
133+
134+
This PR meets the criteria for auto-merge but could not be automatically merged.
135+
136+
**Details:**
137+
- Update type: \($ut)
138+
- Dependencies: \($deps)
139+
- Previous version: \($prev)
140+
- New version: \($new)
141+
- API response: `\($api)`
142+
143+
Please review and merge manually if appropriate."')
144+
post_issue_comment "$failure_body" || true
145+
else
146+
echo "Auto-merge status comment already posted; skipping duplicate"
147+
fi
148+
fi
149+
150+
- name: Comment on Major Version Updates
151+
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
152+
env:
153+
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
154+
PREVIOUS_VERSION: ${{ steps.metadata.outputs.previous-version }}
155+
NEW_VERSION: ${{ steps.metadata.outputs.new-version }}
156+
REPOSITORY: ${{ github.repository }}
157+
PR_NUMBER: ${{ github.event.pull_request.number }}
158+
run: |
159+
set -euo pipefail
160+
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
161+
export GH_TOKEN
162+
163+
comments_http=$(curl -sS -w "%{http_code}" -o /tmp/comments-list.json \
164+
-H "Accept: application/vnd.github+json" \
165+
-H "Authorization: Bearer $GH_TOKEN" \
166+
"https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments")
167+
if [[ "$comments_http" != "200" ]]; then
168+
echo "::warning::Could not list PR comments (HTTP ${comments_http})" >&2
169+
exit 0
170+
fi
171+
existing=$(jq '[.[] | select(.body | contains("Major Version Update Detected"))] | length' /tmp/comments-list.json)
172+
173+
if [[ "$existing" -gt 0 ]]; then
174+
echo "Major-version notice already posted; skipping duplicate comment"
175+
exit 0
176+
fi
177+
178+
major_body=$(jq -rn \
179+
--arg deps "$DEPENDENCY_NAMES" \
180+
--arg prev "$PREVIOUS_VERSION" \
181+
--arg new "$NEW_VERSION" \
182+
'@text "🚨 **Major Version Update Detected** 🚨
183+
184+
This PR contains a major version update that requires manual review:
185+
- **Dependency:** \($deps)
186+
- **Previous version:** \($prev)
187+
- **New version:** \($new)
188+
189+
Please review the changelog and breaking changes before merging.
190+
191+
Auto-merge has been **disabled** for this PR."')
192+
193+
http_code=$(curl -sS -w "%{http_code}" -o /tmp/comment-response.json \
194+
-X POST \
195+
-H "Accept: application/vnd.github+json" \
196+
-H "Authorization: Bearer $GH_TOKEN" \
197+
"https://api.github.com/repos/${REPOSITORY}/issues/${PR_NUMBER}/comments" \
198+
-d "$(jq -n --arg body "$major_body" '{body: $body}')")
199+
200+
if [[ ! "$http_code" =~ ^2 ]]; then
201+
echo "❌ Failed to post major-version comment. HTTP status: ${http_code}"
202+
cat /tmp/comment-response.json
203+
echo "::warning::Major-version comment could not be posted"
204+
fi
205+
206+
- name: Log Auto-Merge Decision
207+
if: always() && steps.metadata.outcome == 'success'
208+
env:
209+
UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }}
210+
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
211+
PR_NUMBER: ${{ github.event.pull_request.number }}
212+
AUTO_MERGE_ENABLED: ${{ steps.enable-auto-merge.outputs.auto_merge_enabled }}
213+
run: |
214+
echo "Auto-merge decision for PR #${PR_NUMBER}:"
215+
echo "- Update type: ${UPDATE_TYPE}"
216+
echo "- Dependency: ${DEPENDENCY_NAMES}"
217+
218+
case "${UPDATE_TYPE}" in
219+
version-update:semver-patch|version-update:semver-minor)
220+
if [[ "${AUTO_MERGE_ENABLED}" == "true" ]]; then
221+
echo "✅ Auto-merge ENABLED (GraphQL mutation succeeded)"
222+
elif [[ "${AUTO_MERGE_ENABLED}" == "false" ]]; then
223+
echo "❌ Auto-merge NOT enabled (GraphQL mutation failed — see enable step logs)"
224+
else
225+
echo "⚠️ Auto-merge enable step did not complete (check workflow logs)"
226+
fi
227+
;;
228+
version-update:semver-major)
229+
echo "❌ Auto-merge DISABLED: Major version update"
230+
;;
231+
*)
232+
echo "❌ Auto-merge DISABLED: Update type not eligible for auto-merge (${UPDATE_TYPE})"
233+
;;
234+
esac

0 commit comments

Comments
 (0)