PSA/server/scripts/run-ee-migrations.js
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

163 lines
6.0 KiB
JavaScript

// Runs CE + EE migrations by merging both migration sets into a single
// directory *under server/* and invoking knex with MIGRATIONS_DIR pointing at
// it. Overlay rule: EE files overwrite CE files when names collide.
//
// Why under server/ (and NOT os.tmpdir())? Migrations resolve sibling resources
// relative to their own file location:
// - bare requires (e.g. require('pg-boss')) resolve via the nearest
// node_modules found walking up the tree.
// - file reads like path.resolve(__dirname, '..', 'src/invoice-templates/...')
// resolve to <migrationsDir>/../src.
// Both only work when the merged dir sits one level under server/, so that
// `..` === server/ (giving server/node_modules and server/src). A dir in
// /tmp has neither nearby and breaks migrations that import modules or read
// source files. This mirrors the Docker setup container, which builds
// /app/server/combined-migrations for exactly the same reason.
//
// Usage: node run-ee-migrations.js [action]
// action: latest (default), down, status
// Env:
// EE_MIGRATIONS_DEBUG=1 pass --debug to knex
// EE_MIGRATIONS_KEEP_TMP=1 keep the merged dir after running (for debugging)
import fs from 'fs';
import fsp from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { spawn } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function ensureDir(p) {
await fsp.mkdir(p, { recursive: true });
}
async function copyDir(src, dest) {
// Node 16+ supports fs.cp; fallback to manual copy if not available
if (fs.cp) {
await fsp.cp(src, dest, { recursive: true, force: true });
return;
}
const entries = await fsp.readdir(src, { withFileTypes: true });
await ensureDir(dest);
for (const e of entries) {
const s = path.join(src, e.name);
const d = path.join(dest, e.name);
if (e.isDirectory()) {
await copyDir(s, d);
} else if (e.isFile()) {
await fsp.copyFile(s, d);
}
}
}
function run(cmd, args, opts = {}) {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts });
child.on('close', (code) => {
if (code === 0) return resolve();
reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
});
child.on('error', reject);
});
}
async function main() {
// Parse action from command line args (default: latest)
const validActions = ['latest', 'down', 'status'];
const action = process.argv[2] || 'latest';
if (!validActions.includes(action)) {
console.error(`Invalid action: ${action}. Must be one of: ${validActions.join(', ')}`);
process.exit(1);
}
// Resolve repo root from server/scripts/
const repoRoot = path.resolve(__dirname, '..', '..');
const serverDir = path.resolve(repoRoot, 'server');
const ceDir = path.resolve(serverDir, 'migrations');
const eeDir = path.resolve(repoRoot, 'ee', 'server', 'migrations');
// Validate inputs
const ceExists = fs.existsSync(ceDir);
const eeExists = fs.existsSync(eeDir);
if (!ceExists && !eeExists) {
console.error('No migrations found: neither server/migrations nor ee/server/migrations exist.');
process.exit(1);
}
// Create the merged workspace DIRECTLY under server/ (one level down) so that
// `path.resolve(__dirname, '..', ...)` inside a migration points at server/,
// and bare requires resolve via server/node_modules -> repo node_modules.
// See the file header for the full rationale. The dir is removed in `finally`.
const mergedDir = path.join(serverDir, `.ee-combined-migrations-${process.pid}-${Date.now()}`);
await ensureDir(mergedDir);
const cleanup = async () => {
if (process.env.EE_MIGRATIONS_KEEP_TMP) {
console.log(`EE_MIGRATIONS_KEEP_TMP set; leaving merged migrations at: ${mergedDir}`);
return;
}
try {
await fsp.rm(mergedDir, { recursive: true, force: true });
} catch (err) {
console.warn(`Warning: failed to clean up ${mergedDir}: ${err?.message || err}`);
}
};
try {
// Copy CE first (recursive: brings utils/ and other helper trees that
// migrations import via relative requires).
if (ceExists) {
console.log(`Copying CE migrations from ${ceDir} -> ${mergedDir}`);
await copyDir(ceDir, mergedDir);
} else {
console.log('CE migrations folder not found; continuing with EE only.');
}
// Overlay EE (overwrites collisions)
if (eeExists) {
console.log(`Overlaying EE migrations from ${eeDir} -> ${mergedDir}`);
await copyDir(eeDir, mergedDir);
} else {
console.log('EE migrations folder not found; continuing with CE only.');
}
console.log(`Prepared merged migrations in: ${mergedDir}`);
// Run knex migrate:<action> with MIGRATIONS_DIR override and migration env
// (uses admin connection). NODE_PATH is set as a backstop so bare requires
// in migrations resolve regardless of where node is launched from.
const knexfilePath = path.resolve(serverDir, 'knexfile.cjs');
const nodePath = [
path.join(serverDir, 'node_modules'),
path.join(repoRoot, 'node_modules'),
process.env.NODE_PATH,
].filter(Boolean).join(path.delimiter);
const env = {
...process.env,
MIGRATIONS_DIR: mergedDir,
NODE_ENV: 'migration',
NODE_PATH: nodePath,
};
const knexAction = `migrate:${action}`;
console.log(`Running knex ${knexAction}...`);
const debugArgs = env.EE_MIGRATIONS_DEBUG ? ['--debug'] : [];
if (env.EE_MIGRATIONS_DEBUG) {
console.log('EE_MIGRATIONS_DEBUG=1 enabled: knex will print debug output.');
}
await run('npx', ['knex', knexAction, '--knexfile', knexfilePath, '--env', 'migration', ...debugArgs], { env, cwd: serverDir });
console.log(`Migration ${action} completed successfully.`);
} finally {
await cleanup();
}
}
main().catch((err) => {
console.error('EE migration run failed:', err?.message || err);
process.exit(1);
});