Skip to content

Commit 83bec3e

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 83bec3e

File tree

8 files changed

+371
-1
lines changed

8 files changed

+371
-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: 211 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,231 @@
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 as e:
168+
print(f"Error updating ruleset: {e}")
169+
return False
170+
finally:
171+
os.unlink(temp_file)
172+
173+
174+
def disable_codeowners_review_requirement() -> None:
175+
"""Disable CODEOWNERS review requirement in GitHub repository ruleset."""
176+
# Get repository info
177+
try:
178+
result = subprocess.run(
179+
["gh", "repo", "view", "--json", "owner,name"],
180+
capture_output=True,
181+
text=True,
182+
check=True,
183+
)
184+
repo_info = json.loads(result.stdout)
185+
org = repo_info["owner"]["login"]
186+
repo = repo_info["name"]
187+
ruleset_url = f"https://github.com/{org}/{repo}/settings/rules"
188+
except subprocess.CalledProcessError:
189+
ruleset_url = "GitHub repository settings > Rules"
190+
191+
if get_default_branch() is None:
192+
manual_step(
193+
"Failed to get default branch. "
194+
"Please manually disable the CODEOWNERS review requirement in the "
195+
f"'Protect version branches' ruleset at: {ruleset_url}"
196+
)
197+
return
198+
199+
version_branch_ruleset = find_version_branch_ruleset()
200+
if not version_branch_ruleset:
201+
manual_step(
202+
"'Protect version branches' ruleset not found. "
203+
"Please manually disable the CODEOWNERS review requirement at: "
204+
f"{ruleset_url}"
205+
)
206+
return
207+
208+
ruleset_id = version_branch_ruleset["id"]
209+
print(f"Found ruleset ID: {ruleset_id}")
210+
211+
try:
212+
result = subprocess.run(
213+
["gh", "api", f"repos/:owner/:repo/rulesets/{ruleset_id}"],
214+
capture_output=True,
215+
text=True,
216+
check=True,
217+
)
218+
ruleset_config = json.loads(result.stdout)
219+
except subprocess.CalledProcessError as e:
220+
manual_step(
221+
f"Failed to fetch ruleset configuration: {e}. "
222+
"This action requires admin permissions. "
223+
f"Please manually disable the CODEOWNERS review requirement at: {ruleset_url}"
224+
)
225+
return
226+
227+
updated = False
228+
for rule in ruleset_config.get("rules", []):
229+
if rule.get("type") == "pull_request":
230+
if rule.get("parameters", {}).get("require_code_owner_review"):
231+
rule["parameters"]["require_code_owner_review"] = False
232+
updated = True
233+
break
234+
235+
if not updated:
236+
print("CODEOWNERS review requirement already disabled.")
237+
return
238+
239+
if update_ruleset(ruleset_id, ruleset_config):
240+
print("Successfully disabled CODEOWNERS review requirement in GitHub ruleset.")
241+
else:
242+
manual_step(
243+
"Failed to update GitHub ruleset. This action requires admin permissions. "
244+
"Please manually disable the CODEOWNERS review requirement in the "
245+
f"'Protect version branches' ruleset at: {ruleset_url}"
246+
)
247+
248+
39249
def apply_patch(patch_content: str) -> None:
40250
"""Apply a patch using the patch utility."""
41251
subprocess.run(["patch", "-p1"], input=patch_content.encode(), check=True)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{% raw %}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'
23+
{% endraw %}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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'
23+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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'
23+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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'
23+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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'
23+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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'
23+

0 commit comments

Comments
 (0)