Skip to content

Commit cd6aa40

Browse files
committed
Add repo-config migration workflow
Add a new workflow that automatically runs the migration script when Dependabot opens a PR for the repo-config group. It handles multi-version jumps by running each intermediate migration in sequence, posts the migration output as a PR comment and in the job summary, and auto-approves/merges clean migrations. PRs that need manual intervention fail the job until a human signals resolution via the `tool:repo-config:migration:intervention-done` label (or by removing the `tool:repo-config:migration:intervention-pending` label). Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
1 parent 7d4aa0e commit cd6aa40

18 files changed

Lines changed: 800 additions & 36 deletions

File tree

RELEASE_NOTES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
This release migrates lightweight GitHub Actions workflow jobs to use the new cost-effective `ubuntu-slim` runner.
66
It also updates cookiecutter pyproject license metadata to SPDX expressions to avoid setuptools deprecation warnings.
77
The auto-dependabot workflow now uses a GitHub App installation token instead of `GITHUB_TOKEN` to fix merge queue and auto-merge failures.
8+
Finally, it adds an automated repo-config migration workflow that runs migration scripts on Dependabot PRs.
89

910
## Upgrading
1011

@@ -41,6 +42,12 @@ But you might still need to adapt your code:
4142

4243
- The CI workflow now uses a simpler matrix.
4344

45+
- Added `repo-config-migration.yaml` workflow that automatically runs the migration script, commits changes, posts results, and auto-approves/merges only when no migration commit is created.
46+
47+
The workflow handles multi-version jumps by running each intermediate migration in sequence. The migration script output is posted as a PR comment and in the job summary. PRs with migration commits stay open for manual approval and merge. PRs that need manual intervention fail the job until a human completes the steps and signals resolution by removing the `tool:repo-config:migration:intervention-pending` label or adding the `tool:repo-config:migration:intervention-done` label.
48+
49+
- The `auto-dependabot.yaml` workflow now skips repo-config group PRs, which are handled by the new migration workflow instead.
50+
4451
## Bug Fixes
4552

4653
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->

cookiecutter/migrate.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ def main() -> None:
5555
print("Migrating the CI workflows to use a platform matrix...")
5656
migrate_platform_matrix()
5757
print("=" * 72)
58+
print("Installing repo-config migration workflow...")
59+
migrate_repo_config_workflow()
60+
print("=" * 72)
5861
print()
5962

6063
if _manual_steps:
@@ -505,6 +508,145 @@ def migrate_auto_dependabot_token() -> None:
505508
print(f" Added {filepath}: installed updated workflow")
506509

507510

