From 5b352438dcad40f65eb2669805d6efeb8d9f9c02 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 Oct 2026 18:13:55 -0500 Subject: [PATCH 1/5] tests: pb-core's sockets in a short directory too The endpoint fixture still put its AF_UNIX sockets under pytest's tmp_path_factory, which the end-to-end tests stopped doing for the same reason. On a Mac the per-user temporary directory is 57 bytes once resolved, and with pytest's two levels and the fixture's own a socket measures 104 bytes as soon as pytest's run counter reaches three digits: one too many for a sun_path of 104, counting the terminator. Every unix case in test_ipc.py then failed with "AF_UNIX path too long" -- on a developer's machine, not on CI, whose runners had not counted that high. The directory is now made under /tmp and removed afterwards, as short_tmp_path does in tests/e2e/conftest.py. --- packages/pb-core/tests/conftest.py | 32 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/packages/pb-core/tests/conftest.py b/packages/pb-core/tests/conftest.py index dac77e1..f0308db 100644 --- a/packages/pb-core/tests/conftest.py +++ b/packages/pb-core/tests/conftest.py @@ -1,9 +1,12 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2026 The Pybricks Authors +import shutil import socket import sys +import tempfile import uuid +from collections.abc import Iterator import pytest @@ -31,9 +34,7 @@ def _schemes() -> list[str]: @pytest.fixture(params=_schemes()) -def endpoint( - request: pytest.FixtureRequest, tmp_path_factory: pytest.TempPathFactory -) -> Endpoint: +def endpoint(request: pytest.FixtureRequest) -> Iterator[Endpoint]: """A unique, unused endpoint for each transport under test. Uniqueness matters: named pipe names are machine-global, so a fixed name @@ -43,14 +44,25 @@ def endpoint( unique = uuid.uuid4().hex[:12] if scheme == "memory": - return Endpoint("memory", f"test-{unique}") + yield Endpoint("memory", f"test-{unique}") + return if scheme == "tcp": - return Endpoint("tcp", f"127.0.0.1:{_free_tcp_port()}") + yield Endpoint("tcp", f"127.0.0.1:{_free_tcp_port()}") + return if scheme == "pipe": - return Endpoint("pipe", f"pb-test-{unique}") - - # AF_UNIX: keep the path short, since sun_path is only 104 bytes on macOS. - directory = tmp_path_factory.mktemp("s") - return Endpoint("unix", str(directory / f"{unique}.sock")) + yield Endpoint("pipe", f"pb-test-{unique}") + return + + # AF_UNIX: under /tmp rather than pytest's tmp_path, since sun_path is only + # 104 bytes on macOS. The per-user temporary directory there is 57 bytes + # once resolved, and with pytest's own two levels a socket in it measures + # 104 as soon as pytest's run counter reaches three digits -- one too many, + # counting the terminator. tests/e2e/conftest.py's short_tmp_path does the + # same for the end-to-end tests. + directory = tempfile.mkdtemp(prefix="pb-", dir="/tmp") + try: + yield Endpoint("unix", f"{directory}/{unique}.sock") + finally: + shutil.rmtree(directory, ignore_errors=True) From 6a5a4dc69c635ae4feaf6c92f6228749875e41b9 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 Oct 2026 18:14:08 -0500 Subject: [PATCH 2/5] docs: the site is at pybricks.com/pybricks-tools GitHub Pages serves it there, because the organisation's Pages site has pybricks.com as its custom domain, and pybricks.github.io/pybricks-tools only redirects to it. site_url gives the canonical links and the sitemap, so they pointed every search engine at the redirect. The packages' documentation URLs moved with it. Those matter more: they are uploaded with each release, and a release on PyPI can never have its metadata changed afterwards. --- packages/pb-cli/pyproject.toml | 2 +- packages/pb-client/pyproject.toml | 2 +- packages/pb-core/pyproject.toml | 2 +- packages/pb-service/pyproject.toml | 2 +- properdocs.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pb-cli/pyproject.toml b/packages/pb-cli/pyproject.toml index 7bceb0d..597ba80 100644 --- a/packages/pb-cli/pyproject.toml +++ b/packages/pb-cli/pyproject.toml @@ -35,7 +35,7 @@ pb = "pb_cli.cli:main" [project.urls] homepage = "https://pybricks.com" repository = "https://github.com/pybricks/pybricks-tools" -documentation = "https://pybricks.github.io/pybricks-tools/cli/" +documentation = "https://pybricks.com/pybricks-tools/cli/" "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-cli/CHANGELOG.md" "Support" = "https://github.com/pybricks/support/discussions" "Issues" = "https://github.com/pybricks/support/issues" diff --git a/packages/pb-client/pyproject.toml b/packages/pb-client/pyproject.toml index aac55b5..358d2ed 100644 --- a/packages/pb-client/pyproject.toml +++ b/packages/pb-client/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ [project.urls] homepage = "https://pybricks.com" repository = "https://github.com/pybricks/pybricks-tools" -documentation = "https://pybricks.github.io/pybricks-tools/reference/" +documentation = "https://pybricks.com/pybricks-tools/reference/" "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-client/CHANGELOG.md" "Support" = "https://github.com/pybricks/support/discussions" "Issues" = "https://github.com/pybricks/support/issues" diff --git a/packages/pb-core/pyproject.toml b/packages/pb-core/pyproject.toml index 5aadd9f..d6b88a1 100644 --- a/packages/pb-core/pyproject.toml +++ b/packages/pb-core/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ [project.urls] homepage = "https://pybricks.com" repository = "https://github.com/pybricks/pybricks-tools" -documentation = "https://pybricks.github.io/pybricks-tools/design/" +documentation = "https://pybricks.com/pybricks-tools/design/" "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-core/CHANGELOG.md" "Support" = "https://github.com/pybricks/support/discussions" "Issues" = "https://github.com/pybricks/support/issues" diff --git a/packages/pb-service/pyproject.toml b/packages/pb-service/pyproject.toml index 44f3981..ef17370 100644 --- a/packages/pb-service/pyproject.toml +++ b/packages/pb-service/pyproject.toml @@ -34,7 +34,7 @@ pb-service = "pb_service.cli:main" [project.urls] homepage = "https://pybricks.com" repository = "https://github.com/pybricks/pybricks-tools" -documentation = "https://pybricks.github.io/pybricks-tools/design/" +documentation = "https://pybricks.com/pybricks-tools/design/" "Changelog" = "https://github.com/pybricks/pybricks-tools/blob/main/packages/pb-service/CHANGELOG.md" "Support" = "https://github.com/pybricks/support/discussions" "Issues" = "https://github.com/pybricks/support/issues" diff --git a/properdocs.yml b/properdocs.yml index cf44ead..ca78c1e 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -1,6 +1,6 @@ site_name: Pybricks Tools site_description: A service, library and CLI for talking to Pybricks hubs -site_url: https://pybricks.github.io/pybricks-tools/ +site_url: https://pybricks.com/pybricks-tools/ repo_url: https://github.com/pybricks/pybricks-tools edit_uri: edit/main/docs/ From ead64d3d80903d644cea3810c0a6ac19c277d7a5 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 Oct 2026 18:14:09 -0500 Subject: [PATCH 3/5] repo: the changelogs catch up with Bluetooth Classic Nothing said that EV3 and NXT hubs can be reached at all. pb-service's changelog last changed with the simulated serial port, and the RFCOMM transport, the macOS permission dialog, the reported port that will not open and the hub protocol's v1.6.0 and v1.7.0 all landed after it, along with --transport rfcomm in the CLI and watching_adapters() in the client. The repository's own changelog also said CI runs Python 3.11 to 3.13, and it has run 3.14 as well since 3.14 was added to the matrix. --- CHANGELOG.md | 2 +- packages/pb-cli/CHANGELOG.md | 1 + packages/pb-client/CHANGELOG.md | 1 + packages/pb-service/CHANGELOG.md | 4 ++++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a42a14..3d2cfcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ The format is based on - A uv workspace of four packages, `src` layout throughout, with `py.typed`. - ruff, pyright strict, pytest and a strict docs build, gated from commit one. -- CI on Linux, macOS, Windows and Windows ARM64, across Python 3.11 to 3.13. +- CI on Linux, macOS, Windows and Windows ARM64, across Python 3.11 to 3.14. - Documentation with the API and command references generated from the code. ### Notes diff --git a/packages/pb-cli/CHANGELOG.md b/packages/pb-cli/CHANGELOG.md index c498b65..1554710 100644 --- a/packages/pb-cli/CHANGELOG.md +++ b/packages/pb-cli/CHANGELOG.md @@ -25,3 +25,4 @@ Notable changes to `pybricks-cli`, the `pb` command. The format is based on - Data on stdout and everything else on stderr, so every command can be piped. - One-line errors with a suggested fix; a traceback only when it is our bug. - An arrow-key picker wherever the tool would otherwise have to guess. +- `--transport usb`, `ble` or `rfcomm`, the last for EV3 and NXT hubs. diff --git a/packages/pb-client/CHANGELOG.md b/packages/pb-client/CHANGELOG.md index 634a710..db39a5b 100644 --- a/packages/pb-client/CHANGELOG.md +++ b/packages/pb-client/CHANGELOG.md @@ -9,6 +9,7 @@ Notable changes to `pybricks-client`. The format is based on - `connect()` gives an async handle to the service, starting one if needed. - Ongoing events are async context managers yielding async iterators. +- `watching_adapters()` says which Bluetooth adapters a search cannot use. - No function takes a `timeout`; callers scope cancellation themselves. - Compiles a script and every module of its own that it imports. - Passes a precompiled `.mpy` through, for modules built for native code. diff --git a/packages/pb-service/CHANGELOG.md b/packages/pb-service/CHANGELOG.md index e0edd3d..544f5b4 100644 --- a/packages/pb-service/CHANGELOG.md +++ b/packages/pb-service/CHANGELOG.md @@ -12,7 +12,11 @@ Notable changes to `pybricks-service`. The format is based on - Single-instance lock, shared-secret authentication, and a startup log line. - Exits when idle, but never while a hub is still connected. - Bluetooth through bleak, behind a seam that USB and RFCOMM can reuse. +- EV3 and NXT hubs over Bluetooth Classic, found among the paired devices. +- Says when macOS is asking for Bluetooth permission, and waits for the answer. +- A serial port that will not open is reported, not taken for a missing hub. - The Pybricks hub protocol, gated by profile version wherever it changed. +- Speaks every Pybricks Profile version up to v1.7.0. - Scanning that reports each hub as it is heard, with type and signal strength. - Remembers the hub chosen with `pb use`, in the user's state directory. - A simulated hub, so the whole service can be exercised without a radio. From d454aeee2a6e06c734302a31630d186d91787c34 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 Oct 2026 18:28:58 -0500 Subject: [PATCH 4/5] repo: 1.0.0b1 The first release is the 1.0.0b1 that protocol.py was written to expect, rather than the 0.1.0 the packages said. - All four packages are 1.0.0b1, in pyproject.toml and in __version__, with "Development Status :: 4 - Beta". - They require one another at >=1.0.0b1. Unbounded until now, which once they are released independently would have let pybricks-cli be installed beside a pybricks-core from before anything it calls existed. The bound names the beta itself, because a beta sorts below the release it leads to: >=1.0.0 would refuse 1.0.0b1. A lower bound only: no library requirement here gets an upper one. - The status notes say beta, and that commands and APIs may still change before 1.0.0, in place of "early development, nothing is stable". MIN_PROTOCOL_VERSION goes on following PROTOCOL_VERSION, as its docstring already says it does through the betas. --- README.md | 2 +- docs/index.md | 2 +- packages/pb-cli/README.md | 2 +- packages/pb-cli/pyproject.toml | 9 +++++---- packages/pb-cli/src/pb_cli/__init__.py | 2 +- packages/pb-client/README.md | 2 +- packages/pb-client/pyproject.toml | 5 +++-- packages/pb-client/src/pb_client/__init__.py | 2 +- packages/pb-core/README.md | 2 +- packages/pb-core/pyproject.toml | 3 ++- packages/pb-core/src/pb_core/__init__.py | 2 +- packages/pb-service/README.md | 2 +- packages/pb-service/pyproject.toml | 5 +++-- packages/pb-service/src/pb_service/__init__.py | 2 +- uv.lock | 8 ++++---- 15 files changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index a5f9ced..d67c2d7 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,6 @@ async with pb_client.connect() as pb: await pb.run_program(await pb_client.compile_script(Path("my_robot.py"))) ``` -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. [Documentation](docs/index.md) · [Contributing](CONTRIBUTING.md) diff --git a/docs/index.md b/docs/index.md index 3e95a96..c9d0ed4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ Run Python programs on your [Pybricks](https://pybricks.com) hub from your computer — from the command line, or from your own Python code. !!! note "Status" - Early development. Nothing here is stable yet. + Beta. Commands and APIs may still change before 1.0.0. ## Install diff --git a/packages/pb-cli/README.md b/packages/pb-cli/README.md index b0111f2..35ad17f 100644 --- a/packages/pb-cli/README.md +++ b/packages/pb-cli/README.md @@ -20,4 +20,4 @@ starts instantly, and so does the hundredth. `pb repl` talks to the hub's interpreter a line at a time, and `pb run -c` runs a line or two of Python without a file. `pb --help` lists the rest. -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. diff --git a/packages/pb-cli/pyproject.toml b/packages/pb-cli/pyproject.toml index 597ba80..1605639 100644 --- a/packages/pb-cli/pyproject.toml +++ b/packages/pb-cli/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pybricks-cli" -version = "0.1.0" +version = "1.0.0b1" description = "The Pybricks command line interface" readme = "README.md" license = "MIT" @@ -10,6 +10,7 @@ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }] requires-python = ">=3.11" keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"] classifiers = [ + "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Education", @@ -19,9 +20,9 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "pybricks-core", - "pybricks-client", - "pybricks-service", + "pybricks-core>=1.0.0b1", + "pybricks-client>=1.0.0b1", + "pybricks-service>=1.0.0b1", "anyio>=4.14", "cyclopts>=4.22", "rich>=13.7", diff --git a/packages/pb-cli/src/pb_cli/__init__.py b/packages/pb-cli/src/pb_cli/__init__.py index 0456c02..cee311b 100644 --- a/packages/pb-cli/src/pb_cli/__init__.py +++ b/packages/pb-cli/src/pb_cli/__init__.py @@ -3,6 +3,6 @@ """The Pybricks command line interface.""" -__version__ = "0.1.0" +__version__ = "1.0.0b1" __all__ = ["__version__"] diff --git a/packages/pb-client/README.md b/packages/pb-client/README.md index 99e1c1e..343c89c 100644 --- a/packages/pb-client/README.md +++ b/packages/pb-client/README.md @@ -23,4 +23,4 @@ everything it opens is closed for you when the `async with` block ends. **Looking for the command line?** That is `pybricks-cli`, which installs `pb` and uses this library underneath. -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. diff --git a/packages/pb-client/pyproject.toml b/packages/pb-client/pyproject.toml index 358d2ed..292135d 100644 --- a/packages/pb-client/pyproject.toml +++ b/packages/pb-client/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pybricks-client" -version = "0.1.0" +version = "1.0.0b1" description = "Client library for the Pybricks service" readme = "README.md" license = "MIT" @@ -10,6 +10,7 @@ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }] requires-python = ">=3.11" keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"] classifiers = [ + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Education", "Natural Language :: English", @@ -18,7 +19,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "pybricks-core", + "pybricks-core>=1.0.0b1", "anyio>=4.14", "mpy-cross-v6>=1.1.1", "typing-extensions>=4.15", diff --git a/packages/pb-client/src/pb_client/__init__.py b/packages/pb-client/src/pb_client/__init__.py index de87568..1a1a213 100644 --- a/packages/pb-client/src/pb_client/__init__.py +++ b/packages/pb-client/src/pb_client/__init__.py @@ -11,7 +11,7 @@ [move_on_after][anyio.move_on_after] instead. """ -__version__ = "0.1.0" +__version__ = "1.0.0b1" from pb_client.compile import ( SOURCE_NAME, diff --git a/packages/pb-core/README.md b/packages/pb-core/README.md index e6ce2d8..a0e5979 100644 --- a/packages/pb-core/README.md +++ b/packages/pb-core/README.md @@ -9,4 +9,4 @@ file locations all of them use. `pybricks-client`, which is the one to reach for if you want to drive a hub from your own Python. -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. diff --git a/packages/pb-core/pyproject.toml b/packages/pb-core/pyproject.toml index d6b88a1..5493545 100644 --- a/packages/pb-core/pyproject.toml +++ b/packages/pb-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pybricks-core" -version = "0.1.0" +version = "1.0.0b1" description = "Shared protocol, IPC transport and logging for the Pybricks tools" readme = "README.md" license = "MIT" @@ -10,6 +10,7 @@ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }] requires-python = ">=3.11" keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"] classifiers = [ + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Education", "Natural Language :: English", diff --git a/packages/pb-core/src/pb_core/__init__.py b/packages/pb-core/src/pb_core/__init__.py index c0ed8df..674ec45 100644 --- a/packages/pb-core/src/pb_core/__init__.py +++ b/packages/pb-core/src/pb_core/__init__.py @@ -8,6 +8,6 @@ platform paths. It deliberately depends on neither ``pb_service`` nor ``pb_client``. """ -__version__ = "0.1.0" +__version__ = "1.0.0b1" __all__ = ["__version__"] diff --git a/packages/pb-service/README.md b/packages/pb-service/README.md index c9b8604..9131a9a 100644 --- a/packages/pb-service/README.md +++ b/packages/pb-service/README.md @@ -10,4 +10,4 @@ session and a program watching the same hub do not get in each other's way. **You do not install this yourself.** It comes with `pybricks-cli`, and it starts when something needs it and stops when nothing has for a while. -> **Status:** early development. Nothing is stable yet. +> **Status:** beta. Commands and APIs may still change before 1.0.0. diff --git a/packages/pb-service/pyproject.toml b/packages/pb-service/pyproject.toml index ef17370..c94b0be 100644 --- a/packages/pb-service/pyproject.toml +++ b/packages/pb-service/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pybricks-service" -version = "0.1.0" +version = "1.0.0b1" description = "The Pybricks service: manages hub connections" readme = "README.md" license = "MIT" @@ -10,6 +10,7 @@ maintainers = [{ name = "David Lechner", email = "david@pybricks.com" }] requires-python = ">=3.11" keywords = ["pybricks", "lego", "micropython", "bluetooth", "robotics"] classifiers = [ + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Education", "Natural Language :: English", @@ -18,7 +19,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "pybricks-core", + "pybricks-core>=1.0.0b1", "anyio>=4.14", "bleak>=1.1.0", "packaging>=22", diff --git a/packages/pb-service/src/pb_service/__init__.py b/packages/pb-service/src/pb_service/__init__.py index 9e2a3d4..9d2b4b0 100644 --- a/packages/pb-service/src/pb_service/__init__.py +++ b/packages/pb-service/src/pb_service/__init__.py @@ -8,6 +8,6 @@ once. """ -__version__ = "0.1.0" +__version__ = "1.0.0b1" __all__ = ["__version__"] diff --git a/uv.lock b/uv.lock index 4c8d7c3..1ef0ede 100644 --- a/uv.lock +++ b/uv.lock @@ -893,7 +893,7 @@ wheels = [ [[package]] name = "pybricks-cli" -version = "0.1.0" +version = "1.0.0b1" source = { editable = "packages/pb-cli" } dependencies = [ { name = "anyio" }, @@ -920,7 +920,7 @@ requires-dist = [ [[package]] name = "pybricks-client" -version = "0.1.0" +version = "1.0.0b1" source = { editable = "packages/pb-client" } dependencies = [ { name = "anyio" }, @@ -939,7 +939,7 @@ requires-dist = [ [[package]] name = "pybricks-core" -version = "0.1.0" +version = "1.0.0b1" source = { editable = "packages/pb-core" } dependencies = [ { name = "anyio" }, @@ -956,7 +956,7 @@ requires-dist = [ [[package]] name = "pybricks-service" -version = "0.1.0" +version = "1.0.0b1" source = { editable = "packages/pb-service" } dependencies = [ { name = "aio-rfcomm" }, From 69216e5019d0c42e15750ac75da15ee3e867d6d4 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 18 Oct 2026 18:14:18 -0500 Subject: [PATCH 5/5] ci: publish a package to PyPI when its tag is pushed The packages are released independently, so a tag names one of them: pybricks-core-v1.0.0 publishes pybricks-core 1.0.0 and nothing else. Before anything is built the tag is checked against the package's own version and against its changelog, which must have a section for that version. A version on PyPI can never be uploaded again, so a tag pushed in error and caught after the upload is a version number lost for good. The upload uses PyPI's trusted publishing, so no token is stored anywhere, and runs in a "pypi" environment that the publisher on PyPI names. It goes through uv publish rather than pypa's action, so it needs no action beyond the setup-uv this repository already pins and Dependabot already moves. --- .github/workflows/release.yml | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4e46ef9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2026 The Pybricks Authors + +name: Release + +# One package per tag, because the packages are released independently: the +# tag pybricks-core-v1.0.0 publishes pybricks-core 1.0.0 and nothing else. +# Tag pybricks-core first, then pybricks-client and pybricks-service, then +# pybricks-cli, so that each one's requirements are on PyPI before it is. +on: + push: + tags: ["pybricks-*-v*"] + +permissions: + contents: read + +env: + UV_FROZEN: "1" + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: { persist-credentials: false } + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + # A tag that disagrees with the package it names is refused before + # anything is built. A version on PyPI can never be uploaded again, so a + # mistake caught after the upload is a version number lost. + - name: Check the tag against the package + shell: bash + env: + TAG: ${{ github.ref_name }} + run: | + package="${TAG%-v*}" + version="${TAG##*-v}" + dir="packages/pb-${package#pybricks-}" + test -f "$dir/pyproject.toml" || { echo "::error::$TAG names no package"; exit 1; } + actual="$(uv version --package "$package" --short)" + test "$actual" = "$version" || { echo "::error::$TAG, but $package is $actual"; exit 1; } + grep -qF "## [$version]" "$dir/CHANGELOG.md" || { echo "::error::$dir/CHANGELOG.md has no [$version] section"; exit 1; } + echo "PACKAGE=$package" >> "$GITHUB_ENV" + - run: uv build --package "$PACKAGE" --out-dir dist + - run: uvx twine check --strict dist/* + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: { name: dist, path: dist/ } + + publish: + needs: build + # The PyPI trusted publisher for each package names this workflow and this + # environment, so a token is never stored anywhere. + environment: + name: pypi + permissions: + id-token: write # OIDC, which is how PyPI knows the upload is from here + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: { name: dist, path: dist } + - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - run: uv publish --trusted-publishing always dist/*