Skip to content

Commit 5736687

Browse files
committed
Handle private repos in workflow migration
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. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
1 parent d9fa53d commit 5736687

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
@@ -55,6 +55,16 @@
5555
]
5656

5757

58+
_PRIVATE_REPO_CI_OPTIONAL_PATTERNS = {
59+
" permissions:\n contents: write\n",
60+
" python -m frequenz.repo.config.cli.version.mike.info\n",
61+
" run: |\n"
62+
' mike deploy --update-aliases --title "$TITLE" "$VERSION" '
63+
"$ALIASES\n",
64+
" python -m frequenz.repo.config.cli.version.mike.sort versions.json\n",
65+
}
66+
67+
5868
def main() -> None:
5969
"""Run the migration steps."""
6070
# Add a separation line like this one after each migration step.
@@ -92,6 +102,8 @@ def main() -> None:
92102

93103
def migrate_ci_workflows() -> None:
94104
"""Update the generated CI workflows to the latest template."""
105+
is_private_repo = has_private_repo_indicators()
106+
95107
_migrate_workflow_file(
96108
Path(".github/workflows/ci-pr.yaml"),
97109
[
@@ -120,8 +132,15 @@ def migrate_ci_workflows() -> None:
120132
description="updated CI pull-request workflow",
121133
)
122134

135+
ci_workflow = Path(".github/workflows/ci.yaml")
136+
private_ci_publish_jobs: list[str] = []
137+
if is_private_repo and ci_workflow.exists():
138+
private_ci_publish_jobs = find_ci_publish_jobs(
139+
_normalize_content(ci_workflow.read_text(encoding="utf-8"))
140+
)
141+
123142
_migrate_workflow_file(
124-
Path(".github/workflows/ci.yaml"),
143+
ci_workflow,
125144
[
126145
(
127146
" workflow_dispatch:\n\nenv:\n",
@@ -247,8 +266,19 @@ def migrate_ci_workflows() -> None:
247266
),
248267
],
249268
description="updated main CI workflow",
269+
ignore_missing_patterns=(
270+
_PRIVATE_REPO_CI_OPTIONAL_PATTERNS if is_private_repo else None
271+
),
250272
)
251273

274+
if is_private_repo and private_ci_publish_jobs:
275+
jobs = ", ".join(f"`{job}`" for job in private_ci_publish_jobs)
276+
manual_step(
277+
f"{ci_workflow} still contains {jobs}. This repository appears to be "
278+
"private, so those publish jobs usually should be removed manually "
279+
"after the migration, even though the workflow was updated."
280+
)
281+
252282

253283
def migrate_dependabot_workflows() -> None:
254284
"""Update the generated Dependabot automation workflows."""
@@ -444,6 +474,7 @@ def _migrate_workflow_file(
444474
replacements: list[tuple[str, str]],
445475
*,
446476
description: str,
477+
ignore_missing_patterns: set[str] | None = None,
447478
) -> None:
448479
"""Apply text replacements to a generated workflow file.
449480
@@ -463,6 +494,12 @@ def _migrate_workflow_file(
463494

464495
updated = _pin_workflow_action_references(content)
465496
updated, missing_patterns = _apply_idempotent_replacements(updated, replacements)
497+
if ignore_missing_patterns:
498+
missing_patterns = [
499+
pattern
500+
for pattern in missing_patterns
501+
if pattern not in ignore_missing_patterns
502+
]
466503

467504
if updated == content:
468505
if not missing_patterns:
@@ -734,6 +771,49 @@ def read_cookiecutter_str_var(name: str) -> str | None:
734771
return value
735772

736773

774+
def has_private_repo_indicators() -> bool:
775+
"""Return whether the repository appears to be private."""
776+
github_org = read_cookiecutter_str_var("github_org")
777+
if github_org is not None and github_org != "frequenz-floss":
778+
return True
779+
780+
license_name = read_cookiecutter_str_var("license")
781+
if license_name == "Proprietary":
782+
return True
783+
784+
pyproject_path = Path("pyproject.toml")
785+
if pyproject_path.exists():
786+
pyproject = _normalize_content(pyproject_path.read_text(encoding="utf-8"))
787+
if 'license = "LicenseRef-Proprietary"' in pyproject:
788+
return True
789+
790+
try:
791+
stdout = subprocess.check_output(
792+
["gh", "repo", "view", "--json", "owner"],
793+
text=True,
794+
stderr=subprocess.PIPE,
795+
)
796+
except (FileNotFoundError, subprocess.CalledProcessError):
797+
return False
798+
799+
try:
800+
info: dict[str, Any] = json.loads(stdout)
801+
owner = info["owner"]["login"]
802+
except (KeyError, TypeError, json.JSONDecodeError):
803+
return False
804+
805+
return owner != "frequenz-floss"
806+
807+
808+
def find_ci_publish_jobs(content: str) -> list[str]:
809+
"""Return CI publish job names found in the workflow content."""
810+
jobs: list[str] = []
811+
for job_name in ("publish-docs", "publish-to-pypi"):
812+
if f"\n {job_name}:\n" in f"\n{content}":
813+
jobs.append(job_name)
814+
return jobs
815+
816+
737817
def manual_step(message: str) -> None:
738818
"""Print a manual step message in yellow."""
739819
_manual_steps.append(message)

0 commit comments

Comments
 (0)