Report the version the package actually is - #191
Merged
Conversation
|
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 |
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
force-pushed
the
feat/version-consistency
branch
from
August 28, 2026 12:04
5bb6ca5 to
ace8674
Compare
Contributor
Author
|
/review |
daniloradovic
approved these changes
Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five surfaces answer "which version is this?" — the manifest, the two places the lockfile records it, the tarball name
npm packderives, an SBOM built from a checkout, andpatchstack-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,
Publishopens a pull request onchore/record-published-versionbringingpackage.jsonandpackage-lock.jsonto the version that was published. Never auto-merged.RELEASING.mddescribes this.--versionis 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 answersunknownrather than throwing, because no command should fail over a diagnostic string.tests/package-version.test.tsholds 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 answeringunknownon 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 rejectingunknown.On this branch all six surfaces report
0.3.31, including a real git installation.Publication, verification and recording are three things
npm publishcannot 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.
publishends when npm accepts the tarball and outputs the version.verify-publishedruns the registry canary, and a failure there turns the workflow red.record-versiondepends on both and is gated onneeds.publish.resultalone.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 withsuccess(), so the in-job invariant check must not be able to gate the pull request: that step carriescontinue-on-error, the pull-request step carries!cancelled(), and a final step fails the job once the pull request exists. The body reportssteps.verify.outcomeand 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.tsnames the steps that must survive an earlier failure and asserts each keeps a status function in its condition, that the verification step stayscontinue-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.ymlinvokesgh workflow run publish.yml, so a job conditioned ongithub.event_name == 'release'never runs there. The condition is on the publish result instead; a dry-run dispatch leavespublishskipped 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.tsparses the workflows and puts every bash script throughbash -n. A syntax check and nothing more, which is what this class of error needs.publish,verify-publishedandrecord-versionare 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 acquiringshell: pwshcannot 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-versiondispatches CI on its branch and carriesactions: writeto do so, since job-levelpermissionsreplaces 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_TOKENis the only thing that removes the manual step.1724 tests, typecheck, build and all 7 consumer shapes pass.