44// while all unit tests passed). Run it against staging before promoting to
55// production.
66//
7- // node scripts/smoke-test.mjs https://pkg-pr-registry-bridge-staging.void.app
7+ // node scripts/smoke-test.mjs <base-url> [--write]
88//
99// Each check prints the actual response (status + key fields/headers) BEFORE
1010// asserting, so a failure is debuggable straight from the CI log.
11+ //
12+ // `--write` (staging only, never production) additionally runs the full admin
13+ // write lifecycle end-to-end against the real runtime: upload a tarball,
14+ // publish its meta, assert the version is NOT served until the ref is
15+ // registered, register it, then assert the packument advertises an integrity
16+ // that matches the exact served tarball bytes. This is the publish -> register
17+ // -> serve -> integrity path that two production incidents broke while every
18+ // unit test stayed green; it self-cleans by purging (and unregistering) the
19+ // artifact afterward. The flag is explicit so a missing SMOKE_ADMIN_TOKEN
20+ // fails loudly instead of silently skipping the coverage.
21+
22+ import { createTarGzip } from 'nanotar'
23+ import { createHash } from 'node:crypto'
24+ import { refToVersion } from './lib/config.mjs'
1125
12- const base = ( process . argv [ 2 ] || '' ) . replace ( / \/ + $ / , '' )
26+ const args = process . argv . slice ( 2 )
27+ const WRITE = args . includes ( '--write' )
28+ const base = ( args . find ( ( a ) => ! a . startsWith ( '--' ) ) ?? '' ) . replace ( / \/ + $ / , '' )
1329if ( ! base ) {
14- console . error ( 'usage: node scripts/smoke-test.mjs <base-url>' )
30+ console . error ( 'usage: node scripts/smoke-test.mjs <base-url> [--write]' )
31+ process . exit ( 2 )
32+ }
33+
34+ const ADMIN_TOKEN = process . env . SMOKE_ADMIN_TOKEN
35+ if ( WRITE && ! ADMIN_TOKEN ) {
36+ console . error ( '--write requires SMOKE_ADMIN_TOKEN (the deployment\'s ADMIN_TOKEN)' )
1537 process . exit ( 2 )
1638}
1739
@@ -27,6 +49,104 @@ function assert(cond, msg) {
2749 if ( ! cond ) throw new Error ( msg )
2850}
2951
52+ // npm dist.integrity SRI from tarball bytes.
53+ const sri = ( buf ) => `sha512-${ createHash ( 'sha512' ) . update ( buf ) . digest ( 'base64' ) } `
54+
55+ const postJson = ( path , body ) =>
56+ fetch ( `${ base } ${ path } ` , {
57+ method : 'POST' ,
58+ headers : {
59+ authorization : `Bearer ${ ADMIN_TOKEN } ` ,
60+ 'content-type' : 'application/json' ,
61+ } ,
62+ body : JSON . stringify ( body ) ,
63+ } )
64+
65+ /**
66+ * The full admin write lifecycle against the real runtime. Uses a unique,
67+ * clearly-labelled `e2e` commit sha per run so it never collides with real
68+ * refs, and purges + unregisters the artifact at the end.
69+ */
70+ async function runAdminLifecycle ( ) {
71+ const name = 'vite-plus'
72+ // Unique per run, valid [0-9a-f]{7,40}, recognizable as a smoke artifact.
73+ const ref = `commit.e2e${ Date . now ( ) . toString ( 16 ) } `
74+ const version = refToVersion ( ref )
75+
76+ // Build a real, valid preview tarball and derive its integrity from the exact
77+ // bytes we upload, the same way CI's publish action does.
78+ const tarball = await createTarGzip ( [
79+ { name : 'package/package.json' , data : JSON . stringify ( { name, version } ) } ,
80+ { name : 'package/index.js' , data : 'export const smoke = true\n' } ,
81+ ] )
82+ const integrity = sri ( tarball )
83+
84+ const fetchDist = async ( ) => {
85+ const res = await fetch ( `${ base } /${ name } ` , {
86+ headers : { accept : 'application/vnd.npm.install-v1+json' } ,
87+ } )
88+ const body = await res . json ( )
89+ return body ?. versions ?. [ version ] ?. dist ?? null
90+ }
91+
92+ try {
93+ // 1. Upload the tarball bytes (worker streams them straight into R2).
94+ const up = await fetch ( `${ base } /-/tarball/${ name } /${ version } .tgz` , {
95+ method : 'PUT' ,
96+ headers : { authorization : `Bearer ${ ADMIN_TOKEN } ` , 'content-type' : 'application/gzip' } ,
97+ body : tarball ,
98+ } )
99+ console . log ( ` PUT tarball -> ${ up . status } ` )
100+ assert ( up . status === 201 , `upload status ${ up . status } ` )
101+
102+ // 2. Publish the meta. This must NOT make the version visible on its own.
103+ const shasum = createHash ( 'sha1' ) . update ( tarball ) . digest ( 'hex' )
104+ const pub = await postJson ( '/-/publish' , {
105+ ref,
106+ packages : [ { name, version, packageJson : { name, version } , integrity, shasum } ] ,
107+ } )
108+ console . log ( ` POST /-/publish -> ${ pub . status } ` )
109+ assert ( pub . status === 201 , `publish status ${ pub . status } ` )
110+
111+ // 3. The atomic gate: an unregistered version is invisible in the packument.
112+ const beforeRegister = await fetchDist ( )
113+ console . log ( ` packument before register: ${ beforeRegister ? 'PRESENT (bug)' : 'absent' } ` )
114+ assert ( ! beforeRegister , 'version visible before /-/register (atomic gate broken)' )
115+
116+ // 4. Register the ref: flips the version visible.
117+ const reg = await postJson ( '/-/register' , { ref } )
118+ console . log ( ` POST /-/register -> ${ reg . status } ` )
119+ assert ( reg . status === 201 , `register status ${ reg . status } ` )
120+
121+ // 5. Now it is served, and its advertised integrity must match the bytes
122+ // (the invariant both production incidents violated).
123+ const dist = await fetchDist ( )
124+ console . log ( ` packument after register: integrity=${ dist ?. integrity ?. slice ( 0 , 20 ) } …` )
125+ assert ( dist , 'version absent from packument after register' )
126+ assert ( dist . integrity === integrity , 'packument integrity != uploaded integrity' )
127+
128+ // 6. Fetch the actually-served tarball from the runtime under test and
129+ // confirm its bytes hash to the advertised integrity (catches a stale or
130+ // mismatched served body directly). Use dist.tarball's PATH against `base`
131+ // rather than its absolute URL: a staging deploy sets PUBLIC_BASE_URL to
132+ // the production host, so dist.tarball points at prod, where this staging
133+ // artifact does not exist. We want to verify the bytes THIS runtime serves.
134+ assert ( dist . tarball , 'no dist.tarball in packument' )
135+ const tarUrl = `${ base } ${ new URL ( dist . tarball ) . pathname } `
136+ const tres = await fetch ( tarUrl )
137+ const served = new Uint8Array ( await tres . arrayBuffer ( ) )
138+ const servedIntegrity = sri ( served )
139+ console . log ( ` GET ${ tarUrl } -> ${ tres . status } , served integrity=${ servedIntegrity . slice ( 0 , 20 ) } …` )
140+ assert ( tres . status === 200 , `served tarball status ${ tres . status } ` )
141+ assert ( servedIntegrity === integrity , 'served tarball bytes != advertised integrity' )
142+ } finally {
143+ // Fully clean up: unregister too, or every run leaks a registered e2e ref
144+ // into /-/refs (and packument rebuilds) until the 90-day TTL.
145+ const p = await postJson ( '/-/purge' , { package : name , version, unregister : true } ) . catch ( ( ) => null )
146+ console . log ( ` cleanup: purge+unregister -> ${ p ? p . status : 'error (ignored)' } ` )
147+ }
148+ }
149+
30150const checks = [
31151 {
32152 name : 'GET /_health' ,
@@ -102,6 +222,18 @@ const checks = [
102222 } ,
103223]
104224
225+ if ( WRITE ) {
226+ // Not retried (`retry: false`): a retry after a partial run would false-fail
227+ // the "invisible before register" gate, and the sha is unique per run anyway.
228+ checks . push ( {
229+ name : 'admin publish -> register -> serve -> integrity (lifecycle)' ,
230+ retry : false ,
231+ run : runAdminLifecycle ,
232+ } )
233+ } else {
234+ console . log ( 'read-only mode (pass --write to include the admin lifecycle)' )
235+ }
236+
105237console . log ( `smoke-testing ${ base } ` )
106238
107239// Retry each check until it passes or a shared deadline, to ride out edge
@@ -128,7 +260,7 @@ async function runWithRetry(check) {
128260let failed = 0
129261for ( const check of checks ) {
130262 try {
131- await runWithRetry ( check )
263+ await ( check . retry === false ? check . run ( ) : runWithRetry ( check ) )
132264 console . log ( ` ✓ ${ check . name } ` )
133265 } catch ( err ) {
134266 console . error ( ` ✗ ${ check . name } : ${ err . message } ` )
0 commit comments