mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-20 04:21:23 +00:00
* 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
106 lines
3.0 KiB
TypeScript
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',
|
|
},
|
|
}
|
|
}
|