From 9d5510325eee5917b8b59738c1ec19587044dfe6 Mon Sep 17 00:00:00 2001 From: John W Date: Thu, 24 Sep 2026 13:52:42 -0700 Subject: [PATCH] feat(wizard-ci): read the e2e gateway key from POSTHOG_GATEWAY_TOKEN Local e2e runs needed a minted phe_ gateway token in a file, which meant a browser login, a weekly mint quota and a 24h expiry. A project secret key (phs_ with the llm_gateway:read scope) works against the gateway directly, so it can live in .env or be injected with `op run`. The runner prefers POSTHOG_GATEWAY_TOKEN, writes it to a 0600 file in a temporary directory for the harness (which only reads WIZARD_CI_GATEWAY_TOKEN_FILE), keeps the variable out of the child env, and removes the directory after the run or on SIGINT/SIGTERM. CI still sets only the file variable, so its path is unchanged. Constraint: the wizard harness reads the gateway credential only from a file Rejected: calling `op read` from the runner | CI has no op CLI, and `op run` needs no code Directive: keep POSTHOG_GATEWAY_TOKEN ahead of the file variable so a stale .env file entry never wins Confidence: high Scope-risk: narrow --- .env.example | 4 ++++ services/wizard-ci/e2e.ts | 40 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 17ada6e04..ad95284f1 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,7 @@ POSTHOG_PERSONAL_API_KEY=phx_... # POSTHOG_REGION is optional, defaults to 'us'. Can also be passed via --region flag or workflow input. # POSTHOG_REGION=us POSTHOG_WIZARD_PROJECT_ID=123 + +# AI gateway key for model calls: a phs_ project secret key with the llm_gateway:read scope. +# With `op run --env-file .env -- pnpm wizard-ci ...` this can be an op:// reference instead of the key. +POSTHOG_GATEWAY_TOKEN=phs_... diff --git a/services/wizard-ci/e2e.ts b/services/wizard-ci/e2e.ts index da5e29677..02c33bf2b 100644 --- a/services/wizard-ci/e2e.ts +++ b/services/wizard-ci/e2e.ts @@ -12,10 +12,12 @@ * pnpm wizard-ci basic-integration/javascript-node/express-todo --e2e * pnpm wizard-ci basic-integration/next-js/15-app-router-todo --e2e --project-id 228144 */ -import { join, basename } from "path"; +import { tmpdir } from "os"; +import { join, basename, dirname } from "path"; import { existsSync, mkdirSync, + mkdtempSync, rmSync, readFileSync, writeFileSync, @@ -238,12 +240,36 @@ function readOrEmpty(file: string): string { } } +function writeGatewayTokenFile(gatewayToken: string): string { + const tokenDir = mkdtempSync(join(tmpdir(), "wizard-e2e-gateway-")); + const tokenFile = join(tokenDir, "gateway-token"); + writeFileSync(tokenFile, gatewayToken, { mode: 0o600 }); + return tokenFile; +} + +function guardGatewayTokenDir(tokenDir: string): () => void { + const removeTokenDirAndReraise = (signal: NodeJS.Signals) => { + releaseTokenDir(); + process.kill(process.pid, signal); + }; + const releaseTokenDir = () => { + process.removeListener("SIGINT", removeTokenDirAndReraise); + process.removeListener("SIGTERM", removeTokenDirAndReraise); + rmSync(tokenDir, { recursive: true, force: true }); + }; + process.on("SIGINT", removeTokenDirAndReraise); + process.on("SIGTERM", removeTokenDirAndReraise); + return releaseTokenDir; +} + /** Run a single app through the real-TUI e2e and assert. Returns exit code. */ export async function runE2e(opts: E2eOptions): Promise { const app = opts.app; const region = opts.region || process.env.POSTHOG_REGION || "us"; const projectId = opts.projectId || process.env.POSTHOG_WIZARD_PROJECT_ID || ""; const apiKey = process.env.POSTHOG_PERSONAL_API_KEY; + const gatewayTokenFile = process.env.WIZARD_CI_GATEWAY_TOKEN_FILE; + const gatewayToken = process.env.POSTHOG_GATEWAY_TOKEN?.trim(); if (!app) { console.error("✖ --e2e requires an app: pnpm wizard-ci --e2e"); @@ -257,6 +283,13 @@ export async function runE2e(opts: E2eOptions): Promise { console.error("✖ project id required: --project-id or POSTHOG_WIZARD_PROJECT_ID."); return 2; } + if (!gatewayTokenFile && !gatewayToken) { + console.error( + "✖ no gateway token: set POSTHOG_GATEWAY_TOKEN (a phs_ key with llm_gateway:read) " + + "or point WIZARD_CI_GATEWAY_TOKEN_FILE at a file holding one.", + ); + return 2; + } // A scenario may run against a sibling app's source tree (`sourceApp`), so a // run variation gets its own matrix leg without a second copy of the fixture. @@ -309,6 +342,7 @@ export async function runE2e(opts: E2eOptions): Promise { const childEnv: NodeJS.ProcessEnv = { ...process.env }; for (const k of Object.keys(childEnv)) if (STRIP_HOST_AUTH.test(k)) delete childEnv[k]; + delete childEnv.POSTHOG_GATEWAY_TOKEN; childEnv.POSTHOG_PERSONAL_API_KEY = apiKey; childEnv.APP_DIR = appDir; childEnv.PROJECT_ID = projectId; @@ -365,12 +399,16 @@ export async function runE2e(opts: E2eOptions): Promise { // synchronous spawn blocks the event loop that serves it — the wizard's MCP // client then gets no answer and the agent runs with no PostHog tool. let run: { status: number | null }; + const childGatewayTokenFile = gatewayToken ? writeGatewayTokenFile(gatewayToken) : gatewayTokenFile ?? ""; + childEnv.WIZARD_CI_GATEWAY_TOKEN_FILE = childGatewayTokenFile; + const releaseGatewayTokenDir = gatewayToken ? guardGatewayTokenDir(dirname(childGatewayTokenFile)) : () => {}; try { run = await runChild("npx", ["tsx", harness], { cwd: repo, env: childEnv, }); } finally { + releaseGatewayTokenDir(); await stub?.stop(); }