|
21 | 21 | """ # noqa: E501 |
22 | 22 |
|
23 | 23 | import hashlib |
24 | | -import json |
25 | 24 | import os |
26 | 25 | import subprocess |
27 | 26 | import tempfile |
28 | 27 | from pathlib import Path |
29 | | -from typing import Any, SupportsIndex |
| 28 | +from typing import SupportsIndex |
30 | 29 |
|
31 | 30 |
|
32 | 31 | def main() -> None: |
33 | 32 | """Run the migration steps.""" |
34 | 33 | # Add a separation line like this one after each migration step. |
35 | 34 | 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) |
42 | | - print("Updating the mkdocs.yml for mkdocstrings-python v2 compatibility...") |
43 | | - update_mkdocs_yml_mkdocstrings_python_v2() |
44 | | - print("=" * 72) |
45 | 35 | print("Migration script finished. Remember to follow any manual instructions.") |
46 | 36 | print("=" * 72) |
47 | 37 |
|
48 | 38 |
|
49 | | -def update_mkdocs_yml_mkdocstrings_python_v2() -> None: |
50 | | - """Rename 'inventories' imports to 'inventory'.""" |
51 | | - replace_file_contents_atomically( |
52 | | - filepath=Path("mkdocs.yml"), |
53 | | - old=" import:", |
54 | | - new=" inventories:", |
55 | | - ) |
56 | | - replace_file_contents_atomically( |
57 | | - filepath=Path("mkdocs.yml"), |
58 | | - old="""\ |
59 | | - options: |
60 | | - paths: ["src"]""", |
61 | | - new="""\ |
62 | | - paths: ["src"] |
63 | | - options:""", |
64 | | - ) |
65 | | - |
66 | | - |
67 | | -def create_dependabot_auto_merge_workflow() -> None: |
68 | | - """Create the Dependabot auto-merge workflow file.""" |
69 | | - workflow_dir = Path(".github") / "workflows" |
70 | | - workflow_dir.mkdir(parents=True, exist_ok=True) |
71 | | - |
72 | | - workflow_content = """\ |
73 | | -name: Auto-merge Dependabot PR |
74 | | -
|
75 | | -on: |
76 | | - pull_request: |
77 | | -
|
78 | | -permissions: |
79 | | - contents: write |
80 | | - pull-requests: write |
81 | | -
|
82 | | -jobs: |
83 | | - auto-merge: |
84 | | - if: github.actor == 'dependabot[bot]' |
85 | | - runs-on: ubuntu-latest |
86 | | - steps: |
87 | | - - name: Auto-merge Dependabot PR |
88 | | - uses: frequenz-floss/dependabot-auto-approve@3cad5f42e79296505473325ac6636be897c8b8a1 # v1.3.2 |
89 | | - with: |
90 | | - github-token: ${{ secrets.GITHUB_TOKEN }} |
91 | | - dependency-type: 'all' |
92 | | - auto-merge: 'true' |
93 | | - merge-method: 'merge' |
94 | | - add-label: 'tool:auto-merged' |
95 | | -""" # noqa: E501 |
96 | | - |
97 | | - workflow_file = workflow_dir / "auto-dependabot.yaml" |
98 | | - workflow_file.write_text(workflow_content, encoding="utf-8") |
99 | | - print(f"Created/Updated Dependabot auto-merge workflow at {workflow_file}") |
100 | | - |
101 | | - |
102 | | -def get_default_branch() -> str | None: |
103 | | - """Get the default branch name from GitHub. |
104 | | -
|
105 | | - Returns: |
106 | | - The default branch name, or None if it cannot be determined. |
107 | | - """ |
108 | | - try: |
109 | | - result = subprocess.run( |
110 | | - ["gh", "api", "repos/:owner/:repo", "--jq", ".default_branch"], |
111 | | - capture_output=True, |
112 | | - text=True, |
113 | | - check=True, |
114 | | - ) |
115 | | - default_branch = result.stdout.strip() |
116 | | - print(f"Default branch: {default_branch}") |
117 | | - return default_branch |
118 | | - except subprocess.CalledProcessError as e: |
119 | | - print(f"Failed to get default branch: {e}") |
120 | | - return None |
121 | | - |
122 | | - |
123 | | -def find_version_branch_ruleset() -> dict[str, Any] | None: |
124 | | - """Find the 'Protect version branches' ruleset. |
125 | | -
|
126 | | - Returns: |
127 | | - The ruleset configuration, or None if not found. |
128 | | - """ |
129 | | - try: |
130 | | - result = subprocess.run( |
131 | | - ["gh", "api", "repos/:owner/:repo/rulesets"], |
132 | | - capture_output=True, |
133 | | - text=True, |
134 | | - check=True, |
135 | | - ) |
136 | | - rulesets = json.loads(result.stdout) |
137 | | - |
138 | | - for ruleset in rulesets: |
139 | | - if ruleset.get("name") == "Protect version branches": |
140 | | - return ruleset # type: ignore[no-any-return] |
141 | | - return None |
142 | | - except subprocess.CalledProcessError as e: |
143 | | - print(f"Failed to fetch rulesets: {e}") |
144 | | - return None |
145 | | - |
146 | | - |
147 | | -def update_ruleset(ruleset_id: int, ruleset_config: dict[str, Any]) -> bool: |
148 | | - """Update a GitHub ruleset configuration. |
149 | | -
|
150 | | - Args: |
151 | | - ruleset_id: The ID of the ruleset to update. |
152 | | - ruleset_config: The updated ruleset configuration. |
153 | | -
|
154 | | - Returns: |
155 | | - True if the update was successful, False otherwise. |
156 | | - """ |
157 | | - update_payload = { |
158 | | - "name": ruleset_config["name"], |
159 | | - "target": ruleset_config["target"], |
160 | | - "enforcement": ruleset_config["enforcement"], |
161 | | - "conditions": ruleset_config["conditions"], |
162 | | - "rules": ruleset_config["rules"], |
163 | | - } |
164 | | - |
165 | | - if "bypass_actors" in ruleset_config: |
166 | | - update_payload["bypass_actors"] = ruleset_config["bypass_actors"] |
167 | | - |
168 | | - with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: |
169 | | - json.dump(update_payload, f, indent=2) |
170 | | - temp_file = f.name |
171 | | - |
172 | | - try: |
173 | | - subprocess.run( |
174 | | - [ |
175 | | - "gh", |
176 | | - "api", |
177 | | - "-X", |
178 | | - "PUT", |
179 | | - f"repos/:owner/:repo/rulesets/{ruleset_id}", |
180 | | - "--input", |
181 | | - temp_file, |
182 | | - ], |
183 | | - capture_output=True, |
184 | | - check=True, |
185 | | - ) |
186 | | - return True |
187 | | - except subprocess.CalledProcessError as e: |
188 | | - print(f"Error updating ruleset: {e}") |
189 | | - return False |
190 | | - finally: |
191 | | - os.unlink(temp_file) |
192 | | - |
193 | | - |
194 | | -def disable_codeowners_review_requirement() -> None: |
195 | | - """Disable CODEOWNERS review requirement in GitHub repository ruleset.""" |
196 | | - # Get repository info |
197 | | - try: |
198 | | - result = subprocess.run( |
199 | | - ["gh", "repo", "view", "--json", "owner,name"], |
200 | | - capture_output=True, |
201 | | - text=True, |
202 | | - check=True, |
203 | | - ) |
204 | | - repo_info = json.loads(result.stdout) |
205 | | - org = repo_info["owner"]["login"] |
206 | | - repo = repo_info["name"] |
207 | | - ruleset_url = f"https://github.com/{org}/{repo}/settings/rules" |
208 | | - except subprocess.CalledProcessError: |
209 | | - ruleset_url = "GitHub repository settings > Rules" |
210 | | - |
211 | | - if get_default_branch() is None: |
212 | | - manual_step( |
213 | | - "Failed to get default branch. " |
214 | | - "Please manually disable the CODEOWNERS review requirement in the " |
215 | | - f"'Protect version branches' ruleset at: {ruleset_url}" |
216 | | - ) |
217 | | - return |
218 | | - |
219 | | - version_branch_ruleset = find_version_branch_ruleset() |
220 | | - if not version_branch_ruleset: |
221 | | - manual_step( |
222 | | - "'Protect version branches' ruleset not found. " |
223 | | - "Please manually disable the CODEOWNERS review requirement at: " |
224 | | - f"{ruleset_url}" |
225 | | - ) |
226 | | - return |
227 | | - |
228 | | - ruleset_id = version_branch_ruleset["id"] |
229 | | - print(f"Found ruleset ID: {ruleset_id}") |
230 | | - |
231 | | - try: |
232 | | - result = subprocess.run( |
233 | | - ["gh", "api", f"repos/:owner/:repo/rulesets/{ruleset_id}"], |
234 | | - capture_output=True, |
235 | | - text=True, |
236 | | - check=True, |
237 | | - ) |
238 | | - ruleset_config = json.loads(result.stdout) |
239 | | - except subprocess.CalledProcessError as e: |
240 | | - manual_step( |
241 | | - f"Failed to fetch ruleset configuration: {e}. " |
242 | | - "This action requires admin permissions. " |
243 | | - f"Please manually disable the CODEOWNERS review requirement at: {ruleset_url}" |
244 | | - ) |
245 | | - return |
246 | | - |
247 | | - updated = False |
248 | | - for rule in ruleset_config.get("rules", []): |
249 | | - if rule.get("type") == "pull_request": |
250 | | - if rule.get("parameters", {}).get("require_code_owner_review"): |
251 | | - rule["parameters"]["require_code_owner_review"] = False |
252 | | - updated = True |
253 | | - break |
254 | | - |
255 | | - if not updated: |
256 | | - print("CODEOWNERS review requirement already disabled.") |
257 | | - return |
258 | | - |
259 | | - if update_ruleset(ruleset_id, ruleset_config): |
260 | | - print("Successfully disabled CODEOWNERS review requirement in GitHub ruleset.") |
261 | | - else: |
262 | | - manual_step( |
263 | | - "Failed to update GitHub ruleset. This action requires admin permissions. " |
264 | | - "Please manually disable the CODEOWNERS review requirement in the " |
265 | | - f"'Protect version branches' ruleset at: {ruleset_url}" |
266 | | - ) |
267 | | - |
268 | | - |
269 | 39 | def apply_patch(patch_content: str) -> None: |
270 | 40 | """Apply a patch using the patch utility.""" |
271 | 41 | subprocess.run(["patch", "-p1"], input=patch_content.encode(), check=True) |
|
0 commit comments