Files
BrowserOS/scripts/build/server/config.ts
Nikhil 9fdb361d67 feat: build prod server resource artifact pipeline (#417)
* feat: build prod server resource artifacts with cloudflare r2

* fix: address PR review comments for prod_server_resources_cloudflare

* feat: fix prod server build setup and CLI ergonomics

* fix: prevent build env secret inlining and template fallback

* fix: read from fileenv

* feat: add ripgrep

* feat: upload prod artifacts to latest and version prefixes
2026-03-05 14:26:46 -08:00

106 lines
3.0 KiB
TypeScript

import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { parse } from 'dotenv'
import type { BuildConfig } from './types'
const REQUIRED_PROD_VARS = [
'BROWSEROS_CONFIG_URL',
'CODEGEN_SERVICE_URL',
'POSTHOG_API_KEY',
'SENTRY_DSN',
]
const INLINED_ENV_VARS = [
...REQUIRED_PROD_VARS,
'NODE_ENV',
'LOG_LEVEL',
] as const
const PROD_ENV_PATH = join('apps', 'server', '.env.production')
const PROD_ENV_TEMPLATE_PATH = join('apps', 'server', '.env.production.example')
function readServerVersion(rootDir: string): string {
const pkgPath = join(rootDir, 'apps/server/package.json')
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
return pkg.version
}
function pickEnv(name: string, fileEnv: Record<string, string>): string {
const value = process.env[name] ?? fileEnv[name]
if (!value || value.trim().length === 0) {
throw new Error(`Missing required environment variable: ${name}`)
}
return value
}
function loadProdEnv(rootDir: string): Record<string, string> {
const prodEnvPath = join(rootDir, PROD_ENV_PATH)
if (!existsSync(prodEnvPath)) {
const prodEnvTemplatePath = join(rootDir, PROD_ENV_TEMPLATE_PATH)
if (existsSync(prodEnvTemplatePath)) {
throw new Error(
`Missing ${PROD_ENV_PATH}. Create it from ${PROD_ENV_TEMPLATE_PATH} before running build:server.`,
)
}
throw new Error(
`Missing ${PROD_ENV_PATH}. The template file ${PROD_ENV_TEMPLATE_PATH} was not found.`,
)
}
return parse(readFileSync(prodEnvPath, 'utf-8'))
}
function buildInlineEnv(
fileEnv: Record<string, string>,
): Record<string, string> {
const inlineEnv: Record<string, string> = {}
for (const key of INLINED_ENV_VARS) {
const value = process.env[key] ?? fileEnv[key]
if (value !== undefined) {
inlineEnv[key] = value
}
}
return inlineEnv
}
function validateProductionEnv(envVars: Record<string, string>): void {
const missing = REQUIRED_PROD_VARS.filter((name) => {
const value = envVars[name]
return !value || value.trim().length === 0
})
if (missing.length > 0) {
throw new Error(
`Production build requires variables: ${missing.join(', ')} (set them in ${PROD_ENV_PATH} or process env).`,
)
}
}
export function loadBuildConfig(rootDir: string): BuildConfig {
const fileEnv = loadProdEnv(rootDir)
const envVars = buildInlineEnv(fileEnv)
validateProductionEnv(envVars)
const processEnv: NodeJS.ProcessEnv = {
PATH: process.env.PATH ?? '',
...fileEnv,
...process.env,
}
return {
version: readServerVersion(rootDir),
envVars,
processEnv,
r2: {
accountId: pickEnv('R2_ACCOUNT_ID', fileEnv),
accessKeyId: pickEnv('R2_ACCESS_KEY_ID', fileEnv),
secretAccessKey: pickEnv('R2_SECRET_ACCESS_KEY', fileEnv),
bucket: pickEnv('R2_BUCKET', fileEnv),
downloadPrefix:
process.env.R2_DOWNLOAD_PREFIX ?? fileEnv.R2_DOWNLOAD_PREFIX ?? '',
uploadPrefix:
process.env.R2_UPLOAD_PREFIX ??
fileEnv.R2_UPLOAD_PREFIX ??
'server/prod-resources',
},
}
}