Skip to content

Commit 94439fc

Browse files
committed
Add Dependabot auto-merge workflow and CODEOWNERS bypass
- Add auto-dependabot.yaml workflow to auto-merge Dependabot PRs - Implement migration script functions to create workflow files - Add GitHub API integration to disable CODEOWNERS review requirement - Update all cookiecutter templates and golden test files Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent 0dcf7c2 commit 94439fc

File tree

8 files changed

+364
-1
lines changed

8 files changed

+364
-1
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Auto-merge Dependabot PR
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
jobs:
11+
auto-merge:
12+
if: github.actor == 'dependabot[bot]'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Auto-merge Dependabot PR
16+
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
dependency-type: 'all'
20+
auto-merge: 'true'
21+
merge-method: 'merge'
22+
add-label: 'tool:auto-merged'

cookiecutter/migrate.py

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,230 @@
2121
""" # noqa: E501
2222

2323
import hashlib
24+
import json
2425
import os
2526
import subprocess
2627
import tempfile
2728
from pathlib import Path
28-
from typing import SupportsIndex
29+
from typing import Any, SupportsIndex
2930

3031

3132
def main() -> None:
3233
"""Run the migration steps."""
3334
# Add a separation line like this one after each migration step.
3435
print("=" * 72)
36+
print("Creating Dependabot auto-merge workflow...")
37+
create_dependabot_auto_merge_workflow()
38+
print("=" * 72)
39+
print("Disabling CODEOWNERS review requirement in GitHub ruleset...")
40+
disable_codeowners_review_requirement()
41+
print("=" * 72)
3542
print("Migration script finished. Remember to follow any manual instructions.")
3643
print("=" * 72)
3744

3845

46+
def create_dependabot_auto_merge_workflow() -> None:
47+
"""Create the Dependabot auto-merge workflow file."""
48+
workflow_dir = Path(".github") / "workflows"
49+
workflow_dir.mkdir(parents=True, exist_ok=True)
50+
51+
workflow_content = """name: Auto-merge Dependabot PR
52+
53+
on:
54+
pull_request:
55+
56+
permissions:
57+
contents: write
58+
pull-requests: write
59+
60+
jobs:
61+
auto-merge:
62+
if: github.actor == 'dependabot[bot]'
63+
runs-on: ubuntu-latest
64+
steps:
65+
- name: Auto-merge Dependabot PR
66+
uses: >-
67+
frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1
68+
69+
with:
70+
github-token: ${{ secrets.GITHUB_TOKEN }}
71+
dependency-type: 'all'
72+
auto-merge: 'true'
73+
merge-method: 'merge'
74+
add-label: 'tool:auto-merged'
75+
"""
76+
77+
workflow_file = workflow_dir / "auto-dependabot.yaml"
78+
workflow_file.write_text(workflow_content, encoding="utf-8")
79+
print(f"Created/Updated Dependabot auto-merge workflow at {workflow_file}")
80+
81+
82+
def get_default_branch() -> str | None:
83+
"""Get the default branch name from GitHub.
84+
85+
Returns:
86+
The default branch name, or None if it cannot be determined.
87+
"""
88+
try:
89+
result = subprocess.run(
90+
["gh", "api", "repos/:owner/:repo", "--jq", ".default_branch"],
91+
capture_output=True,
92+
text=True,
93+
check=True,
94+
)
95+
default_branch = result.stdout.strip()
96+
print(f"Default branch: {default_branch}")
97+
return default_branch
98+
except subprocess.CalledProcessError as e:
99+
print(f"Failed to get default branch: {e}")
100+
return None
101+
102+
103+
def find_version_branch_ruleset() -> dict[str, Any] | None:
104+
"""Find the 'Protect version branches' ruleset.
105+
106+
Returns:
107+
The ruleset configuration, or None if not found.
108+
"""
109+
try:
110+
result = subprocess.run(
111+
["gh", "api", "repos/:owner/:repo/rulesets"],
112+
capture_output=True,
113+
text=True,
114+
check=True,
115+
)
116+
rulesets = json.loads(result.stdout)
117+
118+
for ruleset in rulesets:
119+
if ruleset.get("name") == "Protect version branches":
120+
return ruleset # type: ignore[no-any-return]
121+
return None
122+
except subprocess.CalledProcessError as e:
123+
print(f"Failed to fetch rulesets: {e}")
124+
return None
125+
126+
127+
def update_ruleset(ruleset_id: int, ruleset_config: dict[str, Any]) -> bool:
128+
"""Update a GitHub ruleset configuration.
129+
130+
Args:
131+
ruleset_id: The ID of the ruleset to update.
132+
ruleset_config: The updated ruleset configuration.
133+
134+
Returns:
135+
True if the update was successful, False otherwise.
136+
"""
137+
update_payload = {
138+
"name": ruleset_config["name"],
139+
"target": ruleset_config["target"],
140+
"enforcement": ruleset_config["enforcement"],
141+
"conditions": ruleset_config["conditions"],
142+
"rules": ruleset_config["rules"],
143+
}
144+
145+
if "bypass_actors" in ruleset_config:
146+
update_payload["bypass_actors"] = ruleset_config["bypass_actors"]
147+
148+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
149+
json.dump(update_payload, f, indent=2)
150+
temp_file = f.name
151+
152+
try:
153+
subprocess.run(
154+
[
155+
"gh",
156+
"api",
157+
"-X",
158+
"PUT",
159+
f"repos/:owner/:repo/rulesets/{ruleset_id}",
160+
"--input",
161+
temp_file,
162+
],
163+
capture_output=True,
164+
check=True,
165+
)
166+
return True
167+
except subprocess.CalledProcessError:
168+
return False
169+
finally:
170+
os.unlink(temp_file)
171+
172+
173+
def disable_codeowners_review_requirement() -> None:
174+
"""Disable CODEOWNERS review requirement in GitHub repository ruleset."""
175+
# Get repository info
176+
try:
177+
result = subprocess.run(
178+
["gh", "repo", "view", "--json", "owner,name"],
179+
capture_output=True,
180+
text=True,
181+
check=True,
182+
)
183+
repo_info = json.loads(result.stdout)
184+
org = repo_info["owner"]["login"]
185+
repo = repo_info["name"]
186+
ruleset_url = f"https://github.com/{org}/{repo}/settings/rules"
187+
except subprocess.CalledProcessError:
188+
ruleset_url = "GitHub repository settings > Rules"
189+
190+
if get_default_branch() is None:
191+
manual_step(
192+
"Failed to get default branch. "
193+
"Please manually disable the CODEOWNERS review requirement in the "
194+
f"'Protect version branches' ruleset at: {ruleset_url}"
195+
)
196+
return
197+
198+
version_branch_ruleset = find_version_branch_ruleset()
199+
if not version_branch_ruleset:
200+
manual_step(
201+
"'Protect version branches' ruleset not found. "
202+
"Please manually disable the CODEOWNERS review requirement at: "
203+
f"{ruleset_url}"
204+
)
205+
return
206+
207+
ruleset_id = version_branch_ruleset["id"]
208+
print(f"Found ruleset ID: {ruleset_id}")
209+
210+
try:
211+
result = subprocess.run(
212+
["gh", "api", f"repos/:owner/:repo/rulesets/{ruleset_id}"],
213+
capture_output=True,
214+
text=True,
215+
check=True,
216+
)
217+
ruleset_config = json.loads(result.stdout)
218+
except subprocess.CalledProcessError as e:
219+
manual_step(
220+
f"Failed to fetch ruleset configuration: {e}. "
221+
"This action requires admin permissions. "
222+
f"Please manually disable the CODEOWNERS review requirement at: {ruleset_url}"
223+
)
224+
return
225+
226+
updated = False
227+
for rule in ruleset_config.get("rules", []):
228+
if rule.get("type") == "pull_request":
229+
if rule.get("parameters", {}).get("require_code_owner_review"):
230+
rule["parameters"]["require_code_owner_review"] = False
231+
updated = True
232+
break
233+
234+
if not updated:
235+
print("CODEOWNERS review requirement already disabled.")
236+
return
237+
238+
if update_ruleset(ruleset_id, ruleset_config):
239+
print("Successfully disabled CODEOWNERS review requirement in GitHub ruleset.")
240+
else:
241+
manual_step(
242+
"Failed to update GitHub ruleset. This action requires admin permissions. "
243+
"Please manually disable the CODEOWNERS review requirement in the "
244+
f"'Protect version branches' ruleset at: {ruleset_url}"
245+
)
246+
247+
39248
def apply_patch(patch_content: str) -> None:
40249
"""Apply a patch using the patch utility."""
41250
subprocess.run(["patch", "-p1"], input=patch_content.encode(), check=True)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Auto-merge Dependabot PR
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
jobs:
11+
auto-merge:
12+
if: github.actor == 'dependabot[bot]'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Auto-merge Dependabot PR
16+
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
17+
with:
18+
github-token: {% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}
19+
dependency-type: 'all'
20+
auto-merge: 'true'
21+
merge-method: 'merge'
22+
add-label: 'tool:auto-merged'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Auto-merge Dependabot PR
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
jobs:
11+
auto-merge:
12+
if: github.actor == 'dependabot[bot]'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Auto-merge Dependabot PR
16+
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
dependency-type: 'all'
20+
auto-merge: 'true'
21+
merge-method: 'merge'
22+
add-label: 'tool:auto-merged'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Auto-merge Dependabot PR
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
jobs:
11+
auto-merge:
12+
if: github.actor == 'dependabot[bot]'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Auto-merge Dependabot PR
16+
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
dependency-type: 'all'
20+
auto-merge: 'true'
21+
merge-method: 'merge'
22+
add-label: 'tool:auto-merged'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Auto-merge Dependabot PR
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
jobs:
11+
auto-merge:
12+
if: github.actor == 'dependabot[bot]'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Auto-merge Dependabot PR
16+
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
dependency-type: 'all'
20+
auto-merge: 'true'
21+
merge-method: 'merge'
22+
add-label: 'tool:auto-merged'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Auto-merge Dependabot PR
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
jobs:
11+
auto-merge:
12+
if: github.actor == 'dependabot[bot]'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Auto-merge Dependabot PR
16+
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
dependency-type: 'all'
20+
auto-merge: 'true'
21+
merge-method: 'merge'
22+
add-label: 'tool:auto-merged'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Auto-merge Dependabot PR
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
jobs:
11+
auto-merge:
12+
if: github.actor == 'dependabot[bot]'
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Auto-merge Dependabot PR
16+
uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
dependency-type: 'all'
20+
auto-merge: 'true'
21+
merge-method: 'merge'
22+
add-label: 'tool:auto-merged'

0 commit comments

Comments
 (0)