env
Some checks failed
Build & Deploy KLZ Cables / build-and-deploy (push) Failing after 1m17s

This commit is contained in:
2026-01-28 15:26:36 +01:00
parent 91ebc54571
commit 859d034ed7
7 changed files with 153 additions and 117 deletions

View File

@@ -32,6 +32,9 @@ ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
ENV SENTRY_DSN=$SENTRY_DSN
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
# Validate environment variables during build
RUN npx tsx scripts/validate-env.ts
RUN npm run build
# Production image, copy all the files and run next

View File

@@ -13,6 +13,9 @@ export async function register() {
// Initialize server services on boot
// We do this AFTER Sentry to ensure errors during service init are caught
const { getConfig } = await import('@/lib/config');
getConfig(); // Trigger validation
const { getServerAppServices } = await import('@/lib/services/create-services.server');
getServerAppServices();
}

View File

@@ -2,9 +2,18 @@
* Centralized configuration management for the application.
* This file provides a type-safe way to access environment variables.
*/
import { env } from './env';
import { envSchema, getRawEnv } from './env';
export const config = {
let memoizedConfig: ReturnType<typeof createConfig> | undefined;
/**
* Creates and validates the configuration object.
* Throws if validation fails.
*/
function createConfig() {
const env = envSchema.parse(getRawEnv());
return {
env: env.NODE_ENV,
isProduction: env.NODE_ENV === 'production',
isDevelopment: env.NODE_ENV === 'development',
@@ -53,45 +62,75 @@ export const config = {
recipients: env.MAIL_RECIPIENTS,
},
} as const;
}
/**
* Returns the validated configuration.
* Memoizes the result after the first call.
*/
export function getConfig() {
if (!memoizedConfig) {
memoizedConfig = createConfig();
}
return memoizedConfig;
}
/**
* Exported config object for convenience.
* Uses getters to ensure it's only initialized when accessed.
*/
export const config = {
get env() { return getConfig().env; },
get isProduction() { return getConfig().isProduction; },
get isDevelopment() { return getConfig().isDevelopment; },
get isTest() { return getConfig().isTest; },
get baseUrl() { return getConfig().baseUrl; },
get analytics() { return getConfig().analytics; },
get errors() { return getConfig().errors; },
get cache() { return getConfig().cache; },
get logging() { return getConfig().logging; },
get mail() { return getConfig().mail; },
};
/**
* Helper to get a masked version of the config for logging.
*/
export function getMaskedConfig() {
const c = getConfig();
const mask = (val: string | undefined) => (val ? `***${val.slice(-4)}` : 'not set');
return {
env: config.env,
baseUrl: config.baseUrl,
env: c.env,
baseUrl: c.baseUrl,
analytics: {
umami: {
websiteId: mask(config.analytics.umami.websiteId),
scriptUrl: config.analytics.umami.scriptUrl,
enabled: config.analytics.umami.enabled,
websiteId: mask(c.analytics.umami.websiteId),
scriptUrl: c.analytics.umami.scriptUrl,
enabled: c.analytics.umami.enabled,
},
},
errors: {
glitchtip: {
dsn: mask(config.errors.glitchtip.dsn),
enabled: config.errors.glitchtip.enabled,
dsn: mask(c.errors.glitchtip.dsn),
enabled: c.errors.glitchtip.enabled,
},
},
cache: {
redis: {
url: mask(config.cache.redis.url),
keyPrefix: config.cache.redis.keyPrefix,
enabled: config.cache.redis.enabled,
url: mask(c.cache.redis.url),
keyPrefix: c.cache.redis.keyPrefix,
enabled: c.cache.redis.enabled,
},
},
logging: {
level: config.logging.level,
level: c.logging.level,
},
mail: {
host: config.mail.host,
port: config.mail.port,
user: mask(config.mail.user),
from: config.mail.from,
recipients: config.mail.recipients,
host: c.mail.host,
port: c.mail.port,
user: mask(c.mail.user),
from: c.mail.from,
recipients: c.mail.recipients,
},
};
}

View File

@@ -1,51 +1,50 @@
import { z } from 'zod';
/**
* Helper to treat empty strings as undefined.
*/
const preprocessEmptyString = (val: unknown) => (val === '' ? undefined : val);
/**
* Environment variable schema.
*/
const envSchema = z.object({
export const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
NEXT_PUBLIC_BASE_URL: z.string().url().default('http://localhost:3000'),
NEXT_PUBLIC_BASE_URL: z.preprocess(preprocessEmptyString, z.string().url()),
// Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.string().url().default('https://analytics.infra.mintel.me/script.js'),
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()),
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess(preprocessEmptyString, z.string().url().default('https://analytics.infra.mintel.me/script.js')),
// Error Tracking
SENTRY_DSN: z.string().optional(),
SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()),
// Cache
REDIS_URL: z.string().optional(),
REDIS_KEY_PREFIX: z.string().default('klz:'),
REDIS_URL: z.preprocess(preprocessEmptyString, z.string().optional()),
REDIS_KEY_PREFIX: z.preprocess(preprocessEmptyString, z.string().default('klz:')),
// Logging
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
// Mail
MAIL_HOST: z.string().optional(),
MAIL_PORT: z.coerce.number().default(587),
MAIL_USERNAME: z.string().optional(),
MAIL_PASSWORD: z.string().optional(),
MAIL_FROM: z.string().optional(),
MAIL_RECIPIENTS: z.string().optional().transform(val => val?.split(',').filter(Boolean) || []),
MAIL_HOST: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_PORT: z.preprocess(preprocessEmptyString, z.coerce.number().default(587)),
MAIL_USERNAME: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()),
MAIL_RECIPIENTS: z.preprocess(
(val) => (typeof val === 'string' ? val.split(',').filter(Boolean) : val),
z.array(z.string()).default([])
),
});
export type Env = z.infer<typeof envSchema>;
/**
* Helper to get environment variables.
* Collects all environment variables from the process.
* Explicitly references NEXT_PUBLIC_ variables for Next.js inlining.
*/
function getRawEnv() {
const isServer = typeof window === 'undefined';
if (!isServer) {
// Client-side: only return NEXT_PUBLIC_ variables
return {
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
};
}
// Server-side: return all variables
export function getRawEnv() {
return {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
@@ -63,14 +62,3 @@ function getRawEnv() {
MAIL_RECIPIENTS: process.env.MAIL_RECIPIENTS,
};
}
/**
* Validated environment variables.
* We use safeParse during build to avoid crashing the build process.
*/
const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build';
const parsed = isBuildTime
? envSchema.safeParse(getRawEnv())
: { success: true, data: envSchema.parse(getRawEnv()) };
export const env = parsed.success ? parsed.data : envSchema.parse({});

13
package-lock.json generated
View File

@@ -17,7 +17,6 @@
"axios": "^1.13.2",
"cheerio": "^1.1.2",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"framer-motion": "^12.27.1",
"gray-matter": "^4.0.3",
"i18next": "^25.7.3",
@@ -9406,18 +9405,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@@ -9,7 +9,6 @@
"axios": "^1.13.2",
"cheerio": "^1.1.2",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
"framer-motion": "^12.27.1",
"gray-matter": "^4.0.3",
"i18next": "^25.7.3",

17
scripts/validate-env.ts Normal file
View File

@@ -0,0 +1,17 @@
import { envSchema, getRawEnv } from '../lib/env';
/**
* Simple script to validate environment variables.
* If validation fails, this script will exit with code 1.
*/
try {
const env = envSchema.parse(getRawEnv());
console.log('✅ Environment variables validated successfully.');
console.log('Base URL:', env.NEXT_PUBLIC_BASE_URL);
} catch (error) {
console.error('❌ Environment validation failed.');
if (error instanceof Error) {
console.error(error.message);
}
process.exit(1);
}