From e675d97eff405b611cf4793e763bda41dbe5faba Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Sun, 6 Sep 2026 21:07:32 -0600 Subject: [PATCH] chore: make both release workflows rehearsable with a dry_run input A `dry_run` input, defaulting to true, is added to `release-npm.yml` and `release-pypi.yml`, matching the pattern already established in `kingdom-community/session-client`. Neither workflow has ever been dispatched, so without this the first dispatch after the credentials and the publisher are configured would upload for real. On PyPI that is irreversible: a version number can never be reused, so a wrong 0.1.0 could only be yanked, never replaced. Both workflows are split into a build job and a publish job, with the `if: ${{ !inputs.dry_run }}` gate and the deployment environment placed on the publish job. This is `session-client`'s `release-pypi.yml` shape, applied here to both files because both declare an environment: gating in place would leave a dry run entering the `npm` or `pypi` environment for a run that publishes nothing. `id-token: write` moves down with it, so the build job no longer holds a publishing token. The `tag` input on the npm workflow, the publish commands and the trusted-publishing arrangement are unchanged. A `twine check` step is added so the PyPI dry run checks the distributions it builds, as `session-client` does, and the built distributions are handed to the publish job as an artifact rather than rebuilt. drafted by Claude on behalf of Daniel Stephenson Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TEPCT55iN4DzyuibCoQCa2 --- .github/workflows/release-npm.yml | 47 ++++++++++++++++++++++++++--- .github/workflows/release-pypi.yml | 48 +++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index bd7e685..564be89 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -1,6 +1,11 @@ name: Release (npm) # Manual only. A release is a decision, not a side effect of a merge. +# +# The default is a rehearsal. `dry_run` is true unless it is deliberately +# turned off, so a dispatch builds, tests and packs the tarball and then stops +# short of the registry. The publish job is a separate job so that the `npm` +# environment is only entered by a run that actually publishes. on: workflow_dispatch: inputs: @@ -8,16 +13,18 @@ on: description: 'npm dist-tag to publish under' required: false default: 'latest' + dry_run: + description: 'Build, test and pack without publishing' + type: boolean + default: true permissions: contents: read - id-token: write # npm provenance jobs: - publish: - name: Publish @kingdom-community/github-docs + build: + name: Build @kingdom-community/github-docs runs-on: ubuntu-latest - environment: npm defaults: run: working-directory: js @@ -34,6 +41,38 @@ jobs: - run: npm run typecheck - run: npm test - run: npm run build + # Prints the exact file list that a publish would upload. + - name: Pack + run: npm pack --dry-run + - name: Report (dry run) + if: ${{ inputs.dry_run }} + run: | + echo "Dry run: nothing was published." + echo "The tarball contents listed above are what would have gone to" + echo "npm under dist-tag '${{ inputs.tag }}'." + + publish: + name: Publish @kingdom-community/github-docs + needs: build + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + environment: npm + permissions: + contents: read + id-token: write # npm provenance + defaults: + run: + working-directory: js + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + cache: npm + cache-dependency-path: js/package-lock.json + - run: npm ci + - run: npm run build - run: npm publish --provenance --access public --tag "${{ inputs.tag }}" env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml index 74951fd..e75a531 100644 --- a/.github/workflows/release-pypi.yml +++ b/.github/workflows/release-pypi.yml @@ -1,18 +1,28 @@ name: Release (PyPI) # Manual only. A release is a decision, not a side effect of a merge. +# +# The default is a rehearsal. `dry_run` is true unless it is deliberately +# turned off, so a dispatch builds, tests and checks the distributions and then +# stops short of the upload. A PyPI version number can never be reused - a +# wrong 0.1.0 can only be yanked, never replaced - so the first run is worth +# rehearsing. The publish job is a separate job so that the `pypi` environment +# is only entered by a run that actually uploads. on: workflow_dispatch: + inputs: + dry_run: + description: 'Build and check the distributions without publishing' + type: boolean + default: true permissions: contents: read - id-token: write # PyPI trusted publishing jobs: - publish: - name: Publish github-docs + build: + name: Build github-docs runs-on: ubuntu-latest - environment: pypi defaults: run: working-directory: python @@ -21,13 +31,37 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' - - run: python -m pip install --upgrade pip build + - run: python -m pip install --upgrade pip build twine - run: python -m pip install . # Never publish something that was not just proven to pass. - run: python -m unittest discover -s tests -v - run: python -m build + - run: python -m twine check dist/* + - uses: actions/upload-artifact@v4 + with: + name: distributions + path: python/dist/ + - name: Report (dry run) + if: ${{ inputs.dry_run }} + run: | + echo "Dry run: nothing was uploaded to PyPI." + echo "These distributions would have been published:" + ls -l dist + + publish: + name: Publish github-docs + needs: build + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # PyPI trusted publishing + steps: + # The distributions that were built and checked, not a fresh build. + - uses: actions/download-artifact@v4 + with: + name: distributions + path: dist/ # Trusted publishing: configure this repo + workflow as a publisher on # PyPI, and no long-lived API token has to exist anywhere. - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: python/dist