511+
def migrate_repo_config_workflow() -> None:
512+
"""Install the repo-config migration workflow and update auto-dependabot.
513+
514+
This installs the ``repo-config-migration.yaml`` workflow that uses the
515+
``frequenz-floss/gh-action-dependabot-migrate`` action. It also
516+
updates ``auto-dependabot.yaml`` to skip repo-config group PRs (which
517+
are handled by the migration workflow instead).
518+
519+
The workflow file is created from scratch (overwriting any previous
520+
version) to ensure it stays in sync with the latest template.
521+
"""
522+
workflows_dir = Path(".github") / "workflows"
523+
if not workflows_dir.is_dir():
524+
print(" Skipping (no .github/workflows directory found)")
525+
return
526+
527+
# ── Install repo-config-migration.yaml ────────────────────────────
528+
migration_wf = workflows_dir / "repo-config-migration.yaml"
529+
desired_content = (
530+
r"""# Automatically repo-config migrations for Dependabot PRs
531+
#
532+
# The companion auto-dependabot workflow skips repo-config group PRs so
533+
# they're handled exclusively by the migration workflow.
534+
#
535+
# XXX: !!! SECURITY WARNING !!!
536+
# pull_request_target has write access to the repo, and can read secrets.
537+
# This is required because Dependabot PRs are treated as fork PRs: the
538+
# GITHUB_TOKEN is read-only and secrets are unavailable with a plain
539+
# pull_request trigger. The action mitigates the risk by:
540+
# - Never executing code from the PR (migrate.py is fetched from an
541+
# upstream tag, not from the checked-out branch).
542+
# - Gating migration steps on github.actor == 'dependabot[bot]'.
543+
# - Running checkout with persist-credentials: false and isolating
544+
# push credentials from the migration script environment.
545+
# For more details read:
546+
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
547+
548+
name: Repo Config Migration
549+
550+
on:
551+
pull_request_target:
552+
types: [opened, synchronize, reopened, labeled, unlabeled]
553+
554+
permissions:
555+
contents: write
556+
issues: write
557+
pull-requests: write
558+
559+
jobs:
560+
repo-config-migration:
561+
name: Migrate Repo Config
562+
if: contains(github.event.pull_request.title, 'the repo-config group')
563+
runs-on: ubuntu-24.04
564+
steps:
565+
- name: Generate token
566+
id: create-app-token
567+
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
568+
with:
569+
app-id: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_ID }}
570+
private-key: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_PRIVATE_KEY }}
571+
- name: Migrate
572+
uses: frequenz-floss/gh-action-dependabot-migrate@07dc7e74726498c50726a80cc2167a04d896508f # v1.0.0
573+
with:
574+
script-url-template: >-
575+
https://raw.githubusercontent.com/frequenz-floss/frequenz-repo-config-python/{version}/cookiecutter/migrate.py""" # noqa: E501
576+
r"""
577+
token: ${{ steps.create-app-token.outputs.token }}
578+
migration-token: ${{ secrets.REPO_CONFIG_MIGRATION_TOKEN }}
579+
sign-commits: "true"
580+
auto-merged-label: "tool:auto-merged"
581+
migrated-label: "tool:repo-config:migration:executed"
582+
intervention-pending-label: "tool:repo-config:migration:intervention-pending"
583+
intervention-done-label: "tool:repo-config:migration:intervention-done"
584+
"""
585+
)
586+
587+
if migration_wf.exists():
588+
content = migration_wf.read_text(encoding="utf-8").replace("\r\n", "\n")
589+
if content == desired_content:
590+
print(f" Skipped {migration_wf}: already up to date")
591+
else:
592+
print(
593+
f" Replacing {migration_wf} with updated workflow"
594+
" (overwriting any local changes)"
595+
)
596+
replace_file_atomically(migration_wf, desired_content)
597+
else:
598+
workflows_dir.mkdir(parents=True, exist_ok=True)
599+
replace_file_atomically(migration_wf, desired_content)
600+
print(f" Installed {migration_wf}")
601+
602+
# ── Update auto-dependabot.yaml ───────────────────────────────────
603+
#
604+
# Add a condition to skip repo-config group PRs, which are now
605+
# handled by the migration workflow instead.
606+
auto_dep = workflows_dir / "auto-dependabot.yaml"
607+
if not auto_dep.exists():
608+
print(f" Skipping {auto_dep} (file not found)")
609+
return
610+
611+
dep_content = auto_dep.read_text(encoding="utf-8")
612+
613+
# Already has the exclusion condition.
614+
if "the repo-config group" in dep_content:
615+
print(f" Skipped {auto_dep} (already excludes repo-config group)")
616+
return
617+
618+
# Match both multi-line and single-line `if` formats, with any runner.
619+
old_patterns = [
620+
# Multi-line if (e.g. from a previous migration that used ubuntu-slim)
621+
(" if: github.actor == 'dependabot[bot]'\n runs-on: ubuntu-slim"),
622+
(" if: github.actor == 'dependabot[bot]'\n runs-on: ubuntu-latest"),
623+
(" if: github.actor == 'dependabot[bot]'\n runs-on: ubuntu-24.04"),
624+
]
625+
626+
new_template = (
627+
" if: >\n"
628+
" github.actor == 'dependabot[bot]' &&\n"
629+
" !contains(github.event.pull_request.title, 'the repo-config group')\n"
630+
" runs-on: {runner}"
631+
)
632+
633+
for old_pattern in old_patterns:
634+
if old_pattern in dep_content:
635+
# Extract the runner from the old pattern.
636+
runner = old_pattern.rsplit("runs-on: ", 1)[1]
637+
new_block = new_template.format(runner=runner)
638+
replace_file_contents_atomically(auto_dep, old_pattern, new_block)
639+
print(f" Updated {auto_dep}: added repo-config group exclusion")
640+
return
641+
642+
# If we didn't match any known pattern, flag a manual step.
643+
manual_step(
644+
f"Could not update {auto_dep} automatically. Please add a condition "
645+
"to skip repo-config group PRs: "
646+
"`!contains(github.event.pull_request.title, 'the repo-config group')`"
647+
)
648+
649+
508650
def read_project_type() -> str | None:
509651
"""Read the cookiecutter project type from the replay file."""
510652
replay_path = Path(".cookiecutter-replay.json")

