Skip to content

Report the version the package actually is - #191

Merged
patchstackdave merged 6 commits into
mainfrom
feat/version-consistency
Aug 28, 2026
Merged

Report the version the package actually is#191
patchstackdave merged 6 commits into
mainfrom
feat/version-consistency

Conversation

@patchstackdave

@patchstackdave patchstackdave commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Five surfaces answer "which version is this?" — the manifest, the two places the lockfile records it, the tarball name npm pack derives, an SBOM built from a checkout, and patchstack-connect --version. Only the published tarball takes its answer from the tag; everything read out of the repository takes it from the committed manifest. For a package whose purpose is to shield known vulnerabilities, a manifest naming the wrong version means someone believing they have a fix they do not have.

What this establishes

The tag is the trigger and the source of truth for what is published, which is what lets a release happen with no pre-commit and therefore work with branch protection. After a successful publish, Publish opens a pull request on chore/record-published-version bringing package.json and package-lock.json to the version that was published. Never auto-merged. RELEASING.md describes this.

--version is new. It reads the manifest at run time rather than having a value baked in at build time, so a partial or stale build cannot report a wrong version, and it answers unknown rather than throwing, because no command should fail over a diagnostic string.

tests/package-version.test.ts holds the surfaces checkable from a checkout: a plain release version in the manifest, the lockfile agreeing in both places, the name agreeing, the tarball name following from the manifest, the built CLI reporting it, and the fallback answering unknown on exit 0 both when the manifest has no version and when there is no manifest at all. The consumer matrix covers the one link a checkout cannot — the installed bin reporting the version npm resolved, checked against the tarball's own manifest and rejecting unknown.

On this branch all six surfaces report 0.3.31, including a real git installation.

Publication, verification and recording are three things

npm publish cannot be undone, so the canary that installs the published tarball and attacks the engine inside it runs past the point of no return: its failure is a discovery about something already released.

Each is therefore its own step in the graph. publish ends when npm accepts the tarball and outputs the version. verify-published runs the registry canary, and a failure there turns the workflow red. record-version depends on both and is gated on needs.publish.result alone.

The same reasoning applies inside record-version, where it is easier to get wrong. A step condition containing no status-check function is implicitly ANDed with success(), so the in-job invariant check must not be able to gate the pull request: that step carries continue-on-error, the pull-request step carries !cancelled(), and a final step fails the job once the pull request exists. The body reports steps.verify.outcome and the tarball verification result, and says plainly when either failed — for a failed tarball, that the version is on npm, cannot be unpublished, and needs a deprecation decision before the pull request is treated as routine.

tests/workflow-shell.test.ts names the steps that must survive an earlier failure and asserts each keeps a status function in its condition, that the verification step stays continue-on-error, and that the final step's own script exits non-zero when executed. All three are load-bearing: without the flag the job stops regardless of any condition below it, and a final step that runs but exits zero leaves a green workflow over a release whose surfaces disagree.

Two things this also fixes about the release gate

The recommended release path is workflow_dispatch. release.yml invokes gh workflow run publish.yml, so a job conditioned on github.event_name == 'release' never runs there. The condition is on the publish result instead; a dry-run dispatch leaves publish skipped rather than successful and is excluded by it.

Publish validation builds before testing, matching the main CI, because several tests assert against dist/ and return early without it.

Checking the scripts that CI cannot reach

A run: block is a program nothing checks — not the type checker, and usually not CI either, because a job only runs on the events it is configured for. A release-only job's script is first executed during a release.

tests/workflow-shell.test.ts parses the workflows and puts every bash script through bash -n. A syntax check and nothing more, which is what this class of error needs. publish, verify-published and record-version are all named as jobs that must stay covered, since none of them runs on a pull request. Every bash script in every workflow is checked; the PowerShell steps it cannot check are named in the test, so a bash block acquiring shell: pwsh cannot leave coverage unnoticed.

One expression is worth noting: the verification result is read as needs['verify-published'].result. A hyphen in a dotted property path is ambiguous with subtraction in GitHub's expression grammar, and the failure mode is silent — an empty string, which would trip the warning branch on every successful release.

CI on the automated pull request

record-version dispatches CI on its branch and carries actions: write to do so, since job-level permissions replaces the default set rather than adding to it.

A dispatched run does not attach to a pull request and therefore does not satisfy a required status. It proves the branch is green; closing and reopening the pull request is what produces the required checks. Both facts are in the body it writes. Opening it with an app installation token instead of GITHUB_TOKEN is the only thing that removes the manual step.

1724 tests, typecheck, build and all 7 consumer shapes pass.

@coderbuds

coderbuds Bot commented Aug 28, 2026

Copy link
Copy Markdown

Comprehensive version-consistency overhaul with thorough CI and CLI support.

🎯 Quality: 93% Elite · 📦 Size: Large — consider splitting if possible

📈 This month: Your 150th PR — above team average · Averaging Excellent

See how your team is trending →

patchstackdave and others added 6 commits August 28, 2026 14:03
The committed version was a permanent placeholder: the tag was the only source
of truth, `Publish` wrote the version into `package.json` in CI, and nothing
came back to `main`. Sound for the published tarball, wrong for every other
surface that answers "which version is this?" — a git installation, an SBOM
built from a checkout, `npm pack` from the repository, and the CLI all reported
0.3.6 while 0.3.31 was current.

