Skip to content

Commit eec17e3

Browse files
authored
Handle private repos in workflow migration (#548)
Treat the docs and PyPI publish patterns in ci.yaml as optional when the repository appears to be private, so migrations do not fail just because those jobs were removed intentionally. When those publish jobs are still present in a private repository, warn with a manual step so maintainers can remove them after applying the workflow updates.
2 parents ce994a3 + f50a702 commit eec17e3

1 file changed

Lines changed: 81 additions & 1 deletion

File tree

cookiecutter/migrate.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@
151151
]
152152

153153

154+
_PRIVATE_REPO_CI_OPTIONAL_PATTERNS = {
155+
" permissions:\n contents: write\n",
156+
" python -m frequenz.repo.config.cli.version.mike.info\n",
157+
" run: |\n"
158+
' mike deploy --update-aliases --title "$TITLE" "$VERSION" '
159+
"$ALIASES\n",
160+
" python -m frequenz.repo.config.cli.version.mike.sort versions.json\n",
161+
}
162+
163+
154164
def main() -> None:
155165
"""Run the migration steps."""
156166
# Add a separation line like this one after each migration step.
@@ -191,6 +201,8 @@ def main() -> None:
191201

192202
def migrate_ci_workflows() -> None:
193203
"""Update the generated CI workflows to the latest template."""
204+
is_private_repo = has_private_repo_indicators()
205+
194206
_migrate_workflow_file(
195207
Path(".github/workflows/ci-pr.yaml"),
196208
[
@@ -219,8 +231,15 @@ def migrate_ci_workflows() -> None:
219231
description="updated CI pull-request workflow",
220232
)
221233

234+
ci_workflow = Path(".github/workflows/ci.yaml")
235+
private_ci_publish_jobs: list[str] = []
236+
if is_private_repo and ci_workflow.exists():
237+
private_ci_publish_jobs = find_ci_publish_jobs(
238+
_normalize_content(ci_workflow.read_text(encoding="utf-8"))
239+
)
240+
222241
_migrate_workflow_file(
223-
Path(".github/workflows/ci.yaml"),
242+
ci_workflow,
224243
[
225244
(
226245
" workflow_dispatch:\n\nenv:\n",
@@ -346,8 +365,19 @@ def migrate_ci_workflows() -> None:
346365
),
347366
],
348367
description="updated main CI workflow",
368+
ignore_missing_patterns=(
369+
_PRIVATE_REPO_CI_OPTIONAL_PATTERNS if is_private_repo else None
370+
),
349371
)
350372

373+
if is_private_repo and private_ci_publish_jobs:
374+
jobs = ", ".join(f"`{job}`" for job in private_ci_publish_jobs)
375+
manual_step(
376+
f"{ci_workflow} still contains {jobs}. This repository appears to be "
377+
"private, so those publish jobs usually should be removed manually "
378+
"after the migration, even though the workflow was updated."
379+
)
380+
351381

352382
def migrate_dependabot_workflows() -> None:
353383
"""Update the generated Dependabot automation workflows."""
@@ -561,6 +591,7 @@ def _migrate_workflow_file(
561591
replacements: list[tuple[str, str]],
562592
*,
563593
description: str,
594+
ignore_missing_patterns: set[str] | None = None,
564595
) -> None:
565596
"""Apply text replacements to a generated workflow file.
566597
@@ -580,6 +611,12 @@ def _migrate_workflow_file(
580611

581612
updated = _pin_workflow_action_references(content)
582613
updated, missing_patterns = _apply_idempotent_replacements(updated, replacements)
614+
if ignore_missing_patterns:
615+
missing_patterns = [
616+
pattern
617+
for pattern in missing_patterns
618+
if pattern not in ignore_missing_patterns
619+
]
583620

584621
if updated == content:
585622
if not missing_patterns:
@@ -851,6 +888,49 @@ def read_cookiecutter_str_var(name: str) -> str | None:
851888
return value
852889

853890

891+
def has_private_repo_indicators() -> bool:
892+
"""Return whether the repository appears to be private."""
893+
github_org = read_cookiecutter_str_var("github_org")
894+
if github_org is not None and github_org != "frequenz-floss":
895+
return True
896+
897+
license_name = read_cookiecutter_str_var("license")
898+
if license_name == "Proprietary":
899+
return True
900+
901+
pyproject_path = Path("pyproject.toml")
902+
if pyproject_path.exists():
903+
pyproject = _normalize_content(pyproject_path.read_text(encoding="utf-8"))
904+
if 'license = "LicenseRef-Proprietary"' in pyproject:
905+
return True
906+
907+
try:
908+
stdout = subprocess.check_output(
909+
["gh", "repo", "view", "--json", "owner"],
910+
text=True,
911+
stderr=subprocess.PIPE,
912+
)
913+
except (FileNotFoundError, subprocess.CalledProcessError):
914+
return False
915+
916+
try:
917+
info: dict[str, Any] = json.loads(stdout)
918+
owner: str = info["owner"]["login"]
919+
except (KeyError, TypeError, json.JSONDecodeError):
920+
return False
921+
922+
return owner != "frequenz-floss"
923+
924+
925+
def find_ci_publish_jobs(content: str) -> list[str]:
926+
"""Return CI publish job names found in the workflow content."""
927+
jobs: list[str] = []
928+
for job_name in ("publish-docs", "publish-to-pypi"):
929+
if f"\n {job_name}:\n" in f"\n{content}":
930+
jobs.append(job_name)
931+
return jobs
932+
933+
854934
def manual_step(message: str) -> None:
855935
"""Print a manual step message in yellow."""
856936
_manual_steps.append(message)

0 commit comments

Comments
 (0)