Maintain an Environment-based Build System
TLDR
Given we have different teams with different areas of concern, and we have different deployment services that need different setup configurations, we should maintain an environment-based build system.
Context
The Lossless monorepo is a complex system with multiple teams and deployment services.
For convenience, the development team is primarily concerned with the Website and related code, so the content directory is imported as a git submodule in the directory
src/generated-content
. It points to the same remote repository as the monorepo content directory, but is at a different path.The management team has to bring it all together, so they work from the lossless-monorepo as a root directory, which then, as git submodules, imports the content from the lossless-content repository and the website code from the lossless-site repository.
Getting Astro and Vite to work with this setup is a bit of a challenge, but it is doable. It also requires that once it's working, we should maintain it and not break it.
Solution
We should maintain an environment-based build system.
Environment-based Build System
Files:
From the monorepo root:
content
site
site/.env
site/.env.example
site/src/utils/envUtils.js
site/src/content.config.ts
site/astro.config.mjs
package.json
Environment Variables:
site/.env
text
# Set node environment, default to development
# APP_ENV is a fallback in case vite overrides with its default to production
NODE_ENV=development
APP_ENV=development
# DEPLOY_ENV is used to determine the content location.
# LocalSiteOnly, LocalMonorepo, Vercel, and Railway are options.
DEPLOY_ENV=LocalSiteOnly
Determine the Environment Variables with envUtils.js
site/src/utils/envUtils.js
js
// src/utils/env.js
import dotenv from 'dotenv';
import path from 'path';
import fs from 'fs';
// Load .env file first, before anything else
const envPath = path.resolve(process.cwd(), '.env');
const envFileExists = fs.existsSync(envPath);
// Initialize with current env vars
const envVars = { ...process.env };
// Load .env file if it exists
if (envFileExists) {
const envConfig = dotenv.parse(fs.readFileSync(envPath));
Object.assign(envVars, envConfig);
}
// Set NODE_ENV if not already set
envVars.NODE_ENV = envVars.NODE_ENV || 'development';
// Set APP_ENV to match NODE_ENV if not explicitly set
envVars.APP_ENV = envVars.APP_ENV || envVars.NODE_ENV;
// Update process.env with our final values
Object.assign(process.env, envVars);
// Export environment variables
export const NODE_ENV = process.env.NODE_ENV;
export const APP_ENV = process.env.APP_ENV;
export const isProduction = NODE_ENV === 'production';
export const isDevelopment = !isProduction;
// Log environment info
console.log('Environment Configuration:', {
NODE_ENV,
APP_ENV,
isProduction,
isDevelopment,
envFile: envFileExists ? envPath : 'Not found',
cwd: process.cwd()
});
Content Path Resolution in content.config.ts
The content configuration uses the environment variables to determine the correct content paths:
typescript
// site/src/content.config.ts
import { NODE_ENV, isProduction, isDevelopment } from './utils/envUtils.js';
import path from 'path';
import fs from 'fs';
// Determine content path based on environment
let contentBasePath: string;
switch (process.env.DEPLOY_ENV) {
case 'LocalSiteOnly':
contentBasePath = path.resolve(process.cwd(), 'src/generated-content');
break;
case 'LocalMonorepo':
contentBasePath = path.resolve(process.cwd(), '..', 'content');
break;
case 'Vercel':
contentBasePath = path.resolve(process.cwd(), 'src/generated-content');
break;
case 'Railway':
contentBasePath = '/app/content';
break;
default:
contentBasePath = path.resolve(process.cwd(), '/lossless-monorepo/content');
}
// Log the configuration
console.log('Content configuration:', {
isProduction,
contentBasePath,
cwd: process.cwd(),
resolvedPath: path.resolve(contentBasePath)
});
// Verify the content directory exists
if (!fs.existsSync(contentBasePath)) {
console.warn(`WARNING: Content directory not found at ${contentBasePath}`);
}
export default defineCollection({
// Your collection configuration
});
Build Script in package.json
The build script in
package.json
is modified to properly load environment variables: json
{
"scripts": {
"build": "node -e \"require('dotenv').config({ path: '.env' }); require('child_process').execSync('astro build', { stdio: 'inherit' });\""
}
}
Deployment Configuration
Vercel
- Set environment variables in Vercel dashboard:
NODE_ENV=production
APP_ENV=production
DEPLOY_ENV=Vercel
Railway
- Set environment variables in Railway dashboard:
NODE_ENV=production
APP_ENV=production
DEPLOY_ENV=Railway
Maintenance Guidelines
- Environment Variables:
- Always update both
.env
and.env.example
files - Document new variables in this document
- Never commit sensitive data to version control
- Adding New Environments:
- Add a new case in
content.config.ts
for the new environment - Update the
DEPLOY_ENV
documentation - Test the build locally with the new environment
- Debugging:
- Check the environment logs at the start of the build
- Verify content paths are resolved correctly
- Ensure the content directory exists at the expected location
Common Issues and Solutions
- Content Not Found:
- Check
DEPLOY_ENV
is set correctly - Verify the content directory exists at the expected path
- Check file permissions
- Environment Variables Not Loading:
- Ensure
.env
file exists in thesite
directory - Verify the build script is loading the
.env
file - Check for typos in variable names
- Build Fails in CI/CD:
- Ensure all required environment variables are set in the CI/CD pipeline
- Check the build logs for specific error messages
- Verify the content submodule is initialized in CI
Testing the Setup
To test the environment setup locally:
bash
# Test LocalSiteOnly
DEPLOY_ENV=LocalSiteOnly pnpm build
# Test LocalMonorepo
DEPLOY_ENV=LocalMonorepo pnpm build