Skip to content

ci(release): publish on release published, and verify tag matches version - #21

Closed
saby1101 wants to merge 1 commit into
mainfrom
ci/release-hardening
Closed

saby1101 wants to merge 1 commit into
mainfrom
ci/release-hardening

Conversation

@saby1101

Copy link
Copy Markdown
Member

Batch 3 of the review findings. Workflow and packaging only — no source changes.

The trigger was wrong for drafts

on: release: types: [created]. GitHub's semantics:

  • created fires when a draft release is saved → publishes from the default branch, before the tag exists
  • published fires when a draft is released → the current workflow does not run, so the real release ships nothing

Every release so far was created and published in one action, which is why it has worked:

v2.15.0  draft=false  created 04:55:30  published 04:55:33
v2.14.0  draft=false  created 12:24:20  published 12:24:52

One click on "Save draft" breaks it in both directions. Now types: [published].

Tag and version could disagree silently

The job published whatever package.json.version was at the release commit, regardless of the tag. A mismatch either 409s confusingly (duplicate version) or publishes a number nobody will find.

A guard now compares them:

TAG=v2.15.0 vs v2.15.0 -> passes
TAG=v9.9.9  vs v2.15.0 -> FAILS

The tag is passed through env: rather than interpolated into the script, so there is no injection surface even though release.tag_name is attacker-influenceable in principle.

publishConfig.registry

Absent. CI publishes correctly only because setup-node's registry-url writes the runner .npmrc. A maintainer running npm publish locally would target npmjs.org, build via prepublishOnly, then fail against a scope we do not own. Now pinned to GitHub Packages.

Also

lint and build join test in the pre-publish gate, matching test.yml.

Verification

All three workflows parse as valid YAML with the expected triggers; the publish steps parse as shell; the guard was exercised both ways. Nothing here changes the package contents — pnpm install --frozen-lockfile still succeeds and the source is untouched.

Cannot be end-to-end tested without cutting a real release, which is the nature of a publish workflow. That is also why the --provenance idea stayed out earlier: GitHub Packages rejects attestations, and the publish job is the one place a mistake is expensive.

…sion

The trigger was release:created. GitHub fires created when a draft release is saved,
which would publish from the default branch before the tag exists; it then fires
published (not created) when that draft is released, so the real release would ship
nothing. Every release so far was created and published in one action, which is why
this never bit.

The job also published whatever version happened to be in package.json at the release
commit, so a tag and a version could disagree silently. A guard now compares them and
fails the job.

publishConfig.registry pins GitHub Packages. CI worked only because setup-node writes
the runner .npmrc; a local npm publish would target npmjs.org and fail against a scope
we do not own.

Lint and build join the pre-publish test gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saby1101

Copy link
Copy Markdown
Member Author

Superseded by #23, which carries these commits unchanged. Splitting this into separate reviews added overhead without adding safety.

@saby1101 saby1101 closed this Aug 14, 2026
@saby1101
saby1101 deleted the ci/release-hardening branch August 14, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant