From 990d2ba3b6f120996cb237760d4dce2e7af7ecc1 Mon Sep 17 00:00:00 2001 From: Daniel Young Date: Thu, 23 Jul 2026 12:21:55 -0400 Subject: [PATCH] [ci] Add test workflow and gate releases on it The repo had no CI: 114 tests only ever ran on a laptop, and release.yml went tag -> build -> publish with nothing in between. `python -m build` never imports the package, so an ImportError would have published fine. PyPI versions are immutable, so that mistake costs a version number. - ci.yml runs ruff + pytest on push and PR, macOS (the only supported platform), on 3.11 and 3.13 to bracket requires-python. - release.yml now needs a passing suite, and asserts the tag matches tailctl.__version__ before publishing. - pyproject reads the version from src/tailctl/__init__.py instead of duplicating it, so the two can no longer disagree. - checkout/setup-python bumped to v7, clearing the Node 20 deprecation. Solves: unverified release path, duplicated version string Tests: ruff + pytest green locally; python -m build produces 0.1.1 from the dynamic version --- .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++ .github/workflows/release.yml | 40 +++++++++++++++++++++++++++++------ pyproject.toml | 6 +++++- 3 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef2dcd8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: ci + +# Lint + unit tests. macOS only: tailctl drives a userspace tailscaled on macOS +# and its path resolution assumes Homebrew prefixes. 3.11 and 3.13 bracket the +# requires-python range. + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.13'] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + - run: python -m pip install --upgrade pip + - run: pip install -e '.[dev]' + - run: ruff check src tests + - run: pytest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01a0a23..7aa5e61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,16 +3,20 @@ name: release # Publishes tailctl to PyPI via OIDC trusted publishing on a version tag. # No stored tokens: the job mints a short-lived, workflow-scoped credential from PyPI. # -# Prerequisite (one-time, human, in the PyPI UI) — BEFORE the first tag that uses this: +# Nothing publishes until the suite passes and the tag matches the packaged +# version. PyPI versions are immutable, so a bad publish burns that number for +# good — the gate is much cheaper than the recovery. +# +# Prerequisite (one-time, human, in the PyPI UI) — already configured: # PyPI project `tailctl` -> Settings -> Publishing -> Add a trusted publisher: # Owner: DRYCodeWorks # Repository: tailctl # Workflow name: release.yml # Environment: (leave blank) -# (`tailctl` already exists on PyPI, so this is a normal trusted publisher, not a pending one.) # -# Release: bump `version` in pyproject.toml, commit, then push a matching tag: -# git tag v0.1.1 && git push origin v0.1.1 +# Release: bump `__version__` in src/tailctl/__init__.py (pyproject reads it +# from there), commit, then push a matching tag: +# git tag v0.1.2 && git push origin v0.1.2 on: push: @@ -23,15 +27,37 @@ permissions: contents: read jobs: + test: + runs-on: macos-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: '3.13' + - run: pip install -e '.[dev]' + - run: ruff check src tests + - run: pytest + + - name: Verify tag matches package version + run: | + tag="${GITHUB_REF_NAME#v}" + pkg="$(python -c 'import tailctl; print(tailctl.__version__)')" + if [ "$tag" != "$pkg" ]; then + echo "::error::tag v$tag does not match package version $pkg" + exit 1 + fi + echo "tag v$tag matches package version $pkg" + pypi: + needs: test runs-on: ubuntu-latest permissions: id-token: write # OIDC: mint the PyPI publish credential steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: - python-version: '3.12' + python-version: '3.13' - run: python -m pip install --upgrade build - run: python -m build - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/pyproject.toml b/pyproject.toml index a343f7e..cc5d13b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "tailctl" -version = "0.1.1" +# Single source of truth: src/tailctl/__init__.py. See [tool.setuptools.dynamic]. +dynamic = ["version"] description = "Per-identity Tailscale networking for parallel sessions on a single Mac" readme = "README.md" license = { text = "MIT" } @@ -57,6 +58,9 @@ dev = [ [project.scripts] tailctl = "tailctl.cli:main" +[tool.setuptools.dynamic] +version = { attr = "tailctl.__version__" } + [tool.setuptools.packages.find] where = ["src"]