Skip to content

Commit 1dd1663

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 e13eb70 commit 1dd1663

18 files changed

Lines changed: 792 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: 141 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:
@@ -495,6 +498,144 @@ def migrate_auto_dependabot_token() -> None:
495498
print(f" Added {filepath}: installed updated workflow")
496499

497500

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

0 commit comments

Comments
 (0)