From b055eece05efe352bc72e2cdf5a8cdd6866dbe6e Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 19 Oct 2026 16:50:05 +0200 Subject: [PATCH] feat(ci): keep GitHub-hosted runner labels current through the updater MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflows pin `runs-on: ubuntu-NN.NN`, so no job moves to a new Ubuntu release behind our back, but nothing raised the pin either. A sixth job of `๐Ÿ”„ Update versions` closes that: `utils/update/runner.py` reads the image table of actions/runner-images, takes the newest x64 Ubuntu label whose image carries no preview or beta badge, and raises every older bare `runs-on: ubuntu-NN.NN` to it; arch-suffixed labels stay untouched. The job opens the change as one PR over `.github/workflows`, so its CI run tests the new image before it reaches main. This commit carries the updater's first result: every job now runs on ubuntu-26.04 (49 `runs-on`). actionlint 1.7.12, the newest release, does not know `ubuntu-26.04` yet (rhysd/actionlint#682, fix proposed in rhysd/actionlint#743), so `.github/actionlint.yaml` declares the label until a release knows it. act maps each `ubuntu-NN.NN` label the workflows use to the local platform image, derived from `.github/workflows`, instead of a fixed list that did not know ubuntu-26.04. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/actionlint.yaml | 3 + .github/workflows/call-images-build-ci.yml | 2 +- .../workflows/call-images-mirror-missing.yml | 2 +- .github/workflows/call-lint.yml | 2 +- .github/workflows/call-orchestrator.yml | 4 +- .github/workflows/call-release-version.yml | 4 +- .github/workflows/call-test-deploy.yml | 4 +- .github/workflows/call-test-dns.yml | 2 +- .github/workflows/call-test-install-make.yml | 2 +- .../workflows/call-test-install-pkgmgr.yml | 2 +- .github/workflows/call-test-workspace.yml | 4 +- .github/workflows/call-test.yml | 2 +- .github/workflows/call-validation.yml | 6 +- .github/workflows/cron-cleanup-stale.yml | 6 +- .github/workflows/cron-collection-source.yml | 2 +- .github/workflows/cron-images-cleanup-ci.yml | 2 +- .github/workflows/cron-images-mirror-all.yml | 2 +- .github/workflows/cron-release-highest.yml | 2 +- .github/workflows/cron-security-codeql.yml | 2 +- .github/workflows/cron-security-fuzz.yml | 2 +- .github/workflows/cron-security-scorecard.yml | 2 +- .github/workflows/cron-update.yml | 52 +++++++++++-- .github/workflows/entry-cancel-superseded.yml | 4 +- .github/workflows/entry-delete-branch.yml | 2 +- .../workflows/entry-manual-mirror-cleanup.yml | 2 +- .../workflows/entry-pr-change-orchestrate.yml | 8 +- .../entry-pr-closed-cancel-workflows.yml | 2 +- .../entry-pr-open-dependabot-close.yml | 2 +- .github/workflows/entry-push-latest.yml | 10 +-- cli/contributing/update/runner/__init__.py | 5 ++ cli/contributing/update/runner/__main__.py | 46 +++++++++++ docs/contributing/actions/debugging/ci.md | 6 +- scripts/github/update/runner_labels.sh | 8 ++ scripts/tests/deploy/act/workflow.sh | 8 +- tests/unit/python/utils/update/test_runner.py | 71 +++++++++++++++++ utils/update/runner.py | 77 +++++++++++++++++++ 36 files changed, 307 insertions(+), 55 deletions(-) create mode 100644 .github/actionlint.yaml create mode 100644 cli/contributing/update/runner/__init__.py create mode 100755 cli/contributing/update/runner/__main__.py create mode 100755 scripts/github/update/runner_labels.sh create mode 100644 tests/unit/python/utils/update/test_runner.py create mode 100644 utils/update/runner.py diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..978712b9f1 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - ubuntu-26.04 diff --git a/.github/workflows/call-images-build-ci.yml b/.github/workflows/call-images-build-ci.yml index e88665756a..50e9aef161 100644 --- a/.github/workflows/call-images-build-ci.yml +++ b/.github/workflows/call-images-build-ci.yml @@ -43,7 +43,7 @@ concurrency: jobs: build: name: ๐Ÿ’ฟ Image - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-images-mirror-missing.yml b/.github/workflows/call-images-mirror-missing.yml index 1fe1e5e252..0c6fa3bc08 100644 --- a/.github/workflows/call-images-mirror-missing.yml +++ b/.github/workflows/call-images-mirror-missing.yml @@ -78,7 +78,7 @@ concurrency: jobs: sync: name: ๐Ÿชž Mirror - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-lint.yml b/.github/workflows/call-lint.yml index fce0906703..679feb93f1 100644 --- a/.github/workflows/call-lint.yml +++ b/.github/workflows/call-lint.yml @@ -10,7 +10,7 @@ permissions: jobs: lint: name: ๐Ÿงน Lint - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 45 env: INFINITO_LINT_RUNNER: host diff --git a/.github/workflows/call-orchestrator.yml b/.github/workflows/call-orchestrator.yml index 48453f6ab3..c00c41ab38 100644 --- a/.github/workflows/call-orchestrator.yml +++ b/.github/workflows/call-orchestrator.yml @@ -343,7 +343,7 @@ jobs: - test-deploy-chunk-2 - test-deploy-chunk-3 if: ${{ always() && github.ref == 'refs/heads/main' && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: issues: write actions: read @@ -374,7 +374,7 @@ jobs: - lint - security-codeql if: ${{ always() }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ๐Ÿ›‘ Fail if any upstream job failed or was cancelled if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} diff --git a/.github/workflows/call-release-version.yml b/.github/workflows/call-release-version.yml index e16cfea5ca..3410e44a35 100644 --- a/.github/workflows/call-release-version.yml +++ b/.github/workflows/call-release-version.yml @@ -30,7 +30,7 @@ env: jobs: check-ci-images: name: ๐Ÿ”Ž Check CI images - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: missing: ${{ steps.check.outputs.missing }} ci_tag: ${{ steps.citag.outputs.ci_tag }} @@ -123,7 +123,7 @@ jobs: needs.build-ci-images-if-missing.result == 'success' || needs.build-ci-images-if-missing.result == 'skipped' ) - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/call-test-deploy.yml b/.github/workflows/call-test-deploy.yml index 9fb7d25eff..ca454e0e46 100644 --- a/.github/workflows/call-test-deploy.yml +++ b/.github/workflows/call-test-deploy.yml @@ -163,7 +163,7 @@ env: jobs: discover: name: ๐Ÿ” Chunk ${{ inputs.index }}${{ inputs.marker }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 60 outputs: apps: ${{ steps.discover.outputs.apps }} @@ -251,7 +251,7 @@ jobs: deploy: name: "${{ matrix.label }}" if: needs.discover.outputs.apps != '' && needs.discover.outputs.apps != '[]' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 355 needs: discover diff --git a/.github/workflows/call-test-dns.yml b/.github/workflows/call-test-dns.yml index b45c0aacca..d993811a4b 100644 --- a/.github/workflows/call-test-dns.yml +++ b/.github/workflows/call-test-dns.yml @@ -23,7 +23,7 @@ permissions: jobs: dns: name: ๐ŸŒ DNS - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - name: ๐Ÿงน Free disk space diff --git a/.github/workflows/call-test-install-make.yml b/.github/workflows/call-test-install-make.yml index 36dcf5444d..080b6fcd22 100644 --- a/.github/workflows/call-test-install-make.yml +++ b/.github/workflows/call-test-install-make.yml @@ -10,7 +10,7 @@ permissions: jobs: test-install-make: name: ๐Ÿ› ๏ธ Makefile - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 env: NIX_CONFIG: | diff --git a/.github/workflows/call-test-install-pkgmgr.yml b/.github/workflows/call-test-install-pkgmgr.yml index e37e2281ea..688e5ce28a 100644 --- a/.github/workflows/call-test-install-pkgmgr.yml +++ b/.github/workflows/call-test-install-pkgmgr.yml @@ -10,7 +10,7 @@ permissions: jobs: test-install-pkgmgr: name: ๐Ÿ“ฆ PKGMGR - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 env: NIX_CONFIG: | diff --git a/.github/workflows/call-test-workspace.yml b/.github/workflows/call-test-workspace.yml index f1021a8366..4c748b56e4 100644 --- a/.github/workflows/call-test-workspace.yml +++ b/.github/workflows/call-test-workspace.yml @@ -21,7 +21,7 @@ permissions: jobs: prepare: name: ๐Ÿง‘โ€๐Ÿ’ป Workspace Matrix - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 5 outputs: matrix: ${{ steps.matrix.outputs.matrix }} @@ -44,7 +44,7 @@ jobs: test-workspace: name: ${{ matrix.icon }} Workspace (${{ matrix.label }}) - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 345 needs: prepare diff --git a/.github/workflows/call-test.yml b/.github/workflows/call-test.yml index 47ed98878e..ae113752e1 100644 --- a/.github/workflows/call-test.yml +++ b/.github/workflows/call-test.yml @@ -10,7 +10,7 @@ permissions: jobs: test: name: ๐Ÿงช Test - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 45 env: INFINITO_TEST_RUNNER: host diff --git a/.github/workflows/call-validation.yml b/.github/workflows/call-validation.yml index 6ff6dc2715..05e395339b 100644 --- a/.github/workflows/call-validation.yml +++ b/.github/workflows/call-validation.yml @@ -84,7 +84,7 @@ permissions: jobs: validation: name: โœ… Validation - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: โฌ‡๏ธ Checkout the calling ref for its tooling uses: actions/checkout@v7.0.0 @@ -148,7 +148,7 @@ jobs: wait-fork-prereq-run: name: โณ Wait for fork run if: ${{ inputs.fork_prereqs }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 env: WAIT_REQUIRED: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository && !inputs.trusted_pr }} steps: @@ -194,7 +194,7 @@ jobs: name: ๐Ÿšฆ Gate Dependencies needs: wait-fork-prereq-run if: ${{ !cancelled() && inputs.fork_prereqs }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v7.0.0 - name: โœ… Assert privileged prerequisites are ready diff --git a/.github/workflows/cron-cleanup-stale.yml b/.github/workflows/cron-cleanup-stale.yml index d340ec5255..68a1a1cf4c 100644 --- a/.github/workflows/cron-cleanup-stale.yml +++ b/.github/workflows/cron-cleanup-stale.yml @@ -12,7 +12,7 @@ permissions: jobs: stale: name: ๐Ÿท๏ธ Close inactive issues and pull requests - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: issues: write pull-requests: write @@ -45,7 +45,7 @@ jobs: delete-stale-branches: name: ๐ŸŒฟ Delete inactive branches - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: write issues: write @@ -65,7 +65,7 @@ jobs: cleanup-ghcr-ci-images: name: ๐Ÿ“ฆ Delete old GHCR CI images - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/cron-collection-source.yml b/.github/workflows/cron-collection-source.yml index be31adc04b..877415b27e 100644 --- a/.github/workflows/cron-collection-source.yml +++ b/.github/workflows/cron-collection-source.yml @@ -18,7 +18,7 @@ jobs: benchmark-collection-sources: name: โš–๏ธ Time Galaxy against the git fallback if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 env: HISTORY_PATH: build/collection-source-history.json steps: diff --git a/.github/workflows/cron-images-cleanup-ci.yml b/.github/workflows/cron-images-cleanup-ci.yml index 57f076f6f4..bdde2e46bd 100644 --- a/.github/workflows/cron-images-cleanup-ci.yml +++ b/.github/workflows/cron-images-cleanup-ci.yml @@ -22,7 +22,7 @@ concurrency: jobs: cleanup: name: ๐Ÿงน Delete old ci-* image versions - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: read contents: read diff --git a/.github/workflows/cron-images-mirror-all.yml b/.github/workflows/cron-images-mirror-all.yml index 16427fd220..3f1619f873 100644 --- a/.github/workflows/cron-images-mirror-all.yml +++ b/.github/workflows/cron-images-mirror-all.yml @@ -61,7 +61,7 @@ concurrency: jobs: sync: name: ๐Ÿชž Mirror - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/cron-release-highest.yml b/.github/workflows/cron-release-highest.yml index 571e35e6cd..76f4a4ee35 100644 --- a/.github/workflows/cron-release-highest.yml +++ b/.github/workflows/cron-release-highest.yml @@ -19,7 +19,7 @@ env: jobs: check-and-trigger: name: ๐Ÿ”Ž Backfill highest missing release tag - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: read diff --git a/.github/workflows/cron-security-codeql.yml b/.github/workflows/cron-security-codeql.yml index bc8c1cee3e..f5242ad964 100644 --- a/.github/workflows/cron-security-codeql.yml +++ b/.github/workflows/cron-security-codeql.yml @@ -34,7 +34,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: security-events: write packages: read diff --git a/.github/workflows/cron-security-fuzz.yml b/.github/workflows/cron-security-fuzz.yml index fb3a90cb17..ec8a3005ef 100644 --- a/.github/workflows/cron-security-fuzz.yml +++ b/.github/workflows/cron-security-fuzz.yml @@ -16,7 +16,7 @@ permissions: jobs: fuzz-yaml-bootstrap: name: ๐Ÿงฌ Fuzz the bootstrap YAML reader - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - name: โฌ‡๏ธ Checkout diff --git a/.github/workflows/cron-security-scorecard.yml b/.github/workflows/cron-security-scorecard.yml index 33eb5cebfc..144bd06067 100644 --- a/.github/workflows/cron-security-scorecard.yml +++ b/.github/workflows/cron-security-scorecard.yml @@ -26,7 +26,7 @@ permissions: read-all jobs: analysis: name: ๐Ÿ” Scorecard analysis - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' permissions: diff --git a/.github/workflows/cron-update.yml b/.github/workflows/cron-update.yml index f946f46cac..a396a9d1f0 100644 --- a/.github/workflows/cron-update.yml +++ b/.github/workflows/cron-update.yml @@ -21,7 +21,7 @@ jobs: update-docker-image-versions: name: ๐Ÿณ Update Docker image versions if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@v7.0.0 @@ -68,7 +68,7 @@ jobs: update-version-sources: name: ๐Ÿ”– Update declared version sources if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@v7.0.0 @@ -115,7 +115,7 @@ jobs: update-pip-pins: name: ๐Ÿ Update pinned pip requirements if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@v7.0.0 @@ -162,7 +162,7 @@ jobs: update-galaxy-pins: name: ๐ŸŒŒ Update pinned Ansible collections if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@v7.0.0 @@ -209,7 +209,7 @@ jobs: update-repository-refs: name: ๐ŸŒฟ Update repository refs if: vars.CI_ENABLE_AUTO_UPDATES == 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: โฌ‡๏ธ Checkout uses: actions/checkout@v7.0.0 @@ -252,3 +252,45 @@ jobs: UPDATE_PR_BODY: "Automated update of semver-based git `ref:` values in `roles/*/meta/services.yml`. Upstream tags resolved via `git ls-remote --tags`." UPDATE_BASE_BRANCH: main run: bash scripts/github/update/open/role_prs.sh roles + + update-runner-labels: + name: ๐Ÿƒ Update GitHub-hosted runner labels + if: vars.CI_ENABLE_AUTO_UPDATES == 'true' + runs-on: ubuntu-26.04 + steps: + - name: โฌ‡๏ธ Checkout + uses: actions/checkout@v7.0.0 + with: + ref: main + + - name: ๐Ÿ”ง Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: ๐Ÿ”„ Update runner labels + run: bash scripts/github/update/runner_labels.sh + + - name: ๐Ÿ”Ž Check for changes + id: diff + run: bash scripts/github/resolve/check_git_changes.sh .github/workflows + + - name: โš™๏ธ Generate app token + if: steps.diff.outputs.changed == 'true' + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.BOT_APP_CLIENT_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + + - name: ๐Ÿ“‚ Open PR if runner labels changed + if: steps.diff.outputs.changed == 'true' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + UPDATE_BRANCH_PREFIX: update/runner-labels + UPDATE_COMMIT_MESSAGE: "update(ci): raise GitHub-hosted runner labels" + UPDATE_PR_TITLE: "update(ci): raise GitHub-hosted runner labels" + UPDATE_PR_BODY: "Automated raise of every `runs-on: ubuntu-NN.NN` in `.github/workflows/` to the newest GA x64 Ubuntu image listed in https://github.com/actions/runner-images. Its CI run is the proof that the stack works on the new image." + UPDATE_BASE_BRANCH: main + run: bash scripts/github/update/open/pr.sh .github/workflows diff --git a/.github/workflows/entry-cancel-superseded.yml b/.github/workflows/entry-cancel-superseded.yml index defc9a7e5d..85ed6ee1db 100644 --- a/.github/workflows/entry-cancel-superseded.yml +++ b/.github/workflows/entry-cancel-superseded.yml @@ -27,7 +27,7 @@ jobs: cancel-branch-runs: name: ๐Ÿšซ Cancel superseded branch runs if: ${{ github.event_name == 'push' && (github.ref_name != 'main' || (github.ref_name == 'main' && startsWith(github.event.head_commit.message, 'Release version '))) }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: write contents: read @@ -52,7 +52,7 @@ jobs: cancel-pull-request-runs: name: ๐Ÿšซ Cancel superseded pull request runs if: ${{ github.event_name == 'pull_request_target' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-delete-branch.yml b/.github/workflows/entry-delete-branch.yml index b15f5c811d..21499359ae 100644 --- a/.github/workflows/entry-delete-branch.yml +++ b/.github/workflows/entry-delete-branch.yml @@ -20,7 +20,7 @@ permissions: jobs: cancel-branch-runs: if: ${{ github.event.ref_type == 'branch' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-manual-mirror-cleanup.yml b/.github/workflows/entry-manual-mirror-cleanup.yml index c4a6fc24ee..5f4436dbc7 100644 --- a/.github/workflows/entry-manual-mirror-cleanup.yml +++ b/.github/workflows/entry-manual-mirror-cleanup.yml @@ -40,7 +40,7 @@ permissions: jobs: cleanup: name: ๐Ÿ—‘๏ธ Delete private GHCR mirror packages - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read packages: write diff --git a/.github/workflows/entry-pr-change-orchestrate.yml b/.github/workflows/entry-pr-change-orchestrate.yml index 2cf95bf625..0a3c46f036 100644 --- a/.github/workflows/entry-pr-change-orchestrate.yml +++ b/.github/workflows/entry-pr-change-orchestrate.yml @@ -29,7 +29,7 @@ jobs: detect-pr-scope: name: ๐Ÿ”Ž Detect PR scope if: ${{ github.event.pull_request.draft == false && (github.event_name == 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: scope: ${{ steps.resolve.outputs.scope }} run_ci_orchestrator: ${{ steps.resolve.outputs.run_ci_orchestrator }} @@ -59,7 +59,7 @@ jobs: - detect-pr-scope - validation if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'false' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: โœ… Confirm lightweight PR scope shell: bash @@ -73,7 +73,7 @@ jobs: - detect-pr-scope - validation if: ${{ needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: whitelist: ${{ steps.resolve.outputs.whitelist }} steps: @@ -124,7 +124,7 @@ jobs: - detect-pr-scope - validation if: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && needs.detect-pr-scope.outputs.run_ci_orchestrator == 'true' }} - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: checkout_ref: ${{ steps.out.outputs.checkout_ref }} image_tag: ${{ steps.out.outputs.image_tag }} diff --git a/.github/workflows/entry-pr-closed-cancel-workflows.yml b/.github/workflows/entry-pr-closed-cancel-workflows.yml index f3d880ada1..e11ae6d0be 100644 --- a/.github/workflows/entry-pr-closed-cancel-workflows.yml +++ b/.github/workflows/entry-pr-closed-cancel-workflows.yml @@ -15,7 +15,7 @@ permissions: jobs: cancel-pr-runs: - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: actions: write contents: read diff --git a/.github/workflows/entry-pr-open-dependabot-close.yml b/.github/workflows/entry-pr-open-dependabot-close.yml index 6d11bd73ce..e6ea74258d 100644 --- a/.github/workflows/entry-pr-open-dependabot-close.yml +++ b/.github/workflows/entry-pr-open-dependabot-close.yml @@ -19,7 +19,7 @@ jobs: close-when-disabled: name: ๐Ÿšซ Close Dependabot PR while gated if: github.event.pull_request.user.login == 'dependabot[bot]' && vars.CI_ENABLE_AUTO_UPDATES != 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: ๐Ÿ“ช Close PR env: diff --git a/.github/workflows/entry-push-latest.yml b/.github/workflows/entry-push-latest.yml index 7bbddb2caa..59070de57b 100644 --- a/.github/workflows/entry-push-latest.yml +++ b/.github/workflows/entry-push-latest.yml @@ -21,7 +21,7 @@ permissions: jobs: run-policy: name: ๐Ÿงญ Decide CI policy - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: read pull-requests: read @@ -59,7 +59,7 @@ jobs: name: โญ๏ธ Skip CI needs: run-policy if: needs.run-policy.outputs.should_run != 'true' - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 steps: - name: โญ๏ธ Explain skip shell: bash @@ -72,7 +72,7 @@ jobs: if: needs.run-policy.outputs.should_run == 'true' needs: run-policy name: ๐Ÿท๏ธ Detect release tag - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: is_version: ${{ steps.pick.outputs.is_version }} version_tag: ${{ steps.pick.outputs.version_tag }} @@ -91,7 +91,7 @@ jobs: if: needs.run-policy.outputs.should_run == 'true' name: โญ Resolve new-role priority needs: run-policy - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: priority: ${{ steps.resolve.outputs.priority }} steps: @@ -132,7 +132,7 @@ jobs: - run-policy - detect-release - ci-orchestrator - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 outputs: allowed: ${{ steps.guard.outputs.allowed }} steps: diff --git a/cli/contributing/update/runner/__init__.py b/cli/contributing/update/runner/__init__.py new file mode 100644 index 0000000000..9b7a055ab0 --- /dev/null +++ b/cli/contributing/update/runner/__init__.py @@ -0,0 +1,5 @@ +"""CLI entry point package for the GitHub-hosted runner label update.""" + +from pathlib import Path + +PROJECT_ROOT: Path = Path(__file__).resolve().parents[4] diff --git a/cli/contributing/update/runner/__main__.py b/cli/contributing/update/runner/__main__.py new file mode 100755 index 0000000000..1e6444aa45 --- /dev/null +++ b/cli/contributing/update/runner/__main__.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +from pathlib import Path + +from utils.update.fetch import document +from utils.update.runner import IMAGES_README, bump_workflows, newest_ubuntu_label + +from . import PROJECT_ROOT + + +def main() -> int: + parser = argparse.ArgumentParser( + description=( + "Raise every workflow's runs-on Ubuntu label to the newest GA " + "GitHub-hosted x64 image." + ) + ) + parser.add_argument( + "--repo-root", + type=Path, + default=PROJECT_ROOT, + help="Repository root whose .github/workflows are updated", + ) + args = parser.parse_args() + + label = newest_ubuntu_label(document(IMAGES_README)) + if label is None: + print(f"No GA x64 Ubuntu label found in {IMAGES_README}.") + return 1 + + repo_root = args.repo_root.resolve() + changed = bump_workflows(repo_root / ".github" / "workflows", label) + if not changed: + print(f"Every workflow already runs on {label} or newer.") + return 0 + + print(f"Raised runs-on to {label} in:") + for path in changed: + print(f"- {path.relative_to(repo_root)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/contributing/actions/debugging/ci.md b/docs/contributing/actions/debugging/ci.md index bb84bf1450..6fbb377978 100644 --- a/docs/contributing/actions/debugging/ci.md +++ b/docs/contributing/actions/debugging/ci.md @@ -49,11 +49,11 @@ Some CI failures (most notably OOM-kills such as [#162](https://github.com/infin ### GitHub-Hosted Runner Specs -All [workflows](../../../../.github/workflows/) target `runs-on: ubuntu-24.04`, the GitHub-hosted standard Linux runner pinned to one Ubuntu release. GitHub still refreshes that image weekly; moving to the next release is a deliberate change of the label, and `tests/lint/repository/test_workflow_runner_pinned.py` rejects `*-latest` labels. The effective ceiling depends on the repository's visibility and plan: +All [workflows](../../../../.github/workflows/) target `runs-on: ubuntu-26.04`, the GitHub-hosted standard Linux runner pinned to one Ubuntu release. GitHub still refreshes that image weekly. The `๐Ÿƒ Update GitHub-hosted runner labels` job of [cron-update.yml](../../../../.github/workflows/cron-update.yml) opens a pull request when a newer GA x64 Ubuntu image is listed in [actions/runner-images](https://github.com/actions/runner-images), and `tests/lint/repository/test_workflow_runner_pinned.py` rejects `*-latest` labels. The effective ceiling depends on the repository's visibility and plan: | Repository / plan | vCPU | RAM | SSD | |------------------------------------------|------|--------|-------| -| Public repo (this repo, `ubuntu-24.04`) | 4 | 16 GB | 14 GB | +| Public repo (this repo, `ubuntu-26.04`) | 4 | 16 GB | 14 GB | | Private repo on Free / Pro / Team | 2 | 7 GB | 14 GB | | Private repo on Enterprise | 4 | 16 GB | 14 GB | @@ -70,7 +70,7 @@ For the authoritative specs, see the upstream docs for [standard hosted runners] Pick the profile that matches the runner you want to mimic: ```sh -# Public repo runner (ubuntu-24.04, 4 vCPU / 16 GB): the default CI target +# Public repo runner (ubuntu-26.04, 4 vCPU / 16 GB): the default CI target INFINITO_MEM_LIMIT=16g INFINITO_MEMSWAP_LIMIT=16g INFINITO_CPUS=4 make # Private repo runner on Free/Pro/Team (2 vCPU / 7 GB) diff --git a/scripts/github/update/runner_labels.sh b/scripts/github/update/runner_labels.sh new file mode 100755 index 0000000000..67e93be1b2 --- /dev/null +++ b/scripts/github/update/runner_labels.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)" + +cd "${REPO_ROOT}" +python3 -m cli.contributing.update.runner "$@" diff --git a/scripts/tests/deploy/act/workflow.sh b/scripts/tests/deploy/act/workflow.sh index 7be72c21a6..2f39f599ae 100755 --- a/scripts/tests/deploy/act/workflow.sh +++ b/scripts/tests/deploy/act/workflow.sh @@ -54,10 +54,10 @@ else fi cmd=(act "${ACT_EVENT}" -W "${_act_workflow}") -cmd+=(-P "ubuntu-latest=${ACT_PLATFORM_IMAGE}") -cmd+=(-P "ubuntu-24.04=${ACT_PLATFORM_IMAGE}") -cmd+=(-P "ubuntu-22.04=${ACT_PLATFORM_IMAGE}") -cmd+=(-P "ubuntu-20.04=${ACT_PLATFORM_IMAGE}") +_act_workflows_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)/.github/workflows" +while read -r _act_label; do + cmd+=(-P "${_act_label}=${ACT_PLATFORM_IMAGE}") +done < <(grep -rhE 'runs-on:' "${_act_workflows_dir}" | grep -oE 'ubuntu-[0-9]+\.[0-9]+' | sort -u) if [[ -n "${ACT_JOB}" ]]; then cmd+=(-j "${ACT_JOB}") diff --git a/tests/unit/python/utils/update/test_runner.py b/tests/unit/python/utils/update/test_runner.py new file mode 100644 index 0000000000..352c8f462e --- /dev/null +++ b/tests/unit/python/utils/update/test_runner.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from utils.cache.files import read_text +from utils.update.runner import bump_text, bump_workflows, newest_ubuntu_label + +_BADGE = "
![Endpoint Badge](https://img.shields.io/endpoint?url=x)" +_PREVIEW = "[![preview](https://img.shields.io/badge/preview-0969DA)](https://x)" +_README = "\n".join( + [ + "| Image | Architecture | YAML Label | Included Software |", + "| --------------------|--------------|---------------------|------------------|", + f"| Ubuntu 28.04 {_PREVIEW}{_BADGE} | x64 | `ubuntu-28.04` | [ubuntu-28.04] |", + f"| Ubuntu 26.04{_BADGE} | x64 | `ubuntu-26.04` | [ubuntu-26.04] |", + f"| Ubuntu 26.04 Arm64{_BADGE} | arm64 | `ubuntu-26.04-arm` | [x] |", + f"| Ubuntu 24.04{_BADGE} | x64 | `ubuntu-latest` or `ubuntu-24.04` | [x] |", + f"| Ubuntu Slim{_BADGE} | x64 | `ubuntu-slim` | [ubuntu-slim] |", + f"| macOS 26 Arm64{_BADGE} | arm64 | `macos-26` | [macOS-26-arm64] |", + ] +) + + +class TestNewestUbuntuLabel(unittest.TestCase): + def test_picks_newest_ga_x64_label(self) -> None: + self.assertEqual("ubuntu-26.04", newest_ubuntu_label(_README)) + + def test_none_without_an_ubuntu_row(self) -> None: + self.assertIsNone(newest_ubuntu_label("| Image | x |\n| macOS | arm64 |")) + + +class TestBumpText(unittest.TestCase): + def test_raises_older_and_keeps_newer(self) -> None: + text = ( + "jobs:\n" + " a:\n runs-on: ubuntu-22.04\n" + " b:\n runs-on: ubuntu-24.04\n" + " c:\n runs-on: ubuntu-28.04\n" + " d:\n runs-on: ubuntu-24.04-arm\n" + "# runs on ubuntu-24.04 elsewhere\n" + ) + self.assertEqual( + "jobs:\n" + " a:\n runs-on: ubuntu-26.04\n" + " b:\n runs-on: ubuntu-26.04\n" + " c:\n runs-on: ubuntu-28.04\n" + " d:\n runs-on: ubuntu-24.04-arm\n" + "# runs on ubuntu-24.04 elsewhere\n", + bump_text(text, "ubuntu-26.04"), + ) + + +class TestBumpWorkflows(unittest.TestCase): + def test_rewrites_only_changed_files(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "old.yml").write_text("runs-on: ubuntu-24.04\n", encoding="utf-8") + (root / "new.yml").write_text("runs-on: ubuntu-26.04\n", encoding="utf-8") + + changed = bump_workflows(root, "ubuntu-26.04") + + self.assertEqual([root / "old.yml"], changed) + self.assertEqual( + "runs-on: ubuntu-26.04\n", read_text(str(root / "old.yml")) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/utils/update/runner.py b/utils/update/runner.py new file mode 100644 index 0000000000..326de8d007 --- /dev/null +++ b/utils/update/runner.py @@ -0,0 +1,77 @@ +"""Bump the pinned GitHub-hosted Ubuntu runner label of every workflow job. + +GitHub lists its hosted images in the runner-images README; the newest x64 +Ubuntu label whose image carries no preview or beta badge is the target. +""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +IMAGES_README = "https://raw.githubusercontent.com/actions/runner-images/main/README.md" + +_LABEL = re.compile(r"`ubuntu-(\d{2})\.(\d{2})`") +_RUNS_ON = re.compile(r"(runs-on:\s*)ubuntu-(\d{2})\.(\d{2})(?![\w.-])") +_PRERELEASE = re.compile(r"preview|beta", re.IGNORECASE) + + +def newest_ubuntu_label(readme: str) -> str | None: + """Return the newest GA x64 Ubuntu label listed in the images table. + + Args: + readme: text of the runner-images README. + """ + versions: list[tuple[int, int]] = [] + for row in readme.splitlines(): + cells = [cell.strip() for cell in row.split("|")] + if len(cells) < 5 or not cells[1].startswith("Ubuntu"): + continue + if _PRERELEASE.search(cells[1]) or cells[2] != "x64": + continue + versions += [ + (int(major), int(minor)) for major, minor in _LABEL.findall(cells[3]) + ] + if not versions: + return None + major, minor = max(versions) + return f"ubuntu-{major:02d}.{minor:02d}" + + +def bump_text(text: str, label: str) -> str: + """Raise every older ``runs-on: ubuntu-NN.NN`` in *text* to *label*. + + Args: + text: workflow source. + label: target label, ``ubuntu-NN.NN``. + """ + target = tuple(int(part) for part in label.removeprefix("ubuntu-").split(".")) + + def _raise(match: re.Match[str]) -> str: + if (int(match.group(2)), int(match.group(3))) < target: + return match.group(1) + label + return match.group(0) + + return _RUNS_ON.sub(_raise, text) + + +def bump_workflows(workflows_dir: Path, label: str) -> list[Path]: + """Rewrite the workflows under *workflows_dir* to *label*; return the changed files. + + Args: + workflows_dir: directory holding the workflow YAML files. + label: target label, ``ubuntu-NN.NN``. + """ + changed: list[Path] = [] + for path in sorted(workflows_dir.glob("*.y*ml")): + text = path.read_text( # nocheck: cache-read โ€” read-then-write of the same file; cached read would go stale + encoding="utf-8" + ) + bumped = bump_text(text, label) + if bumped != text: + path.write_text(bumped, encoding="utf-8") + changed.append(path) + return changed