The tag stays the trigger and the source of truth. `Publish` now proposes the
matching commit to `main` afterwards, as a pull request rather than a push
because branch protection is the point of branch protection, and never
auto-merged. The manifest and lockfile are brought to 0.3.31 here.

Adds `--version`, which did not exist, reading the manifest at run time rather
than baking a constant in at build time, so a partial build cannot leave it
stale. `tests/package-version.test.ts` pins the surfaces checkable from the
repository; the consumer matrix proves the installed bin reports the version npm
resolved, which nothing in this repository can stand in for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The recommended release path runs `gh workflow run publish.yml`, which arrives as
`workflow_dispatch` — so gating the new job on `github.event_name == 'release'`
skipped it on the path that publishes most versions while the workflow still
reported success. `needs: publish` is the correct gate on its own: a dry-run
dispatch skips `publish`, which skips this with it.

The publish validation now builds before testing, matching the main CI. Several
tests assert against `dist/` and return early without it, so the previous order
let the release gate pass having skipped the checks that cover what ships.

The fallback test now exercises the fallback. It ran the normal CLI and asserted
the output was not `unknown`, which passes whether the fallback works, rethrows
or prints an empty line. Two cases replace it — a manifest with no version, and
no manifest at all, the second being the only one that reaches the catch — both
asserting `unknown` on exit 0. Reverting the fallback to `''` or to a rethrow is
now caught; before, neither was.

CI is dispatched on the branch after the pull request is opened, and the body
says plainly that a dispatched run does not attach and therefore does not
satisfy a required status.

Workflow comments and release documentation state the current invariant rather
than the history behind it.

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

A `run:` block is a program nothing checks. The type checker does not see it, and
CI usually does not either, because a job only runs on the events it is
configured for — so a release-only job's script is first executed during a
release. An unterminated quote in the pull-request body written by
`record-version` was exactly that: correct YAML, invalid bash, invisible to every
check that gates a merge.

`tests/workflow-shell.test.ts` parses the workflows and puts every bash script
through `bash -n`. It is a syntax check and nothing more, which is enough for the
class of error that is otherwise undetectable until it fires. It asserts that the
release-only jobs are among those covered, and pins the two PowerShell steps it
cannot check so a bash block acquiring `shell: pwsh` cannot leave coverage
unnoticed.

`record-version` also needed `actions: write`. Job-level `permissions` replaces
the default set rather than adding to it, so every unlisted scope is `none` and
the CI dispatch could only ever take its non-fatal warning path — leaving the
branch with no run while the workflow reported success.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`npm publish` cannot be undone. The canary that installs the published tarball
and attacks the engine inside it therefore runs after the point of no return, and
its failure is a discovery about something already released rather than something
prevented.

Inside the `publish` job, that discovery failed the job — which skipped
`record-version` and produced exactly the state this change exists to prevent: a
version irreversibly on npm, a tag pointing at it, `main` still naming the
previous version, and no pull request proposing the correction. Drift, during the
release where accurate state matters most.

Publication now ends when npm accepts the tarball. `verify-published` is its own
job, so the workflow still goes red on a bad artifact, and `record-version`
depends on both but is gated on `needs.publish.result` alone: the version is on
npm either way, and the repository naming a different one is a second problem
rather than a safeguard against the first. The generated pull request states the
verification result, and says plainly when it failed that the version needs a
deprecation decision before the pull request is treated as routine.

`always()` also keeps a dry-run dispatch excluded, where `publish` is skipped
rather than successful.

The verification result is read as `needs['verify-published'].result`. A hyphen
in a dotted property path is ambiguous with subtraction in GitHub's expression
grammar, and the failure mode is silent — an empty string, which would have
tripped the warning branch on every successful release.

`verify-published` joins the release-only jobs the workflow shell check asserts
coverage of, since its scripts are first executed during a release too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A step condition containing no status-check function is implicitly ANDed with
`success()`, so the pull-request step was gated on nothing having failed — and the
in-job verification exiting non-zero skipped it. The state that leaves is the one
the job exists to prevent: the version published, `main` naming a different one,
and no correction proposed.

The verification step now carries `continue-on-error`, the pull-request step
condition carries `!cancelled()`, and a final step fails the job once the pull
request exists. `steps.verify.outcome` carries the real result into the body,
which says explicitly when the surfaces disagreed that the pull request may not
be sufficient on its own.

`tests/workflow-shell.test.ts` names the steps that must survive an earlier
failure and asserts each keeps a status function in its condition, plus that the
verification step stays `continue-on-error`. Both halves are needed: without the
flag the job stops regardless of any condition below it.

Workflow comments state the invariant rather than the path to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The structural assertions prove the step runs after a failed check. They said
nothing about whether it fails anything, and a step that runs and exits zero
leaves a green workflow over a release whose version surfaces disagree.

The step's own script is now executed and its exit status asserted non-zero.
Interpolations are left as written, since bash treats them as text and
substituting them would test something other than what the runner receives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@patchstackdave
patchstackdave force-pushed the feat/version-consistency branch from 5bb6ca5 to ace8674 Compare August 28, 2026 12:04
@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave
patchstackdave merged commit 95ed007 into main Aug 28, 2026
12 checks passed
@patchstackdave
patchstackdave deleted the feat/version-consistency branch August 28, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants