From 22b19edb3c38d305028c8feca5ed7e050343b252 Mon Sep 17 00:00:00 2001 From: Phil Pennock Date: Mon, 23 Mar 2026 11:54:07 -0400 Subject: [PATCH 1/2] Add GitHub CI workflow --- .github/workflows/ci.yaml | 53 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..1db74ca --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,53 @@ +# This workflow MUST be audited with zizmor. +# This workflow SHOULD be using pinned action refs. + +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: # let someone trigger the CI on their branch without a PR + +permissions: {} + +jobs: + + test: + name: Test + + runs-on: ubuntu-slim + + permissions: + contents: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + with: + version: "0.10.11" + enable-cache: true # not a release workflow, caching is fine + + - name: "Set up Python" + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version-file: "pyproject.toml" + + - name: Install the project + run: uv sync --locked --all-extras --dev + + # Run the tests before the lint and type-check, so that we at least can + # see useful results before going and fixing nits. + + - name: Test across Python versions + run: uv run tox + + - name: Lint + run: uv run ruff check . + + - name: Type-check + run: uv run ty check . From cbfce04507d21c78257fe9ec41a0335778d91ace Mon Sep 17 00:00:00 2001 From: Phil Pennock Date: Mon, 23 Mar 2026 12:32:27 -0400 Subject: [PATCH 2/2] workflow pedantic cleanups * pass `zizmor --persona=pedantic .` * ensure release workflow_dispatch is resilient against draft release already existing * for the CI workflow, drop the explict setup-python because we're going to let tox-uv just manage everything. I might drop for the release workflow too, I need to mull over the audit implications for the version used for the actual release though ... _should_ be fine. This was an optimisation stright from the setup-uv documentation on efficiency but I think it became a non-issue when I added multi-Python tests via tox. --- .github/workflows/ci.yaml | 9 ++++----- .github/workflows/release.yaml | 27 ++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1db74ca..26c6ea4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,10 @@ on: permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: test: @@ -32,11 +36,6 @@ jobs: version: "0.10.11" enable-cache: true # not a release workflow, caching is fine - - name: "Set up Python" - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version-file: "pyproject.toml" - - name: Install the project run: uv sync --locked --all-extras --dev diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b049deb..240bbe6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,11 +16,28 @@ on: workflow_dispatch: inputs: tag: + # The tag should still exist, but use this if some transient error means that + # we failed to start, or complete. + # + # If we did start and it was just a failure to upload, you should consider + # re-running from the failed jobs in the existing action run, instead + # of a new run. description: "The tag to release (e.g., v1.0.0)" required: true permissions: {} +# We don't want to interrupt an existing flow, and we do (or should!) have a +# tag protection ruleset preventing updates/deletions/force-pushes for v* +# +# But if something goes wrong where I want to manually create a release, we do +# have workflow_dispatch. For that, we don't cancel and we don't guard on a +# release existing. My assumption is that the draft release will have been created +# but a transient error blocked the release (PyPI service outage or somesuch). +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + jobs: plan-release: @@ -55,7 +72,15 @@ jobs: run: | # NOTE: No quotes around PRERELEASE_FLAG so that it expands to either `--prerelease` or an empty string, # rather than an empty argument. - gh release create --repo "$GITHUB_REPOSITORY" --draft --verify-tag ${PRERELEASE_FLAG} "${RELEASE_TAG}" --title "${RELEASE_TAG}" + set -e # no 'u' (even though PRERELEASE_FLAG should always exist, just usually be empty) + if ! gh release create --repo "$GITHUB_REPOSITORY" --draft --verify-tag ${PRERELEASE_FLAG} "${RELEASE_TAG}" --title "${RELEASE_TAG}" + then + # record some information about the already-existing release in our output + gh release view --json id,tagName,createdAt,isDraft,isPrerelease,isImmutable "${RELEASE_TAG}" + # if that fails, then it doesn't exist and we failed to create, so abort under `-set -e` is correct + # Now, refuse to proceed if the release is no longer draft + gh release view --json isDraft "${RELEASE_TAG}" | jq -er .isDraft + fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ needs.plan-release.outputs.release-tag }}