Skip to content

S0232-cli-release-1: run the outside-the-repo install gate on the release path - #52

Merged
NSchatz merged 2 commits into
mainfrom
sdd/S0232-cli-release-1
Sep 1, 2026
Merged

S0232-cli-release-1: run the outside-the-repo install gate on the release path#52
NSchatz merged 2 commits into
mainfrom
sdd/S0232-cli-release-1

Conversation

@NSchatz

@NSchatz NSchatz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Spec: work/specs/S0232-cli-release-1/spec.md (umbrella).

@cosyte/cli@0.0.1 and 0.0.2 are on npm permanently uninstallable, and the check that catches that
shape was a sentence in a human checklist: a --dry-run packs a tarball but never resolves its
dependencies from a registry. This makes it a step the release path executes.

What is here

  • scripts/release-install-gate.mjs. Packs this tree with pnpm pack, installs the packed
    tarball with npm install into a fresh directory outside the repository working tree (and
    refuses one inside it), executes every declared bin from the installed copy, and refuses a
    local-path specifier (file:, link:, a relative or absolute path) in dependencies,
    optionalDependencies or peerDependencies before it packs anything. devDependencies is
    reported and exempt: no consumer install resolves it, and this manifest carries one on purpose.
  • One rule covers both bin shapes: a process that exits non-zero inside the window did not run.
    cosyte exits 0; cosyte-mcp is a stdio server that never exits on its own and is still serving
    at the end of its window. A process that has exited is never reported as still running, however
    late its pipes close.
  • It fails closed, with a distinct reason per class: install-failed, bin-missing (a packaging
    defect, worded so it does not read like an install failure), bin-failed,
    local-path-specifier, and gate-timeout / gate-registry-unreachable / gate-error for a run
    that reached no verdict at all.
  • .github/workflows/release.yml gains an install-gate job, and the publishing job needs: it
    with no if: on either, so a red or skipped gate stops the publish. The caller's four permission
    grants are unchanged. ci.yml is untouched and no step moved out of its verify job.
  • RELEASING.md now presents the install as something the pipeline does, says what the gate does
    not cover, and keeps the version-string check as the human step it still is.

Negative controls (test/release-install-gate.test.ts, 27 cases)

A deliberately broken consumer specifier reds the gate, in each of the three fields and each shape a
local path takes. This repository's own unmodified manifest passes, and the report shows the gate
saw the file: devDependency rather than passing because it never looked. An installed copy
carrying neither @cosyte/fhir nor @cosyte/transform passes. A bin whose target is not in the
tarball reds as a packaging defect with the install recorded as having exited zero. A bin that exits
non-zero on load reds, naming the bin and its first diagnostic line. A manifest declaring no bin at
all reds rather than passing vacuously. And the workflow reader carries its own negative control, so
a publish job with no needs: cannot pass over it.

Evidence

pnpm lint, pnpm format:check, pnpm typecheck, pnpm test (39 files, 717 tests, was 690),
pnpm test:coverage (core 97.84 / commands 98.53 statements, unchanged: no src/ code here),
pnpm check:no-emdash, pnpm check:no-internal-refs, pnpm check:agent-notes, pnpm phi-scan,
pnpm smoke and pnpm attw all green. The gate itself was run end to end against this tree:
@cosyte/cli packs, installs outside the tree, cosyte exits 0 and cosyte-mcp is still running.

No dependency was added, no manifest field moved, and nothing a consumer receives changed.

…ease path

`@cosyte/cli@0.0.1` and `0.0.2` are on npm permanently uninstallable, and the
check that catches that shape was a sentence in a human checklist: a dry-run
packs a tarball but never resolves its dependencies from a registry.

`scripts/release-install-gate.mjs` packs this tree, installs the packed tarball
with npm into a fresh directory outside the repository working tree (refusing
one inside it), executes every declared bin from that installed copy, and
refuses a local-path specifier in `dependencies`, `optionalDependencies` or
`peerDependencies` before it packs anything. `devDependencies` is reported and
exempt, because no consumer install resolves it and this manifest carries one
on purpose.

Both bin shapes are covered by one rule: a process that exits non-zero inside
the window did not run. `cosyte` exits 0; `cosyte-mcp` is a stdio server that
never exits on its own and is still serving at the end of its window.

It fails closed. Every refusal carries its own reason: `install-failed`,
`bin-missing` (a packaging defect, worded so it does not read like an install
failure), `bin-failed`, `local-path-specifier`, and `gate-timeout` /
`gate-registry-unreachable` / `gate-error` for a run that reached no verdict.

`.github/workflows/release.yml` gains an `install-gate` job and the publishing
job `needs:` it, with no `if:` on either, so a red or skipped gate stops the
publish. The caller's four permission grants are unchanged; `ci.yml` is
untouched and no step moved out of its `verify` job.

No dependency was added and no manifest field moved.
Comment thread scripts/release-install-gate.mjs Fixed
…eck-then-write

CodeQL flagged the `existsSync` then `writeFileSync` pair as a file-system
race (js/file-system-race, high). An exclusive create says the same thing in
one syscall and says it better: a manifest already in the install directory is
left exactly as it is, with no window in between, so a caller-supplied one is
never overwritten and a deliberately broken one stays observable.
@NSchatz
NSchatz merged commit db3f923 into main Sep 1, 2026
8 checks passed
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.

2 participants