Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 9 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,32 +173,23 @@ with `void secret put ADMIN_TOKEN`); without it configured the write endpoints
return 503. `GET /-/refs` is a public read.

```bash
# List configured refs (static env + runtime R2 index) - no auth required.
# Each entry: { ref, version, publishedAt, prUrl, expiresAt }. publishedAt/prUrl
# are null until the ref is published (prUrl only when published from a PR);
# expiresAt is the index TTL (null for static env refs, which never expire).
# List registered refs - no auth required.
# Each entry: { ref, version, publishedAt, prUrl, expiresAt }. publishedAt is the
# server-stamped release date; prUrl is null unless published from a PR; expiresAt
# is the index TTL (90 days out).
curl https://.../-/refs

# Register a ref at runtime (no redeploy).
curl -X POST -H "authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
-d '{"ref":"commit.a832a55"}' https://.../-/refs

# Unregister a ref
curl -X DELETE -H "authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
-d '{"ref":"commit.a832a55"}' https://.../-/refs

# Purge a generated build (its tarball + meta) from R2
curl -X POST -H "authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
-d '{"package":"vite-plus","version":"0.0.0-commit.a832a55"}' https://.../-/purge
```

Tarball upload (`PUT /-/tarball/<pkg>/<version>.tgz`) and publish
(`POST /-/publish`, stores metadata + registers the ref) are also admin-guarded;
they are driven by the [publish action](#publishing-from-ci), not by hand.
Refs are created by publishing: tarball upload (`PUT /-/tarball/<pkg>/<version>.tgz`)
then `POST /-/publish` (stores metadata + registers the ref), both admin-guarded
and driven by the [publish action](#publishing-from-ci), not by hand.

A registered ref is reflected immediately and built into the packument on the
next request (and into R2 on first fetch). This is the no-redeploy path for
exposing new pkg.pr.new builds.
A published ref is reflected immediately and built into the packument on the next
request. This is the no-redeploy path for exposing new pkg.pr.new builds.

### Publishing from CI

Expand Down
46 changes: 2 additions & 44 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ import {
parsePreviewVersion,
shaToVersion,
} from './preview/parsePreviewVersion'
import { parseConfiguredPreviewRefs } from './preview/parseConfiguredPreviewRefs'
import {
getConfiguredRefs,
getConfiguredRefsWithEtag,
latestVersionByPr,
registerRef,
unregisterRef,
} from './preview/getConfiguredRefs'
import {
parseNpmTarballPath,
Expand Down Expand Up @@ -265,7 +263,7 @@ app.post('/-/publish', async (c) => {
return c.json({ ref, version: parsed.version, published }, 201)
})

/** List the configured preview refs (static env + runtime R2 index). Public read. */
/** List the registered preview refs (the runtime R2 index). Public read. */
app.get('/-/refs', async (c) => {
const refs = await getConfiguredRefs(c.env)
return c.json({
Expand All @@ -274,52 +272,12 @@ app.get('/-/refs', async (c) => {
version: r.version,
publishedAt: r.publishedAt ?? null,
prUrl: r.prUrl ?? null,
// ISO TTL; null for static env refs, which never expire.
// ISO TTL (90 days out), refreshed on each (re)publish.
expiresAt: r.expiresAt ? new Date(r.expiresAt).toISOString() : null,
})),
})
})

/**
* Register a preview ref at runtime (no redeploy). Body:
* `{ "ref": "commit.<sha>" }`.
*/
app.post('/-/refs', async (c) => {
admin(c)
const body = (await c.req.json().catch(() => ({}))) as { ref?: string }
const ref = (body.ref ?? '').trim()

let parsed
try {
;[parsed] = parseConfiguredPreviewRefs(ref)
} catch {
throw new HttpError(400, `Invalid ref: ${ref || '(empty)'}`)
}

try {
await registerRef(c.env, ref)
} catch (err) {
throw new HttpError(503, String(err))
}
// The tarballs/integrity are built and uploaded by the publish action (CI);
// until then this ref's packument uses name-derived platform metas and the
// tarball endpoint redirects platform binaries to pkg.pr.new.
return c.json({ added: ref, version: parsed.version }, 201)
})

/** Unregister a runtime preview ref. Body: `{ "ref": "commit.<sha>" }`. */
app.delete('/-/refs', async (c) => {
admin(c)
const body = (await c.req.json().catch(() => ({}))) as { ref?: string }
const ref = (body.ref ?? '').trim()
try {
await unregisterRef(c.env, ref)
} catch (err) {
throw new HttpError(400, String(err))
}
return c.json({ removed: ref })
})

/**
* Purge a generated build from R2. Body:
* `{ "package": "vite-plus", "version": "0.0.0-commit.<sha>" }`.
Expand Down
8 changes: 0 additions & 8 deletions src/preview/getConfiguredRefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,3 @@ export async function registerRef(
})
return parsed
}

/** Remove a runtime-registered ref. */
export async function unregisterRef(env: Env, ref: string): Promise<void> {
const [parsed] = parseConfiguredPreviewRefs(ref)
await mutateRefIndex(env, (index) => {
delete index[canonical(parsed)]
})
}
8 changes: 8 additions & 0 deletions test/buildPreviewTarball.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ async function makeUpstream(pkg: Record<string, any>) {

describe('buildPreviewTarball', () => {
it('rewrites package.json name/version/deps and preserves other files', async () => {
const binSha = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef'
const upstream = await makeUpstream({
name: '@voidzero-dev/vite-plus-core',
version: '1891',
dependencies: { 'vite-plus': '1891', picomatch: '^2.3.1' },
optionalDependencies: {
'@voidzero-dev/vite-plus-darwin-arm64': `https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-darwin-arm64@${binSha}`,
},
bin: { vp: './bin/vp' },
})

Expand All @@ -45,6 +49,10 @@ describe('buildPreviewTarball', () => {
expect(build.packageJson.version).toBe('0.0.0-commit.a832a55')
expect(build.packageJson.dependencies['vite-plus']).toBe('0.0.0-commit.a832a55')
expect(build.packageJson.dependencies.picomatch).toBe('^2.3.1')
// pkg.pr.new optionalDependency URLs are rewritten to version strings.
expect(
build.packageJson.optionalDependencies['@voidzero-dev/vite-plus-darwin-arm64'],
).toBe(`0.0.0-commit.${binSha}`)

const files = await parseTarGzip(build.tarball)

Expand Down
158 changes: 76 additions & 82 deletions test/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,50 @@ afterAll(() => {
vi.unstubAllGlobals()
})

// The suite's fixture preview ref (`commit.a832a55`). It used to be injected via
// the VITE_PLUS_PREVIEW_REFS env var; refs are now runtime-only, so register it
// dynamically before each test (idempotent, so it survives storage isolation).
// The suite's fixture preview ref (`commit.a832a55`), published before each test
// (idempotent, so it survives storage isolation). Publishing vite-plus + core
// with their rewritten package.json is exactly what a CI publish stores, so
// packuments inject the ref the way they do in production. (`POST /-/refs`
// register-only was removed; a ref exists only once it has been published.)
const FIXTURE_VERSION = '0.0.0-commit.a832a55'
beforeEach(async () => {
await SELF.fetch(`${BASE}/-/refs`, {
await SELF.fetch(`${BASE}/-/publish`, {
method: 'POST',
headers: {
authorization: 'Bearer test-admin-token',
'content-type': 'application/json',
},
body: JSON.stringify({ ref: 'commit.a832a55' }),
body: JSON.stringify({
ref: 'commit.a832a55',
packages: [
{
name: 'vite-plus',
version: FIXTURE_VERSION,
packageJson: {
name: 'vite-plus',
version: FIXTURE_VERSION,
dependencies: { '@voidzero-dev/vite-plus-core': FIXTURE_VERSION },
optionalDependencies: {
'@voidzero-dev/vite-plus-darwin-arm64': `0.0.0-commit.${PLATFORM_SHA}`,
},
bin: { vp: './bin/vp' },
},
integrity: 'sha512-Zm9vYmFy',
shasum: 'a'.repeat(40),
},
{
name: '@voidzero-dev/vite-plus-core',
version: FIXTURE_VERSION,
packageJson: {
name: '@voidzero-dev/vite-plus-core',
version: FIXTURE_VERSION,
dependencies: { 'vite-plus': FIXTURE_VERSION },
},
integrity: 'sha512-Y29yZQ',
shasum: 'b'.repeat(40),
},
],
}),
})
})

Expand Down Expand Up @@ -364,26 +397,19 @@ describe('packument endpoint', () => {
})

it('injects the version into a platform package packument with os/cpu', async () => {
// Register the platform commit ref so the platform packument exposes it.
await SELF.fetch(`${BASE}/-/refs`, {
method: 'POST',
headers: {
authorization: 'Bearer test-admin-token',
'content-type': 'application/json',
},
body: JSON.stringify({ ref: `commit.${PLATFORM_SHA}` }),
})
// The fixture ref has no darwin meta published, so the platform packument
// derives os/cpu from the package name (platformMetaFromName).
const res = await SELF.fetch(
`${BASE}/@voidzero-dev%2Fvite-plus-darwin-arm64`,
{ headers: { accept: 'application/json' } },
)
const body = (await res.json()) as Record<string, any>
const v = body.versions[`0.0.0-commit.${PLATFORM_SHA}`]
const v = body.versions[FIXTURE_VERSION]
expect(v).toBeTruthy()
expect(v.os).toEqual(['darwin'])
expect(v.cpu).toEqual(['arm64'])
expect(v.dist.tarball).toBe(
`${BASE}/tarballs/@voidzero-dev/vite-plus-darwin-arm64/0.0.0-commit.${PLATFORM_SHA}.tgz`,
`${BASE}/tarballs/@voidzero-dev/vite-plus-darwin-arm64/${FIXTURE_VERSION}.tgz`,
)
})

Expand Down Expand Up @@ -724,19 +750,6 @@ describe('CORS', () => {
})

describe('admin: refs', () => {
it('writes require auth (reads do not)', async () => {
// POST/DELETE require the bearer token.
expect(
(await SELF.fetch(`${BASE}/-/refs`, { method: 'POST' })).status,
).toBe(401)
expect(
(await SELF.fetch(`${BASE}/-/refs`, {
method: 'POST',
headers: { authorization: 'Bearer wrong' },
})).status,
).toBe(401)
})

it('lists the registered refs without auth (public read)', async () => {
const res = await SELF.fetch(`${BASE}/-/refs`)
expect(res.status).toBe(200)
Expand Down Expand Up @@ -868,71 +881,52 @@ describe('admin: refs', () => {
expect(body['dist-tags']['pr-777']).toBe(verB)
})

it('registers a ref and injects it into the packument', async () => {
const add = await SELF.fetch(`${BASE}/-/refs`, {
method: 'POST',
headers: { ...AUTH, 'content-type': 'application/json' },
body: JSON.stringify({ ref: 'commit.a832a55' }),
})
expect(add.status).toBe(201)
expect((await add.json()) as any).toMatchObject({
version: '0.0.0-commit.a832a55',
})

// The dynamically registered ref now appears in the packument.
const pack = await SELF.fetch(`${BASE}/vite-plus`, {
headers: { accept: 'application/json' },
})
const body = (await pack.json()) as Record<string, any>
expect(body.versions['0.0.0-commit.a832a55']).toBeTruthy()
})

it('rejects an invalid ref', async () => {
const res = await SELF.fetch(`${BASE}/-/refs`, {
it('rejects an invalid ref (publish)', async () => {
const res = await SELF.fetch(`${BASE}/-/publish`, {
method: 'POST',
headers: { ...AUTH, 'content-type': 'application/json' },
body: JSON.stringify({ ref: 'nonsense' }),
body: JSON.stringify({
ref: 'nonsense',
packages: [
{
name: 'vite-plus',
version: '0.0.0-commit.abc1234',
packageJson: { name: 'vite-plus', version: '0.0.0-commit.abc1234' },
integrity: 'sha512-x',
shasum: 'x',
},
],
}),
})
expect(res.status).toBe(400)
})

it('registers concurrently without overwriting (incremental)', async () => {
const a = 'commit.aaaaaaa'
const b = 'commit.bbbbbbb'
const post = (ref: string) =>
SELF.fetch(`${BASE}/-/refs`, {
it('publishes concurrently without overwriting (incremental)', async () => {
const publish = (sha: string) =>
SELF.fetch(`${BASE}/-/publish`, {
method: 'POST',
headers: { ...AUTH, 'content-type': 'application/json' },
body: JSON.stringify({ ref }),
body: JSON.stringify({
ref: `commit.${sha}`,
packages: [
{
name: 'vite-plus',
version: `0.0.0-commit.${sha}`,
packageJson: { name: 'vite-plus', version: `0.0.0-commit.${sha}` },
integrity: 'sha512-x',
shasum: 'x',
},
],
}),
})
// Two PRs registering at the same time: both must survive.
await Promise.all([post(a), post(b)])
// Two PRs publishing at the same time: both refs must survive the CAS.
await Promise.all([publish('aaaaaaa'), publish('bbbbbbb')])
const list = (await (
await SELF.fetch(`${BASE}/-/refs`, { headers: AUTH })
await SELF.fetch(`${BASE}/-/refs`)
).json()) as { refs: Array<{ ref: string }> }
const refs = list.refs.map((r) => r.ref)
expect(refs).toContain(a)
expect(refs).toContain(b)
})

it('unregisters a ref', async () => {
// Use a sha that is not the fixture ref, so removal is observable.
const ref = 'commit.beefcafe'
await SELF.fetch(`${BASE}/-/refs`, {
method: 'POST',
headers: { ...AUTH, 'content-type': 'application/json' },
body: JSON.stringify({ ref }),
})
const del = await SELF.fetch(`${BASE}/-/refs`, {
method: 'DELETE',
headers: { ...AUTH, 'content-type': 'application/json' },
body: JSON.stringify({ ref }),
})
expect(del.status).toBe(200)
const list = (await (
await SELF.fetch(`${BASE}/-/refs`, { headers: AUTH })
).json()) as { refs: Array<{ ref: string }> }
expect(list.refs.some((r) => r.ref === ref)).toBe(false)
expect(refs).toContain('commit.aaaaaaa')
expect(refs).toContain('commit.bbbbbbb')
})
})

Expand Down