From c9c3954e61c90ec2bd789e15990dcf4cef695df4 Mon Sep 17 00:00:00 2001 From: tignear Date: Tue, 18 Aug 2026 11:35:34 +0000 Subject: [PATCH 1/3] chore(deps): coordinate Python and cocotb updates --- .github/workflows/updatecli.yml | 47 ++++++++ .updatecli-version | 1 + .updatecli/README.md | 25 +++++ .../scripts/latest_compatible_python.py | 98 +++++++++++++++++ .../tests/test_latest_compatible_python.py | 102 ++++++++++++++++++ .updatecli/updatecli.d/python-cocotb.yaml | 72 +++++++++++++ renovate.json | 9 ++ 7 files changed, 354 insertions(+) create mode 100644 .github/workflows/updatecli.yml create mode 100644 .updatecli-version create mode 100644 .updatecli/README.md create mode 100755 .updatecli/scripts/latest_compatible_python.py create mode 100644 .updatecli/tests/test_latest_compatible_python.py create mode 100644 .updatecli/updatecli.d/python-cocotb.yaml diff --git a/.github/workflows/updatecli.yml b/.github/workflows/updatecli.yml new file mode 100644 index 0000000000..b91b53f3bf --- /dev/null +++ b/.github/workflows/updatecli.yml @@ -0,0 +1,47 @@ +name: Update Python and cocotb + +on: + pull_request: + paths: + - ".github/workflows/updatecli.yml" + - ".updatecli-version" + - ".updatecli/**" + - "renovate.json" + schedule: + - cron: "17 6 * * 1" + workflow_dispatch: + +permissions: + actions: write + contents: write + pull-requests: write + +concurrency: + group: updatecli-python-cocotb + cancel-in-progress: false + +jobs: + update: + name: Updatecli + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - name: Install Updatecli + uses: updatecli/updatecli-action@v3 + with: + version-file: .updatecli-version + + - name: Preview update policy + run: updatecli pipeline diff --config .updatecli/updatecli.d + env: + UPDATECLI_GITHUB_ACTOR: ${{ github.actor }} + UPDATECLI_GITHUB_TOKEN: ${{ github.token }} + + - name: Apply update policy + if: github.event_name != 'pull_request' + run: updatecli pipeline apply --config .updatecli/updatecli.d + env: + UPDATECLI_GITHUB_ACTOR: ${{ github.actor }} + UPDATECLI_GITHUB_TOKEN: ${{ github.token }} diff --git a/.updatecli-version b/.updatecli-version new file mode 100644 index 0000000000..709f2c802a --- /dev/null +++ b/.updatecli-version @@ -0,0 +1 @@ +v0.120.1 diff --git a/.updatecli/README.md b/.updatecli/README.md new file mode 100644 index 0000000000..f42a343633 --- /dev/null +++ b/.updatecli/README.md @@ -0,0 +1,25 @@ +# Updatecli + +Updatecli owns the Python runtime and cocotb versions used by the Rust CI job. +Renovate remains responsible for the repository's other dependencies and is +configured not to update the `actions/setup-python` runtime input. + +The Python/cocotb pipeline selects the newest stable cocotb release from PyPI, +then chooses the newest stable CPython minor that has both: + +- a cocotb CPython wheel for manylinux x86-64; and +- a Linux x64 build in the `actions/python-versions` manifest. + +This keeps the two versions compatible without permanently capping Python. The +regular CI workflow remains the final compatibility check. + +Run the policy locally with: + +```sh +export UPDATECLI_GITHUB_TOKEN=... +export UPDATECLI_GITHUB_ACTOR=... +updatecli pipeline diff --config .updatecli/updatecli.d +``` + +The scheduled workflow uses `GITHUB_TOKEN`. GitHub may require a maintainer to +approve the CI runs on pull requests created with that token. diff --git a/.updatecli/scripts/latest_compatible_python.py b/.updatecli/scripts/latest_compatible_python.py new file mode 100755 index 0000000000..38e717a850 --- /dev/null +++ b/.updatecli/scripts/latest_compatible_python.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Select the newest CPython minor supported by a cocotb release.""" + +from __future__ import annotations + +import json +import re +import sys +from typing import Any +from urllib.request import Request, urlopen + +PYPI_RELEASE_URL = "https://pypi.org/pypi/cocotb/{version}/json" +PYTHON_VERSIONS_MANIFEST_URL = ( + "https://raw.githubusercontent.com/actions/python-versions/" + "main/versions-manifest.json" +) +CPYTHON_WHEEL_RE = re.compile(r"-cp(?P[0-9]+)-cp(?P=abi)-") +PYTHON_VERSION_RE = re.compile(r"^(?P[0-9]+)\.(?P[0-9]+)\.[0-9]+$") + + +def load_json(url: str) -> Any: + request = Request(url, headers={"User-Agent": "celox-updatecli"}) + with urlopen(request, timeout=30) as response: + return json.load(response) + + +def cocotb_linux_x64_minors(release: dict[str, Any]) -> set[tuple[int, int]]: + minors: set[tuple[int, int]] = set() + + for artifact in release.get("urls", []): + if artifact.get("packagetype") != "bdist_wheel": + continue + + filename = artifact.get("filename", "") + if "manylinux" not in filename or "x86_64" not in filename: + continue + + match = CPYTHON_WHEEL_RE.search(filename) + if match is None: + continue + + abi = match.group("abi") + if len(abi) < 2: + continue + + minors.add((int(abi[0]), int(abi[1:]))) + + return minors + + +def actions_linux_x64_minors(manifest: list[dict[str, Any]]) -> set[tuple[int, int]]: + minors: set[tuple[int, int]] = set() + + for release in manifest: + if not release.get("stable", False): + continue + + match = PYTHON_VERSION_RE.fullmatch(release.get("version", "")) + if match is None: + continue + + has_linux_x64 = any( + artifact.get("platform") == "linux" and artifact.get("arch") == "x64" + for artifact in release.get("files", []) + ) + if has_linux_x64: + minors.add((int(match.group("major")), int(match.group("minor")))) + + return minors + + +def latest_compatible_minor( + cocotb_release: dict[str, Any], python_manifest: list[dict[str, Any]] +) -> tuple[int, int]: + compatible = cocotb_linux_x64_minors(cocotb_release) & actions_linux_x64_minors( + python_manifest + ) + if not compatible: + raise RuntimeError("no compatible stable CPython Linux x64 release found") + + return max(compatible) + + +def main() -> int: + if len(sys.argv) != 2: + print(f"usage: {sys.argv[0]} ", file=sys.stderr) + return 2 + + cocotb_version = sys.argv[1] + cocotb_release = load_json(PYPI_RELEASE_URL.format(version=cocotb_version)) + python_manifest = load_json(PYTHON_VERSIONS_MANIFEST_URL) + major, minor = latest_compatible_minor(cocotb_release, python_manifest) + print(f"{major}.{minor}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.updatecli/tests/test_latest_compatible_python.py b/.updatecli/tests/test_latest_compatible_python.py new file mode 100644 index 0000000000..ec0efdc35a --- /dev/null +++ b/.updatecli/tests/test_latest_compatible_python.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import importlib.util +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).parents[1] / "scripts" / "latest_compatible_python.py" +SPEC = importlib.util.spec_from_file_location("latest_compatible_python", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class LatestCompatiblePythonTests(unittest.TestCase): + def test_selects_latest_shared_stable_linux_x64_minor(self) -> None: + cocotb_release = { + "urls": [ + { + "filename": ( + "cocotb-2.1.0-cp313-cp313-" + "manylinux_2_17_x86_64.whl" + ), + "packagetype": "bdist_wheel", + }, + { + "filename": ( + "cocotb-2.1.0-cp314-cp314-" + "manylinux_2_17_x86_64.whl" + ), + "packagetype": "bdist_wheel", + }, + { + "filename": "cocotb-2.1.0-cp315-cp315-win_amd64.whl", + "packagetype": "bdist_wheel", + }, + ] + } + python_manifest = [ + { + "version": "3.15.0-beta.1", + "stable": False, + "files": [{"platform": "linux", "arch": "x64"}], + }, + { + "version": "3.14.4", + "stable": True, + "files": [{"platform": "linux", "arch": "x64"}], + }, + { + "version": "3.13.12", + "stable": True, + "files": [{"platform": "linux", "arch": "x64"}], + }, + ] + + self.assertEqual( + MODULE.latest_compatible_minor(cocotb_release, python_manifest), + (3, 14), + ) + + def test_ignores_source_archives_and_non_linux_wheels(self) -> None: + cocotb_release = { + "urls": [ + { + "filename": "cocotb-2.1.0.tar.gz", + "packagetype": "sdist", + }, + { + "filename": "cocotb-2.1.0-cp314-cp314-win_amd64.whl", + "packagetype": "bdist_wheel", + }, + ] + } + + self.assertEqual(MODULE.cocotb_linux_x64_minors(cocotb_release), set()) + + def test_fails_when_no_compatible_release_exists(self) -> None: + cocotb_release = { + "urls": [ + { + "filename": ( + "cocotb-2.1.0-cp314-cp314-" + "manylinux_2_17_x86_64.whl" + ), + "packagetype": "bdist_wheel", + } + ] + } + python_manifest = [ + { + "version": "3.13.12", + "stable": True, + "files": [{"platform": "linux", "arch": "x64"}], + } + ] + + with self.assertRaisesRegex(RuntimeError, "no compatible stable CPython"): + MODULE.latest_compatible_minor(cocotb_release, python_manifest) + + +if __name__ == "__main__": + unittest.main() diff --git a/.updatecli/updatecli.d/python-cocotb.yaml b/.updatecli/updatecli.d/python-cocotb.yaml new file mode 100644 index 0000000000..2a549941c0 --- /dev/null +++ b/.updatecli/updatecli.d/python-cocotb.yaml @@ -0,0 +1,72 @@ +name: Keep Python and cocotb compatible +pipelineid: python-cocotb +version: v0.120.1 + +scms: + default: + kind: github + spec: + branch: master + commitusingapi: true + email: "41898282+github-actions[bot]@users.noreply.github.com" + owner: celox-sim + repository: celox + token: '{{ requiredEnv "UPDATECLI_GITHUB_TOKEN" }}' + user: "github-actions[bot]" + username: '{{ requiredEnv "UPDATECLI_GITHUB_ACTOR" }}' + +actions: + pull-request: + kind: github/pullrequest + scmid: default + title: "chore(deps): update compatible Python and cocotb versions" + spec: + description: | + Updates the CI Python runtime together with the latest cocotb release. + The selected Python minor has a cocotb manylinux x86-64 wheel and an + actions/python-versions Linux x64 build. + + - Python: {{ source "python" }} + - cocotb: {{ source "cocotb" }} + labels: + - dependencies + mergemethod: squash + +sources: + cocotb: + name: Get the latest stable cocotb release + kind: pypi + spec: + name: cocotb + versionfilter: + kind: semver + pattern: "*" + + python: + name: Find the latest Python minor supported by cocotb + kind: shell + dependson: + - source#cocotb + spec: + command: 'python3 .updatecli/scripts/latest_compatible_python.py {{ source "cocotb" }}' + +targets: + python: + name: Update the CI Python runtime to {{ source "python" }} + kind: file + scmid: default + sourceid: python + spec: + file: .github/workflows/ci.yml + matchpattern: 'python-version: "([0-9]+\.[0-9]+)"' + replacepattern: 'python-version: "{{ source "python" }}"' + + cocotb: + name: Update the CI cocotb pin to {{ source "cocotb" }} + kind: file + scmid: default + sourceid: cocotb + spec: + file: .github/workflows/ci.yml + matchpattern: 'python -m pip install cocotb==([0-9A-Za-z.+_-]+)' + replacepattern: 'python -m pip install cocotb=={{ source "cocotb" }}' diff --git a/renovate.json b/renovate.json index 19b7791128..3c21fbdfdf 100644 --- a/renovate.json +++ b/renovate.json @@ -32,6 +32,15 @@ } ], "packageRules": [ + { + "matchManagers": [ + "github-actions" + ], + "matchDepNames": [ + "python" + ], + "enabled": false + }, { "matchManagers": [ "cargo" From 69a956ecc11bb0ac056e99bcb1f3672b777b2c8d Mon Sep 17 00:00:00 2001 From: tignear Date: Tue, 18 Aug 2026 11:41:56 +0000 Subject: [PATCH 2/3] fix(ci): use existing Updatecli action tag --- .github/workflows/updatecli.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/updatecli.yml b/.github/workflows/updatecli.yml index b91b53f3bf..5f188a6d77 100644 --- a/.github/workflows/updatecli.yml +++ b/.github/workflows/updatecli.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@v7 - name: Install Updatecli - uses: updatecli/updatecli-action@v3 + uses: updatecli/updatecli-action@v3.6.0 with: version-file: .updatecli-version From 0879d868240dcd5f3e63b2961f102c1b3da06fbe Mon Sep 17 00:00:00 2001 From: tignear Date: Tue, 18 Aug 2026 12:01:46 +0000 Subject: [PATCH 3/3] fix(ci): validate Updatecli-generated pull requests --- .github/RELEASING.md | 2 +- .github/workflows/updatecli-check.yml | 40 +++++++++++++++++++++++++++ .github/workflows/updatecli.yml | 29 +++++++++++-------- .updatecli/README.md | 6 ++-- 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/updatecli-check.yml diff --git a/.github/RELEASING.md b/.github/RELEASING.md index a627e7d06a..3d64aed75e 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -102,7 +102,7 @@ distribution in every channel. ## Repository configuration -Release automation uses the `celox-release-please` GitHub App, installed for this +Release automation uses the `celox-automation` GitHub App, installed for this repository with repository contents and pull request write access. Store its numeric App ID as the `RELEASE_APP_ID` Actions variable and its complete PEM private key as the `RELEASE_APP_PRIVATE_KEY` Actions secret. Each job mints a diff --git a/.github/workflows/updatecli-check.yml b/.github/workflows/updatecli-check.yml new file mode 100644 index 0000000000..eb75175e1f --- /dev/null +++ b/.github/workflows/updatecli-check.yml @@ -0,0 +1,40 @@ +name: Validate Python and cocotb update policy + +on: + pull_request: + paths: + - ".github/workflows/ci.yml" + - ".github/workflows/updatecli.yml" + - ".github/workflows/updatecli-check.yml" + - ".updatecli-version" + - ".updatecli/**" + - "renovate.json" + +permissions: + contents: read + +concurrency: + group: updatecli-check-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate: + name: Updatecli + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + + - name: Install Updatecli + uses: updatecli/updatecli-action@v3.6.0 + with: + version-file: .updatecli-version + + - name: Test Python compatibility selector + run: python3 -m unittest discover -s .updatecli/tests -v + + - name: Preview update policy + run: updatecli pipeline diff --config .updatecli/updatecli.d + env: + UPDATECLI_GITHUB_ACTOR: ${{ github.actor }} + UPDATECLI_GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/updatecli.yml b/.github/workflows/updatecli.yml index 5f188a6d77..e94efa602e 100644 --- a/.github/workflows/updatecli.yml +++ b/.github/workflows/updatecli.yml @@ -1,20 +1,12 @@ name: Update Python and cocotb on: - pull_request: - paths: - - ".github/workflows/updatecli.yml" - - ".updatecli-version" - - ".updatecli/**" - - "renovate.json" schedule: - cron: "17 6 * * 1" workflow_dispatch: permissions: - actions: write - contents: write - pull-requests: write + contents: read concurrency: group: updatecli-python-cocotb @@ -28,20 +20,33 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Create automation app token + id: automation-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ vars.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-issues: write + permission-pull-requests: write + permission-workflows: write + - name: Install Updatecli uses: updatecli/updatecli-action@v3.6.0 with: version-file: .updatecli-version + - name: Test Python compatibility selector + run: python3 -m unittest discover -s .updatecli/tests -v + - name: Preview update policy run: updatecli pipeline diff --config .updatecli/updatecli.d env: UPDATECLI_GITHUB_ACTOR: ${{ github.actor }} - UPDATECLI_GITHUB_TOKEN: ${{ github.token }} + UPDATECLI_GITHUB_TOKEN: ${{ steps.automation-token.outputs.token }} - name: Apply update policy - if: github.event_name != 'pull_request' run: updatecli pipeline apply --config .updatecli/updatecli.d env: UPDATECLI_GITHUB_ACTOR: ${{ github.actor }} - UPDATECLI_GITHUB_TOKEN: ${{ github.token }} + UPDATECLI_GITHUB_TOKEN: ${{ steps.automation-token.outputs.token }} diff --git a/.updatecli/README.md b/.updatecli/README.md index f42a343633..c37b975094 100644 --- a/.updatecli/README.md +++ b/.updatecli/README.md @@ -21,5 +21,7 @@ export UPDATECLI_GITHUB_ACTOR=... updatecli pipeline diff --config .updatecli/updatecli.d ``` -The scheduled workflow uses `GITHUB_TOKEN`. GitHub may require a maintainer to -approve the CI runs on pull requests created with that token. +Pull request validation uses the read-only `GITHUB_TOKEN`. Scheduled and manual +updates mint a short-lived token for the `celox-automation` GitHub App from the +existing `RELEASE_APP_ID` variable and `RELEASE_APP_PRIVATE_KEY` secret. Pull +requests created with that installation token trigger the regular CI workflows.