Skip to content

Commit 106f36c

Browse files
Brooooooklynclaude
andcommitted
fix(cli): export OIDC-minted token to build env so prerender getToken sees it (review)
The single-step GitHub Actions workflow drops the curl/jq step that wrote `VOID_TOKEN` to `$GITHUB_ENV`. `void deploy` now mints the deploy token in-process, but assigned it only to a local variable. The downstream build and prerender re-read credentials via `getToken()` (which reads `process.env.VOID_TOKEN` first) — both in-process (`collectPrerenderPaths`) and inside the spawned `vite build` child (`plugin-prerender`'s closeBundle, which inherits `process.env` via `buildEnv`). A local variable reaches neither, so in CI the prerender injected no `__VOID_TOKEN`/`__VOID_PROJECT_ID` and rendered against local/empty bindings (AI, remote D1/KV/R2 break) — a regression from the removed scaffold step. Export the minted token to `process.env.VOID_TOKEN` right after masking, inside the `!token` OIDC branch so an explicit `VOID_TOKEN` is never overwritten. This restores exact parity with the removed `$GITHUB_ENV` write. The token is the same deploy-scoped (`aud: deploy`, short-TTL) token the old workflow exposed to the build, so visibility is parity, not new exposure. The `command === 'serve'` gate on `__VOID_TOKEN` worker-var injection is untouched, so this does not leak the token into `dist/ssr/wrangler.json`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e5faca6 commit 106f36c

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

‎packages/void/src/cli/deploy.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,18 @@ async function runDeployInner(
404404
cliLog.info('github_oidc_exchange_start', { slug });
405405
token = await exchangeGithubOidcForDeployToken(getApiUrl(root), slug);
406406
maskGithubActionsSecret(token);
407+
// Export the minted token so the downstream build/prerender can reach
408+
// it. Both prerender paths re-read credentials via `getToken()`, which
409+
// reads `process.env.VOID_TOKEN` first: the in-process path
410+
// (`collectPrerenderPaths` below) and the spawned `vite build` child
411+
// (`plugin-prerender`'s closeBundle, which inherits `process.env` via
412+
// `buildEnv`). A local variable can't reach either, so without this the
413+
// prerender injects no `__VOID_TOKEN`/`__VOID_PROJECT_ID` and renders
414+
// against local/empty bindings (AI, remote D1/KV/R2 break). This
415+
// restores parity with the removed scaffold step that wrote
416+
// `VOID_TOKEN` to `$GITHUB_ENV`. Guarded by the enclosing `!token`
417+
// branch, so an explicit `VOID_TOKEN` is never overwritten.
418+
process.env.VOID_TOKEN = token;
407419
cliLog.info('github_oidc_exchange_success', { slug });
408420
}
409421
if (!token) {

‎packages/void/test/unit/deploy-auth.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const mockExchangeOidc = vi.fn();
2020
const mockMaskSecret = vi.fn();
2121

2222
const originalIsTTY = process.stdin.isTTY;
23+
let savedVoidToken: string | undefined;
2324

2425
vi.mock('@clack/prompts', () => ({
2526
intro: vi.fn(),
@@ -92,6 +93,11 @@ beforeEach(() => {
9293
});
9394
mockPromptForLoginToken.mockResolvedValue('fresh-token');
9495
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
96+
97+
// Isolate process.env.VOID_TOKEN across tests: the OIDC branch now exports
98+
// the minted token there, so start each test clean and restore afterwards.
99+
savedVoidToken = process.env.VOID_TOKEN;
100+
delete process.env.VOID_TOKEN;
95101
});
96102

97103
describe('runDeploy auth failures', () => {
@@ -150,6 +156,35 @@ describe('runDeploy GitHub Actions OIDC exchange', () => {
150156
expect(mockMaskSecret).not.toHaveBeenCalled();
151157
});
152158

159+
it('exports the minted token to process.env.VOID_TOKEN so the build/prerender getToken sees it', async () => {
160+
mockGetToken.mockReturnValue(null);
161+
mockIsOidcAvailable.mockReturnValue(true);
162+
mockExchangeOidc.mockResolvedValue('minted-token');
163+
164+
// The deploy throws later (listProjects rejects), but the OIDC branch runs
165+
// first. The minted token must land in process.env.VOID_TOKEN — the
166+
// downstream `vite build` child (which inherits process.env) and the
167+
// in-process prerender both re-read it via getToken(), which reads
168+
// process.env.VOID_TOKEN first. A local variable can't reach either.
169+
await expect(runDeploy(process.cwd(), { projectSlug: 'my-app' })).rejects.toThrow();
170+
171+
expect(process.env.VOID_TOKEN).toBe('minted-token');
172+
});
173+
174+
it('does NOT overwrite an explicit VOID_TOKEN in process.env (no mint)', async () => {
175+
// getToken returns a token (explicit VOID_TOKEN / keychain / file), so the
176+
// OIDC branch is skipped and the existing process.env.VOID_TOKEN is left
177+
// untouched — no re-mint, no clobber.
178+
mockGetToken.mockReturnValue('explicit-token');
179+
mockIsOidcAvailable.mockReturnValue(true);
180+
process.env.VOID_TOKEN = 'explicit-env-token';
181+
182+
await expect(runDeploy(process.cwd(), { projectSlug: 'my-app' })).rejects.toThrow();
183+
184+
expect(mockExchangeOidc).not.toHaveBeenCalled();
185+
expect(process.env.VOID_TOKEN).toBe('explicit-env-token');
186+
});
187+
153188
it('errors when in Actions but the project is unknown', async () => {
154189
mockGetToken.mockReturnValue(null);
155190
mockIsOidcAvailable.mockReturnValue(true);
@@ -297,4 +332,9 @@ describe('buildEnv', () => {
297332

298333
afterEach(() => {
299334
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
335+
if (savedVoidToken === undefined) {
336+
delete process.env.VOID_TOKEN;
337+
} else {
338+
process.env.VOID_TOKEN = savedVoidToken;
339+
}
300340
});

0 commit comments

Comments
 (0)