cookiecutter/{{cookiecutter.github_repo_name}}/.github/workflows/auto-dependabot.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ permissions:
1919
jobs:
2020
auto-merge:
2121
name: Auto-merge Dependabot PR
22-
if: github.actor == 'dependabot[bot]'
22+
if: |
23+
github.actor == 'dependabot[bot]' &&
24+
!contains(github.event.pull_request.title, 'the repo-config group')
2325
runs-on: ubuntu-slim
2426
steps:
2527
- name: Generate GitHub App token
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{% raw -%}
2+
# Automatically repo-config migrations for Dependabot PRs
3+
#
4+
# The companion auto-dependabot workflow skips repo-config group PRs so
5+
# they're handled exclusively by the migration workflow.
6+
#
7+
# XXX: !!! SECURITY WARNING !!!
8+
# pull_request_target has write access to the repo, and can read secrets.
9+
# This is required because Dependabot PRs are treated as fork PRs: the
10+
# GITHUB_TOKEN is read-only and secrets are unavailable with a plain
11+
# pull_request trigger. The action mitigates the risk by:
12+
# - Never executing code from the PR (migrate.py is fetched from an
13+
# upstream tag, not from the checked-out branch).
14+
# - Gating migration steps on github.actor == 'dependabot[bot]'.
15+
# - Running checkout with persist-credentials: false and isolating
16+
# push credentials from the migration script environment.
17+
# For more details read:
18+
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
19+
20+
name: Repo Config Migration
21+
22+
on:
23+
pull_request_target:
24+
types: [opened, synchronize, reopened, labeled, unlabeled]
25+
26+
permissions:
27+
contents: write
28+
issues: write
29+
pull-requests: write
30+
31+
jobs:
32+
repo-config-migration:
33+
name: Migrate Repo Config
34+
if: contains(github.event.pull_request.title, 'the repo-config group')
35+
runs-on: ubuntu-24.04
36+
steps:
37+
- name: Generate token
38+
id: create-app-token
39+
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
40+
with:
41+
app-id: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_ID }}
42+
private-key: ${{ secrets.FREQUENZ_AUTO_DEPENDABOT_APP_PRIVATE_KEY }}
43+
- name: Migrate
44+
uses: frequenz-floss/gh-action-dependabot-migrate@07dc7e74726498c50726a80cc2167a04d896508f # v1.0.0
45+
with:
46+
script-url-template: >-
47+
https://raw.githubusercontent.com/frequenz-floss/frequenz-repo-config-python/{version}/cookiecutter/migrate.py
48+
token: ${{ steps.create-app-token.outputs.token }}
49+
migration-token: ${{ secrets.REPO_CONFIG_MIGRATION_TOKEN }}
50+
sign-commits: "true"
51+
auto-merged-label: "tool:auto-merged"
52+
migrated-label: "tool:repo-config:migration:executed"
53+
intervention-pending-label: "tool:repo-config:migration:intervention-pending"
54+
intervention-done-label: "tool:repo-config:migration:intervention-done"
55+
{%- endraw %}

0 commit comments

Comments
 (0)