-
Notifications
You must be signed in to change notification settings - Fork 6
fix(cli): pin init's installs to the release's own package versions (#661) #666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| --- | ||
| 'stash': patch | ||
| --- | ||
|
|
||
| `stash init` now pins the packages it installs (`@cipherstash/stack`, the | ||
| integration adapter, and `stash` itself) to the exact versions this CLI | ||
| release was built alongside, instead of installing bare package names that | ||
| resolve through npm dist-tags (#661). During a pre-release window dist-tags | ||
| lag or point at placeholders, so an unpinned `init` could silently deliver a | ||
| different release than the CLI driving the setup — stale `@cipherstash/stack`, | ||
| or an empty placeholder adapter — breaking `/v3` imports out of the box. The | ||
| versions are embedded at build time from the release train itself | ||
| (`src/release-train.ts`, the single source both the build and the runtime | ||
| check against), so they can never disagree with what was published together. | ||
|
|
||
| Init also now surfaces **version skew** on already-installed packages — | ||
| unconditionally, before any prompt or early exit, including when the install | ||
| is declined or partially fails. Interactively it offers to align the skewed | ||
| packages in the same confirm as the missing installs (keeping `stash` a dev | ||
| dependency); non-interactively it never mutates an existing install — it | ||
| warns and prints the exact align commands. A package whose manifest exists | ||
| but can't be read (an aborted install) is reported as skew, not treated as | ||
| matching. All other install guidance is pinned the same way: the | ||
| missing-package hints, `.cipherstash/context.json`'s `installCommand`, the | ||
| `install-eql` manual note, the native-module recovery hint (previously | ||
| `stash@latest`), and the `stash wizard` one-shot spawn (previously an | ||
| unpinned `npx @cipherstash/wizard`). The `stash-cli` skill documents the | ||
| behaviour, and the other bundled skills' manual install commands now carry a | ||
| verify-what-resolved note. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import { readFileSync } from 'node:fs' | ||
| import { dirname, resolve } from 'node:path' | ||
| import { fileURLToPath } from 'node:url' | ||
| import { describe, expect, it } from 'vitest' | ||
| import { INTEGRATION_ADAPTER_PACKAGES } from '../commands/init/steps/install-deps.js' | ||
| import { RELEASE_TRAIN_MANIFESTS } from '../release-train.js' | ||
|
|
||
| const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..') | ||
|
|
||
| // The growth guard for #661: `RELEASE_TRAIN_MANIFESTS` is the single source | ||
| // the build embeds versions from, and `INTEGRATION_ADAPTER_PACKAGES` is what | ||
| // init installs. An adapter present in the second but absent from the first | ||
| // would install UNPINNED and be invisible to the skew warning — silently | ||
| // reintroducing the dist-tag failure mode on exactly the newest package. This | ||
| // suite turns that omission into a red test. | ||
| describe('release train coverage', () => { | ||
| it('every integration adapter package rides the release train', () => { | ||
| for (const pkg of Object.values(INTEGRATION_ADAPTER_PACKAGES)) { | ||
| expect( | ||
| RELEASE_TRAIN_MANIFESTS, | ||
| `${pkg} is installed by init but missing from RELEASE_TRAIN_MANIFESTS — it would install unpinned`, | ||
| ).toHaveProperty([pkg]) | ||
| } | ||
| }) | ||
|
|
||
| it('the core packages and every one-shot-executed package are on the train', () => { | ||
| // stash: init self-installs it; @cipherstash/stack: the client; | ||
| // @cipherstash/wizard: EXECUTED via `npx` from `stash wizard` / the impl | ||
| // handoff, so an unpinned run would execute a different release. | ||
| for (const pkg of ['stash', '@cipherstash/stack', '@cipherstash/wizard']) { | ||
| expect(RELEASE_TRAIN_MANIFESTS).toHaveProperty([pkg]) | ||
| } | ||
| }) | ||
|
|
||
| it('every train manifest exists and carries a version (what tsup will embed)', () => { | ||
| // Exercises the exact inputs tsup.config.ts reads at build time, so a | ||
| // renamed/moved workspace package fails HERE in source-mode tests, not | ||
| // only at the next build. | ||
| for (const [pkg, rel] of Object.entries(RELEASE_TRAIN_MANIFESTS)) { | ||
| const manifest: unknown = JSON.parse( | ||
| readFileSync(resolve(CLI_ROOT, rel), 'utf8'), | ||
| ) | ||
| const m = manifest as { name?: unknown; version?: unknown } | ||
| expect(m.name, `${rel} package name`).toBe(pkg) | ||
| expect(typeof m.version, `${pkg} version`).toBe('string') | ||
| expect((m.version as string).length).toBeGreaterThan(0) | ||
| } | ||
| }) | ||
| }) |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { | ||
| expectedVersion, | ||
| parseEmbeddedVersions, | ||
| pinnedSpec, | ||
| RUNTIME_PACKAGE_VERSIONS, | ||
| } from '../runtime-versions.js' | ||
|
|
||
| // Source-mode runs (this test) never see the tsup define, so the embedded map | ||
| // must be empty and every helper must degrade to the unpinned behaviour — | ||
| // that fallback is itself part of the contract (dev/`tsx` runs keep working). | ||
| describe('runtime-versions (no build-time embed)', () => { | ||
| it('exposes an empty version map', () => { | ||
| expect(RUNTIME_PACKAGE_VERSIONS).toEqual({}) | ||
| }) | ||
|
|
||
| it('pinnedSpec falls back to the bare package name', () => { | ||
| expect(pinnedSpec('@cipherstash/stack')).toBe('@cipherstash/stack') | ||
| }) | ||
|
|
||
| it('expectedVersion is undefined', () => { | ||
| expect(expectedVersion('@cipherstash/stack')).toBeUndefined() | ||
| }) | ||
| }) | ||
|
|
||
| // The embed parser is what stands between a build defect and silently | ||
| // unpinned installs (#661): absent (source mode) is fine; present-but-broken | ||
| // must THROW, never degrade to {}. | ||
| describe('parseEmbeddedVersions', () => { | ||
| it('absent embed (source mode) yields an empty map', () => { | ||
| expect(parseEmbeddedVersions(undefined)).toEqual({}) | ||
| }) | ||
|
|
||
| it('parses a valid embed', () => { | ||
| expect( | ||
| parseEmbeddedVersions('{"stash":"9.9.9-test.1","@x/y":"8.8.8-test.2"}'), | ||
| ).toEqual({ stash: '9.9.9-test.1', '@x/y': '8.8.8-test.2' }) | ||
| }) | ||
|
|
||
| it('throws on unparseable JSON instead of degrading to unpinned', () => { | ||
| expect(() => parseEmbeddedVersions('{not json')).toThrow( | ||
| /build defect.*not valid JSON/s, | ||
| ) | ||
| }) | ||
|
|
||
| it('throws on a non-object shape', () => { | ||
| expect(() => parseEmbeddedVersions('["stash"]')).toThrow( | ||
| /not a plain object/, | ||
| ) | ||
| expect(() => parseEmbeddedVersions('null')).toThrow(/not a plain object/) | ||
| expect(() => parseEmbeddedVersions('"9.9.9"')).toThrow(/not a plain object/) | ||
| }) | ||
|
|
||
| it('throws on a missing/non-string version value', () => { | ||
| expect(() => parseEmbeddedVersions('{"stash":""}')).toThrow( | ||
| /usable version for "stash"/, | ||
| ) | ||
| expect(() => parseEmbeddedVersions('{"stash":42}')).toThrow( | ||
| /usable version for "stash"/, | ||
| ) | ||
| }) | ||
| }) | ||
|
|
||
| describe('runtime-versions (explicit version map)', () => { | ||
| // Arbitrary FIXTURE versions, deliberately unreal: these tests assert the | ||
| // map is threaded through verbatim, not that any actual release exists. | ||
| // Real versions are never hard-coded anywhere — the shipped map is read | ||
| // from the workspace manifests at build time (tsup.config.ts). | ||
| const versions = { | ||
| stash: '9.9.9-test.1', | ||
| '@cipherstash/stack': '9.9.9-test.1', | ||
| '@cipherstash/prisma-next': '8.8.8-test.2', | ||
| } | ||
|
|
||
| it('pins known packages to the release version', () => { | ||
| expect(pinnedSpec('@cipherstash/stack', versions)).toBe( | ||
| '@cipherstash/stack@9.9.9-test.1', | ||
| ) | ||
| // prisma-next versions on its own line — the map, not a shared constant, | ||
| // is the source of truth. | ||
| expect(pinnedSpec('@cipherstash/prisma-next', versions)).toBe( | ||
| '@cipherstash/prisma-next@8.8.8-test.2', | ||
| ) | ||
| }) | ||
|
|
||
| it('leaves unknown packages unpinned', () => { | ||
| expect(pinnedSpec('@cipherstash/not-on-the-train', versions)).toBe( | ||
| '@cipherstash/not-on-the-train', | ||
| ) | ||
| }) | ||
|
|
||
| it('expectedVersion reads the map', () => { | ||
| expect(expectedVersion('stash', versions)).toBe('9.9.9-test.1') | ||
| expect(expectedVersion('nope', versions)).toBeUndefined() | ||
| }) | ||
| }) |
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.