chore: add a dry_run input so both release workflows can be rehearsed before the first publish - #7
Conversation
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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEPCT55iN4DzyuibCoQCa2
Self-review (carried-over PR, adopted this cycle)This pull request was opened by a previous cycle and no self-review had been recorded against it, so the rubric was run now, against head Rubric
Local verification performed this cycleThe external anchor is not applicable in the ordinary sense, since no anchor-relevant file is changed. What could be checked mechanically was checked:
FindingsThree observations are raised. None is judged blocking, and none was fixed in place, because each is either a disclosed trade-off or a latent condition rather than a defect in the current dispatch-only shape.
Backlog deferred this cycleThe cycle was scoped to this carried-over pull request, so no new work was selected. Both open issues are left untouched for reasons outside the repository rather than by preference: #6 is blocked on a pending publisher being registered on PyPI and on the Merge dispositionNot merged autonomously. This comment was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener). drafted by Claude on behalf of Daniel Stephenson |
Why
Both release workflows here are
workflow_dispatchonly, and neither has ever been run. Neither takes adry_runinput, so the moment the npm secret and the PyPI publisher are configured, the next dispatch uploads for real. This is the only library in the set with no rehearsal.That matters most on PyPI, where a version number can never be reused: a
0.1.0uploaded by mistake cannot be replaced, only yanked, and0.1.1becomes the first usable version. Tracked in #6, which proposes this as a prerequisite rather than a nice-to-have, and referenced from #5.kingdom-community/session-clienthas the same two-registry shape and both of its release workflows already take adry_runboolean defaulting totrue. That pattern is adopted here rather than a new one.What changed
A
dry_runinput,type: boolean,default: true, is added torelease-npm.ymlandrelease-pypi.yml. A dispatch with the defaults builds, tests, packs or builds the distributions, checks them, and stops short of the registry.Each workflow is split into a
buildjob and apublishjob. The publish job carriesneeds: build,if: ${{ !inputs.dry_run }}, the deployment environment, andid-token: write.Why the split on both, rather than gating the publish step in place
session-clientuses both shapes, and the difference between them tracks exactly one thing: whether an environment is declared. Itsrelease-npm.ymlhas no environment and gates the step in place; itsrelease-pypi.ymldeclaresenvironment: pypiand splits the job. Both workflows in this repository declare an environment —environment: npmatrelease-npm.ymlandenvironment: pypiatrelease-pypi.yml— so both take the split shape.Gating in place would have left the job-level
environment:on the only job, so a dry run would enter and touch thenpmorpypideployment environment, create a deployment record, and — once those environments exist and carry reviewers — sit waiting on an approval for a run that publishes nothing. That defeats the point of a rehearsal. Following the sibling repository's own rule rather than one of its two files verbatim keeps this consistent with it: no third pattern is introduced.A side effect worth noting:
id-token: writewas declared at the top level of both workflows and now sits on the publish job only, so the build job — the job a dry run actually runs — no longer holds a token exchangeable for publishing rights.npm
build: checkout,setup-node,npm ci,typecheck,test,build, thennpm pack --dry-run, which prints the exact file list a publish would upload. AReport (dry run)step then states plainly that nothing was published and names the dist-tag that would have been used.publish: unchanged in substance.npm publish --provenance --access public --tag "${{ inputs.tag }}"is byte-identical to the command onmain, with the sameNODE_AUTH_TOKENsecret. Thetaginput is preserved, including itslatestdefault.npm ciandnpm run buildrather than consuming an artifact from the build job. That repeats one build, but it keeps the publish command and its workspace context exactly as they are today, which seemed the right trade in a workflow that cannot be executed to prove a change. Thepackstep above is the rehearsal.PyPI
build: the existing steps, pluspython -m twine check dist/*and anupload-artifactofpython/dist/, mirroringsession-client. The dry run now checks the distributions it builds rather than only building them.publish: downloads that artifact todist/and runspypa/gh-action-pypi-publish@release/v1. The publish job consumes the distributions that were built and checked, rather than rebuilding them — on PyPI, what is checked and what is uploaded should be the same files.On
packages-dir: python/distThat path was checked rather than assumed, and it was correct as written.
defaults.run.working-directory: pythondoes not apply to auses:step, sopackages-dirwas being resolved against the workspace root;python -m buildruns under thepythonworking directory and writes topython/dist, sopython/distfrom the workspace root was the right value. It was not a latent bug.It is nonetheless gone from this version, because the split changes where the files are. The publish job downloads the artifact into
dist/at the workspace root, which is the action's own default, so the input is no longer needed. Confirmed locally that a build frompython/producespython/dist/github_docs-0.1.0.tar.gzandgithub_docs-0.1.0-py3-none-any.whl.What was verified, and what was not
Verified locally:
actionlintv1.7.7 on both changed workflows plusci.ymlif:expressions, theinputs.dry_runcontext and the action inputsactionlintonsession-client's two release workflows, as a comparison baselinejs:npm ci,npm run typecheck,npm testjs:npm run buildthennpm pack --dry-run@kingdom-community/github-docs@0.1.0, 22.6 kBpython:pip install .thenpython -m unittest discover -s testspython:python -m buildthenpython -m twine check dist/*twine checkstep is known to pass on this treegit statusNot verified, and not claimed to be: neither workflow was executed. Publishing is blocked on a missing organisation secret and an unconfigured PyPI publisher, and firing a release workflow was out of scope regardless — an accidental dispatch is precisely the irreversible event this change exists to prevent. So nothing here demonstrates the dry-run path end to end on a runner, that the
publishjob is skipped as intended on a dry run, that the artifact hand-off between the two PyPI jobs works on Actions, or that the publish path still works after the split. Those become observable on the first real dispatch, which is what thedry_rundefault now makes safe to attempt.The Python tests were run under Python 3.11 rather than the 3.12 the workflow pins, because 3.12 is not available on this machine; the system interpreter is 3.8, below the project's
requires-python = ">=3.9".ci.ymlalready exercises the supported matrix.This adds a safety input and relocates the publish steps. No version was bumped, no tag was created, no workflow was dispatched.
Refs #6, #5.
drafted by Claude on behalf of Daniel Stephenson
🤖 Generated with Claude Code
https://claude.ai/code/session_01TEPCT55iN4DzyuibCoQCa2