Two structural issues in the release workflow, found while reviewing #21. Neither blocks 0.1.0, and both are better addressed once the first release has actually run.
1. The dry run does not stop; the guarantee is a convention
The step named "Stop here on a dry run" only writes a summary. Every step after it — commit and tag, create the Release, publish, summarise — carries its own if: ${{ !inputs.dry_run }}.
That is correct today; all of them have it. The problem is what happens next time someone adds a step. A new publish target, a registry mirror, a notification: forget the guard, and a run invoked with dry_run: true executes it. For anything that publishes, that is unrecoverable, since npm never allows a version to be replaced.
Suggested fix: split into two jobs — one that validates and prepares, one that releases, with needs: and a single if: ${{ !inputs.dry_run }} on the second. The guarantee then comes from the job graph rather than from remembering to repeat a condition.
2. The release commit is pushed directly to main
git push origin HEAD:"${GITHUB_REF_NAME}"
Branch protection cannot be configured on this repository yet — it is private on a free plan, and the API returns 403 — so this works now. Once the repo is public and main is protected against direct pushes, the GITHUB_TOKEN actor is rejected and the release stops after the checks and version bump, before the tag is pushed. Nothing is published, so it is recoverable, but no release can proceed until it is resolved.
Two ways out, and the choice depends on how main ends up protected:
- exempt the Actions bot from the push restriction, keeping the workflow as-is
- have the workflow open a pull request with the version commit instead of pushing, and tag once it merges — safer, but turns a one-step release into two
Why later
The fix for 2 depends on protection settings that do not exist yet. The fix for 1 is a restructure, and doing it immediately before the first real release would change the shape of the workflow right before its only untested path runs. Better to let 0.1.0 exercise the reviewed version, then improve with evidence.
Two structural issues in the release workflow, found while reviewing #21. Neither blocks 0.1.0, and both are better addressed once the first release has actually run.
1. The dry run does not stop; the guarantee is a convention
The step named "Stop here on a dry run" only writes a summary. Every step after it — commit and tag, create the Release, publish, summarise — carries its own
if: ${{ !inputs.dry_run }}.That is correct today; all of them have it. The problem is what happens next time someone adds a step. A new publish target, a registry mirror, a notification: forget the guard, and a run invoked with
dry_run: trueexecutes it. For anything that publishes, that is unrecoverable, since npm never allows a version to be replaced.Suggested fix: split into two jobs — one that validates and prepares, one that releases, with
needs:and a singleif: ${{ !inputs.dry_run }}on the second. The guarantee then comes from the job graph rather than from remembering to repeat a condition.2. The release commit is pushed directly to
maingit push origin HEAD:"${GITHUB_REF_NAME}"Branch protection cannot be configured on this repository yet — it is private on a free plan, and the API returns 403 — so this works now. Once the repo is public and
mainis protected against direct pushes, theGITHUB_TOKENactor is rejected and the release stops after the checks and version bump, before the tag is pushed. Nothing is published, so it is recoverable, but no release can proceed until it is resolved.Two ways out, and the choice depends on how
mainends up protected:Why later
The fix for 2 depends on protection settings that do not exist yet. The fix for 1 is a restructure, and doing it immediately before the first real release would change the shape of the workflow right before its only untested path runs. Better to let 0.1.0 exercise the reviewed version, then improve with evidence.