Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/updatecli-check.yml
Original file line number Diff line number Diff line change
@@ -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 }}
52 changes: 52 additions & 0 deletions .github/workflows/updatecli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Update Python and cocotb

on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:

permissions:
contents: read

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: 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the new selector tests in CI

When a pull request changes the selector or its test cases, this workflow only runs updatecli pipeline diff, while the regular CI workflow invokes only the Node test suite under scripts/; no workflow runs .updatecli/tests/test_latest_compatible_python.py. The preview exercises the current live-data path but not the filtering and failure cases covered by these tests, so those tests can regress or fail while every required check remains green; add the unittest discovery command as a workflow step.

Useful? React with 👍 / 👎.

env:
UPDATECLI_GITHUB_ACTOR: ${{ github.actor }}
UPDATECLI_GITHUB_TOKEN: ${{ steps.automation-token.outputs.token }}

- name: Apply update policy
run: updatecli pipeline apply --config .updatecli/updatecli.d
env:
UPDATECLI_GITHUB_ACTOR: ${{ github.actor }}
UPDATECLI_GITHUB_TOKEN: ${{ steps.automation-token.outputs.token }}
1 change: 1 addition & 0 deletions .updatecli-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v0.120.1
27 changes: 27 additions & 0 deletions .updatecli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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
```

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.
98 changes: 98 additions & 0 deletions .updatecli/scripts/latest_compatible_python.py
Original file line number Diff line number Diff line change
@@ -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<abi>[0-9]+)-cp(?P=abi)-")
Comment thread
tignear marked this conversation as resolved.
PYTHON_VERSION_RE = re.compile(r"^(?P<major>[0-9]+)\.(?P<minor>[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]} <cocotb-version>", 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())
102 changes: 102 additions & 0 deletions .updatecli/tests/test_latest_compatible_python.py
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading