mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-11-06 20:58:14 -04:00
* feat(install): enforce and guard the zero-warning npm install contract
`npm install -g @gitlawb/openclaude` is verified zero-warning today, but
nothing kept it that way: the runtime deps were caret ranges resolved
fresh on every user install (the published tarball ships no lockfile),
no CI step ever installed the package, and registry-side drift (a
transitive dep deprecated after we ship) is invisible to file-based CI.
Static contract (fast, offline, every PR via `bun run build`):
- Pin the 3 runtime deps to exact versions so the verified resolution IS
the shipped resolution.
- New validators in scripts/externalsValidation.ts (unit-tested):
dependencies must equal RUNTIME_DEPENDENCY_CONTRACT exactly (no ranges,
no unreviewed additions), no consumer-run install hooks or funding
field, engines.node pinned. Wired into validate-externals.ts.
Runtime verification (scripts/verify-clean-install.ts, `install:verify`):
- Tarball mode (release gate) and published mode (registry watch), each
running cold-install and upgrade-over-previous scenarios in throwaway
prefixes with a cold cache and normalized env/flags.
- Strict output whitelist (summary lines only) with network failures
retried and reported as infra (exit 2), never as a hygiene verdict.
- Structural authority over the installed tree: any package declaring
install scripts fails, the installed manifest must match the static
contract, tarball payload/size asserted.
- Boot must be silent: --version prints the exact packed version;
--help (which, unlike the --version zero-import fast path, loads the
real bundle) must exit 0 with empty stderr.
CI: release publishes only after the verify passes on Node 22 (npm 10,
the supported floor — warning phrasing and EBADENGINE behavior differ
from npm 11) and Node 24, plus a final gate on the publishing machine
replacing `npm pack --dry-run`. A daily install-hygiene workflow
re-verifies the published @latest on {ubuntu, macos, windows} x
{Node 22, 24} — the only defense against post-release registry drift,
and the OS matrix covers the per-platform @vscode/ripgrep packages.
Found-by-the-guard fix: a fresh machine printed "Warning: ignoring saved
provider profile. OPENGATEWAY_API_KEY is required..." on every command
(even --help) because the injected fresh-install Opengateway default
fails validation without a key (#1651 chose ignore+warn). The default
env is still ignored, but the warning now only fires for genuinely
persisted profiles; published 0.24.0 carries the old noise, so the
verify script exempts exactly that version until the next release.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(install): address CodeRabbit review on the install-hygiene guard
- release.yml: pin install-verify to least-privilege `contents: read` and
disable credential persistence on its checkout; same persist-credentials
hardening on the install-hygiene cron checkout.
- verify-clean-install: previousPublishedVersion now follows the same
retry/infra discipline as installWithRetry — transient registry failures
retry and then exit 2 (infra) instead of silently skipping the
upgrade-scenario coverage; a clean not-published answer still skips.
- providerProfile: the fresh-install warning suppression now keys on
explicit provenance (persisted profile resolved once in
applyStartupEnvFromProfile) instead of sniffing the
DEFAULT_STARTUP_PROVIDER_ENV_VAR marker, which a persisted profile's
env can inherit from a parent CLI process; regression test covers the
marker-collision case.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* test(install): cover previousPublishedVersion retry/skip/infra branches
CodeRabbit follow-up: the branches deciding whether the upgrade-install
scenario runs, skips, or aborts as infra were untested. Extract the loop
as resolvePreviousPublishedVersion with injected effects (runView,
onRetry, onInfraFailure) per the repo's dependency-injection testing
convention, guard main() behind import.meta.main so the test import does
not launch a real verification, and add regression tests: first-try
success, transient-infra retry then success, clean E404 → null skip
without retries, persistent infra → onInfraFailure (exit 2 in the real
wiring), and unparseable version output → null.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
77 lines
2.7 KiB
TypeScript
77 lines
2.7 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { resolvePreviousPublishedVersion } from './verify-clean-install.js'
|
|
|
|
// The retry/skip/infra branches decide whether the upgrade-install scenario
|
|
// runs, is skipped, or aborts as an infra failure — regression-covered here
|
|
// with injected npm results (the real script wires runView to `npm view` and
|
|
// onInfraFailure to process.exit(2)).
|
|
|
|
const ok = (version: string) => ({ status: 0, stdout: `${version}\n`, stderr: '' })
|
|
const infraFail = { status: 1, stdout: '', stderr: 'npm error network ECONNRESET while fetching' }
|
|
const notPublished = { status: 1, stdout: '', stderr: 'npm error code E404\nnpm error 404 Not Found' }
|
|
|
|
class InfraExit extends Error {
|
|
constructor(readonly combined: string) {
|
|
super('infra exit')
|
|
}
|
|
}
|
|
|
|
function run(results: Array<{ status: number; stdout: string; stderr: string }>, retries = 3) {
|
|
let calls = 0
|
|
const retryAttempts: number[] = []
|
|
const value = resolvePreviousPublishedVersion({
|
|
runView: () => {
|
|
const result = results[calls]
|
|
calls++
|
|
if (!result) throw new Error(`runView called ${calls} times, only ${results.length} results provided`)
|
|
return result
|
|
},
|
|
onRetry: attempt => retryAttempts.push(attempt),
|
|
onInfraFailure: combined => {
|
|
throw new InfraExit(combined)
|
|
},
|
|
retries,
|
|
})
|
|
return { value, calls, retryAttempts }
|
|
}
|
|
|
|
describe('resolvePreviousPublishedVersion', () => {
|
|
test('returns the version on first success without retrying', () => {
|
|
const { value, calls, retryAttempts } = run([ok('0.24.0')])
|
|
expect(value).toBe('0.24.0')
|
|
expect(calls).toBe(1)
|
|
expect(retryAttempts).toEqual([])
|
|
})
|
|
|
|
test('transient infra failure retries and then succeeds', () => {
|
|
const { value, calls, retryAttempts } = run([infraFail, infraFail, ok('0.24.0')])
|
|
expect(value).toBe('0.24.0')
|
|
expect(calls).toBe(3)
|
|
expect(retryAttempts).toEqual([1, 2])
|
|
})
|
|
|
|
test('clean unavailability (E404) returns null immediately — skip, not infra', () => {
|
|
const { value, calls, retryAttempts } = run([notPublished])
|
|
expect(value).toBeNull()
|
|
expect(calls).toBe(1)
|
|
expect(retryAttempts).toEqual([])
|
|
})
|
|
|
|
test('persistent infra failure invokes onInfraFailure after exhausting retries', () => {
|
|
let caught: InfraExit | null = null
|
|
try {
|
|
run([infraFail, infraFail, infraFail])
|
|
} catch (error) {
|
|
caught = error as InfraExit
|
|
}
|
|
expect(caught).toBeInstanceOf(InfraExit)
|
|
expect(caught!.combined).toContain('ECONNRESET')
|
|
})
|
|
|
|
test('unparseable success output returns null rather than a bogus version', () => {
|
|
const { value } = run([ok('not-a-version')])
|
|
expect(value).toBeNull()
|
|
})
|
|
})
|