Files
rxminder/scripts/migrate-to-unified-config.ts
William Valentin f3936f23fe feat: add unified configuration generation and migration scripts
- Add generate-unified-config.ts for generating environment configs
- Support multiple output formats: env files, Kubernetes configs, Docker
- Include migration script for transitioning from legacy configuration
- Support dry-run and verbose modes for safe configuration changes
- Generate type-safe environment exports for each environment
- Provide comprehensive error handling and validation
2025-09-08 09:44:50 -07:00

597 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bun
/**
* Migration Script: Old Config System → Unified Config
*
* This script helps migrate from the existing .env and multiple config files
* to the new unified configuration system.
*
* Usage:
* bun scripts/migrate-to-unified-config.ts
* bun scripts/migrate-to-unified-config.ts --dry-run
* bun scripts/migrate-to-unified-config.ts --backup
*/
import {
readFileSync,
writeFileSync,
existsSync,
mkdirSync,
copyFileSync,
} from 'fs';
import { join } from 'path';
interface MigrationOptions {
dryRun?: boolean;
backup?: boolean;
verbose?: boolean;
}
interface ParsedEnv {
[key: string]: string;
}
class ConfigMigrator {
private options: MigrationOptions;
private projectRoot: string;
private backupDir: string;
constructor(options: MigrationOptions = {}) {
this.options = options;
this.projectRoot = join(__dirname, '..');
this.backupDir = join(this.projectRoot, '.config-backup');
}
/**
* Run the migration process
*/
async migrate(): Promise<void> {
console.warn('🔄 Starting migration to unified configuration system...');
console.warn(`📁 Project root: ${this.projectRoot}`);
if (this.options.backup) {
await this.createBackup();
}
// Step 1: Read existing configuration
const existingConfig = await this.readExistingConfig();
// Step 2: Generate unified config values
const unifiedValues = this.mapToUnifiedConfig(existingConfig);
// Step 3: Update .env file with unified structure
await this.updateMainEnvFile(unifiedValues);
// Step 4: Generate new config files
await this.generateUnifiedConfigFiles();
// Step 5: Update package.json scripts if needed
await this.updatePackageJsonScripts();
console.warn('✅ Migration completed successfully!');
console.warn('\n📋 Next steps:');
console.warn('1. Review the updated .env file');
console.warn('2. Run: make generate-config');
console.warn('3. Test your application: make dev');
console.warn('4. Deploy when ready: make deploy-prod-quick');
if (this.options.backup) {
console.warn(`5. Remove backup when satisfied: rm -rf ${this.backupDir}`);
}
}
/**
* Create backup of existing configuration files
*/
private async createBackup(): Promise<void> {
console.warn('\n💾 Creating backup of existing configuration...');
if (!existsSync(this.backupDir)) {
mkdirSync(this.backupDir, { recursive: true });
}
const filesToBackup = [
'.env',
'.env.production',
'.env.example',
'config/app.config.ts',
'k8s-kustomize/base/config.env',
'vite.config.ts',
];
for (const file of filesToBackup) {
const sourcePath = join(this.projectRoot, file);
if (existsSync(sourcePath)) {
const backupPath = join(this.backupDir, file.replace('/', '_'));
copyFileSync(sourcePath, backupPath);
console.warn(` 📄 Backed up: ${file}`);
}
}
console.warn(`✅ Backup created in: ${this.backupDir}`);
}
/**
* Read existing configuration from various sources
*/
private async readExistingConfig(): Promise<ParsedEnv> {
console.warn('\n🔍 Reading existing configuration...');
const config: ParsedEnv = {};
// Read .env files
const envFiles = ['.env', '.env.production', '.env.local'];
for (const envFile of envFiles) {
const envPath = join(this.projectRoot, envFile);
if (existsSync(envPath)) {
const envData = this.parseEnvFile(envPath);
Object.assign(config, envData);
console.warn(
` 📄 Read: ${envFile} (${Object.keys(envData).length} variables)`
);
}
}
// Read Kubernetes config.env
const k8sConfigPath = join(
this.projectRoot,
'k8s-kustomize/base/config.env'
);
if (existsSync(k8sConfigPath)) {
const k8sData = this.parseEnvFile(k8sConfigPath);
Object.assign(config, k8sData);
console.warn(
` 🚢 Read: k8s config.env (${Object.keys(k8sData).length} variables)`
);
}
console.warn(
`✅ Found ${Object.keys(config).length} configuration variables`
);
return config;
}
/**
* Parse environment file
*/
private parseEnvFile(filePath: string): ParsedEnv {
try {
const content = readFileSync(filePath, 'utf8');
const env: ParsedEnv = {};
content.split('\n').forEach(line => {
line = line.trim();
if (line && !line.startsWith('#')) {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length > 0) {
env[key.trim()] = valueParts
.join('=')
.trim()
.replace(/^["']|["']$/g, '');
}
}
});
return env;
} catch (error) {
console.warn(` ⚠️ Could not read ${filePath}: ${error}`);
return {};
}
}
/**
* Map existing config to unified config structure
*/
private mapToUnifiedConfig(existingConfig: ParsedEnv): ParsedEnv {
console.warn('\n🔄 Mapping to unified configuration structure...');
const unified: ParsedEnv = {};
// Application
unified.APP_NAME = existingConfig.APP_NAME || 'RxMinder';
unified.APP_VERSION = existingConfig.APP_VERSION || '1.0.0';
unified.APP_BASE_URL =
existingConfig.APP_BASE_URL ||
existingConfig.VITE_BASE_URL ||
'http://localhost:5173';
unified.NODE_ENV = existingConfig.NODE_ENV || 'development';
// Database
unified.VITE_COUCHDB_URL =
existingConfig.VITE_COUCHDB_URL ||
existingConfig.COUCHDB_URL ||
'http://localhost:5984';
unified.VITE_COUCHDB_USER =
existingConfig.VITE_COUCHDB_USER ||
existingConfig.COUCHDB_USER ||
'admin';
unified.VITE_COUCHDB_PASSWORD =
existingConfig.VITE_COUCHDB_PASSWORD ||
existingConfig.COUCHDB_PASSWORD ||
'changeme';
// Container & Registry
unified.CONTAINER_REGISTRY =
existingConfig.CONTAINER_REGISTRY || 'gitea-http.taildb3494.ts.net';
unified.CONTAINER_REPOSITORY =
existingConfig.CONTAINER_REPOSITORY ||
existingConfig.GITEA_REPOSITORY ||
'will/meds';
unified.CONTAINER_TAG = this.getContainerTag(existingConfig);
// Kubernetes
unified.INGRESS_HOST =
existingConfig.INGRESS_HOST ||
existingConfig.APP_BASE_URL?.replace(/^https?:\/\//, '') ||
'rxminder.local';
unified.STORAGE_CLASS = existingConfig.STORAGE_CLASS || 'longhorn';
unified.STORAGE_SIZE = existingConfig.STORAGE_SIZE || '1Gi';
// Email
unified.VITE_MAILGUN_API_KEY =
existingConfig.VITE_MAILGUN_API_KEY ||
existingConfig.MAILGUN_API_KEY ||
'';
unified.VITE_MAILGUN_DOMAIN =
existingConfig.VITE_MAILGUN_DOMAIN || existingConfig.MAILGUN_DOMAIN || '';
unified.VITE_MAILGUN_FROM_NAME =
existingConfig.VITE_MAILGUN_FROM_NAME ||
existingConfig.MAILGUN_FROM_NAME ||
'RxMinder';
unified.VITE_MAILGUN_FROM_EMAIL =
existingConfig.VITE_MAILGUN_FROM_EMAIL ||
existingConfig.MAILGUN_FROM_EMAIL ||
'';
// OAuth
unified.VITE_GOOGLE_CLIENT_ID =
existingConfig.VITE_GOOGLE_CLIENT_ID ||
existingConfig.GOOGLE_CLIENT_ID ||
'';
unified.VITE_GITHUB_CLIENT_ID =
existingConfig.VITE_GITHUB_CLIENT_ID ||
existingConfig.GITHUB_CLIENT_ID ||
'';
// Feature Flags
unified.ENABLE_EMAIL_VERIFICATION = this.toBooleanString(
existingConfig.ENABLE_EMAIL_VERIFICATION,
true
);
unified.ENABLE_OAUTH = this.toBooleanString(
existingConfig.ENABLE_OAUTH,
true
);
unified.ENABLE_ADMIN_INTERFACE = this.toBooleanString(
existingConfig.ENABLE_ADMIN_INTERFACE,
true
);
unified.ENABLE_MONITORING = this.toBooleanString(
existingConfig.ENABLE_MONITORING,
false
);
unified.DEBUG_MODE = this.toBooleanString(
existingConfig.DEBUG_MODE,
unified.NODE_ENV === 'development'
);
// Performance
unified.LOG_LEVEL =
existingConfig.LOG_LEVEL ||
(unified.NODE_ENV === 'production' ? 'warn' : 'info');
unified.CACHE_TTL = existingConfig.CACHE_TTL || '1800';
// Security
unified.JWT_SECRET =
existingConfig.JWT_SECRET ||
'your-super-secret-jwt-key-change-in-production';
console.warn(
`✅ Mapped ${Object.keys(unified).length} unified configuration variables`
);
return unified;
}
/**
* Get container tag from existing config
*/
private getContainerTag(existingConfig: ParsedEnv): string {
if (existingConfig.CONTAINER_TAG) return existingConfig.CONTAINER_TAG;
// Extract from DOCKER_IMAGE if present
if (existingConfig.DOCKER_IMAGE) {
const parts = existingConfig.DOCKER_IMAGE.split(':');
if (parts.length > 1) return parts[parts.length - 1];
}
// Default based on environment
const env = existingConfig.NODE_ENV || 'development';
return env === 'production'
? 'v1.0.0'
: env === 'staging'
? 'staging'
: 'latest';
}
/**
* Convert value to boolean string
*/
private toBooleanString(
value: string | undefined,
defaultValue: boolean
): string {
if (value === undefined) return defaultValue.toString();
return (value.toLowerCase() === 'true' || value === '1').toString();
}
/**
* Update main .env file with unified structure
*/
private async updateMainEnvFile(unifiedValues: ParsedEnv): Promise<void> {
console.warn('\n📝 Updating .env file with unified structure...');
const envPath = join(this.projectRoot, '.env');
const timestamp = new Date().toISOString();
const content = `# Unified Application Configuration
# Migrated on: ${timestamp}
#
# This file now serves as the single source of truth for configuration.
# All other config files are generated from this unified configuration.
# ============================================================================
# APPLICATION CONFIGURATION
# ============================================================================
# Application Identity
APP_NAME=${unifiedValues.APP_NAME}
APP_VERSION=${unifiedValues.APP_VERSION}
APP_BASE_URL=${unifiedValues.APP_BASE_URL}
NODE_ENV=${unifiedValues.NODE_ENV}
# ============================================================================
# DATABASE CONFIGURATION
# ============================================================================
# CouchDB Configuration
VITE_COUCHDB_URL=${unifiedValues.VITE_COUCHDB_URL}
VITE_COUCHDB_USER=${unifiedValues.VITE_COUCHDB_USER}
VITE_COUCHDB_PASSWORD=${unifiedValues.VITE_COUCHDB_PASSWORD}
# ============================================================================
# CONTAINER & DEPLOYMENT CONFIGURATION
# ============================================================================
# Container Registry
CONTAINER_REGISTRY=${unifiedValues.CONTAINER_REGISTRY}
CONTAINER_REPOSITORY=${unifiedValues.CONTAINER_REPOSITORY}
CONTAINER_TAG=${unifiedValues.CONTAINER_TAG}
# Kubernetes Configuration
INGRESS_HOST=${unifiedValues.INGRESS_HOST}
STORAGE_CLASS=${unifiedValues.STORAGE_CLASS}
STORAGE_SIZE=${unifiedValues.STORAGE_SIZE}
# ============================================================================
# EMAIL CONFIGURATION
# ============================================================================
# Mailgun Configuration
VITE_MAILGUN_API_KEY=${unifiedValues.VITE_MAILGUN_API_KEY}
VITE_MAILGUN_DOMAIN=${unifiedValues.VITE_MAILGUN_DOMAIN}
VITE_MAILGUN_FROM_NAME=${unifiedValues.VITE_MAILGUN_FROM_NAME}
VITE_MAILGUN_FROM_EMAIL=${unifiedValues.VITE_MAILGUN_FROM_EMAIL}
# ============================================================================
# OAUTH CONFIGURATION
# ============================================================================
# OAuth Provider Configuration
VITE_GOOGLE_CLIENT_ID=${unifiedValues.VITE_GOOGLE_CLIENT_ID}
VITE_GITHUB_CLIENT_ID=${unifiedValues.VITE_GITHUB_CLIENT_ID}
# ============================================================================
# FEATURE FLAGS
# ============================================================================
# Application Features
ENABLE_EMAIL_VERIFICATION=${unifiedValues.ENABLE_EMAIL_VERIFICATION}
ENABLE_OAUTH=${unifiedValues.ENABLE_OAUTH}
ENABLE_ADMIN_INTERFACE=${unifiedValues.ENABLE_ADMIN_INTERFACE}
ENABLE_MONITORING=${unifiedValues.ENABLE_MONITORING}
DEBUG_MODE=${unifiedValues.DEBUG_MODE}
# ============================================================================
# PERFORMANCE & SECURITY
# ============================================================================
# Logging and Performance
LOG_LEVEL=${unifiedValues.LOG_LEVEL}
CACHE_TTL=${unifiedValues.CACHE_TTL}
# Security
JWT_SECRET=${unifiedValues.JWT_SECRET}
# ============================================================================
# ENVIRONMENT-SPECIFIC OVERRIDES
# ============================================================================
# Add environment-specific variables below this line
# These will override the unified configuration for this environment
`;
if (this.options.dryRun) {
console.warn(' 🔍 [DRY RUN] Would update .env file');
console.warn(` Path: ${envPath}`);
console.warn(` Size: ${content.length} bytes`);
} else {
writeFileSync(envPath, content, 'utf8');
console.warn(' ✅ Updated .env file with unified structure');
}
}
/**
* Generate unified config files
*/
private async generateUnifiedConfigFiles(): Promise<void> {
console.warn('\n🛠 Generating unified configuration files...');
if (this.options.dryRun) {
console.warn(' 🔍 [DRY RUN] Would generate unified config files');
} else {
try {
// Use the new generator script
const { ConfigGenerator } = await import('./generate-unified-config');
const generator = new ConfigGenerator({ generateAll: true });
await generator.generate();
console.warn(' ✅ Generated unified configuration files');
} catch (_error) {
console.warn(' ⚠️ Could not auto-generate config files');
console.warn(' 💡 Run manually: make generate-config');
}
}
}
/**
* Update package.json scripts to use unified config
*/
private async updatePackageJsonScripts(): Promise<void> {
console.warn('\n📦 Checking package.json scripts...');
const packageJsonPath = join(this.projectRoot, 'package.json');
if (!existsSync(packageJsonPath)) {
console.warn(' ⚠️ package.json not found, skipping script updates');
return;
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
let updated = false;
// Update scripts that might reference old config generation
const scriptUpdates = {
'generate:config': 'bun scripts/generate-unified-config.ts --all',
'config:dev': 'bun scripts/generate-unified-config.ts development',
'config:prod': 'bun scripts/generate-unified-config.ts production',
'config:staging': 'bun scripts/generate-unified-config.ts staging',
};
for (const [scriptName, command] of Object.entries(scriptUpdates)) {
if (!packageJson.scripts[scriptName]) {
packageJson.scripts[scriptName] = command;
updated = true;
}
}
if (updated) {
if (this.options.dryRun) {
console.warn(
' 🔍 [DRY RUN] Would add unified config scripts to package.json'
);
} else {
writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
'utf8'
);
console.warn(' ✅ Added unified config scripts to package.json');
}
} else {
console.warn(' No package.json script updates needed');
}
} catch (error) {
console.warn(` ⚠️ Could not update package.json: ${error}`);
}
}
}
/**
* CLI Interface
*/
async function main() {
const args = process.argv.slice(2);
const options: MigrationOptions = {};
// Parse command line arguments
for (const arg of args) {
switch (arg) {
case '--dry-run':
options.dryRun = true;
break;
case '--backup':
options.backup = true;
break;
case '--verbose':
case '-v':
options.verbose = true;
break;
case '--help':
case '-h':
showHelp();
process.exit(0);
break;
default:
console.warn(`Unknown option: ${arg}`);
break;
}
}
try {
const migrator = new ConfigMigrator(options);
await migrator.migrate();
} catch (error) {
console.error('❌ Migration failed:', error);
process.exit(1);
}
}
/**
* Show help message
*/
function showHelp() {
console.warn(`
🔄 Configuration Migration Tool
Migrates from the old configuration system (multiple .env files,
separate Kubernetes configs) to the new unified configuration system.
USAGE:
bun scripts/migrate-to-unified-config.ts [options]
OPTIONS:
--dry-run Show what would be changed without making changes
--backup Create backup of existing configuration files
--verbose, -v Show detailed output
--help, -h Show this help message
EXAMPLES:
bun scripts/migrate-to-unified-config.ts --backup
bun scripts/migrate-to-unified-config.ts --dry-run
bun scripts/migrate-to-unified-config.ts --backup --verbose
WHAT IT DOES:
1. Reads existing .env files and Kubernetes config
2. Maps old config structure to unified configuration
3. Updates .env file with unified structure
4. Generates new configuration files for all environments
5. Updates package.json scripts (if needed)
BACKUP:
When --backup is used, existing config files are copied to:
.config-backup/
AFTER MIGRATION:
1. Review the updated .env file
2. Run: make generate-config
3. Test: make dev
4. Deploy: make deploy-prod-quick
`);
}
// Run CLI if this file is executed directly
if (import.meta.main) {
main().catch(console.error);
}
export { ConfigMigrator, type MigrationOptions };