Extract API, migrations and portable NAS deployment
This commit is contained in:
commit
673255fa4f
57 files changed
+3799
No files matched your search
@@ -0,0 +1,9 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
export function compose(args) {
|
||||
const engine = ['docker', 'podman'].find(command => spawnSync(command, ['version'], { stdio: 'ignore' }).status === 0);
|
||||
if (!engine) throw new Error('Start Docker Desktop or Podman, then retry.');
|
||||
const result = spawnSync(engine, ['compose', '--env-file', '.env', '-f', 'infra/compose.yaml', ...args], { stdio: 'inherit' });
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) throw new Error(`Compose failed (${result.status})`);
|
||||
}
|
||||
if (process.argv[1]?.replaceAll('\\', '/').endsWith('/compose.mjs')) compose(process.argv.slice(2));
|
||||
@@ -0,0 +1,9 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { compose } from './compose.mjs';
|
||||
// The fixed, separate database is never dropped. No credentials are passed on the command line.
|
||||
compose(['exec', '-T', 'db', 'sh', '-c', `export MYSQL_PWD="$MARIADB_ROOT_PASSWORD"; mariadb -u root -e "CREATE DATABASE IF NOT EXISTS model_delivery_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT ALL PRIVILEGES ON model_delivery_test.* TO '$MARIADB_USER'@'%';"`]);
|
||||
for (const script of ['db:migrate', 'db:seed']) {
|
||||
// pnpm exposes its JS entry point to scripts; invoking Node avoids Windows shell quoting.
|
||||
const result = spawnSync(process.execPath, [process.env.npm_execpath, script], { stdio: 'inherit', env: { ...process.env, DB_NAME: 'model_delivery_test' } });
|
||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const apiRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const webRoot = resolve(process.argv[2] ?? join(apiRoot, '../delivery-app-web'));
|
||||
const output = join(apiRoot, '.image');
|
||||
if (existsSync(output)) throw new Error('Existing .image directory: move it aside before staging a new release.');
|
||||
if (JSON.parse(readFileSync(join(webRoot, 'package.json'), 'utf8')).name !== '@model/web') {
|
||||
throw new Error('Expected the delivery-app-web checkout as the argument.');
|
||||
}
|
||||
if (!process.env.npm_execpath) throw new Error('Run through pnpm image:stage.');
|
||||
function pnpm(cwd, args) {
|
||||
const result = spawnSync(process.execPath, [process.env.npm_execpath, ...args], { cwd, stdio: 'inherit' });
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) throw new Error('Staging command failed. No image was built.');
|
||||
}
|
||||
pnpm(apiRoot, ['build']);
|
||||
pnpm(webRoot, ['build']);
|
||||
mkdirSync(output);
|
||||
// A hoisted deployment contains real directories. Windows junctions from the
|
||||
// default isolated linker become dangling links when copied into a Linux image.
|
||||
pnpm(apiRoot, ['--config.node-linker=hoisted', '--filter', '@model/api', 'deploy', '--legacy', '--prod', join(output, 'api')]);
|
||||
// Legacy hoisted deploy may resolve again. Refuse any version that wasn't in the
|
||||
// workstation's locked install, so staging cannot silently ship untested updates.
|
||||
function packagesIn(modules, visit) {
|
||||
for (const entry of readdirSync(modules, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith('.')) continue;
|
||||
const path = join(modules, entry.name);
|
||||
if (entry.name.startsWith('@')) { packagesIn(path, visit); continue; }
|
||||
if (!existsSync(join(path, 'package.json'))) continue;
|
||||
const pkg = JSON.parse(readFileSync(join(path, 'package.json'), 'utf8'));
|
||||
visit(`${pkg.name}@${pkg.version}`);
|
||||
if (existsSync(join(path, 'node_modules'))) packagesIn(join(path, 'node_modules'), visit);
|
||||
}
|
||||
}
|
||||
const testedPackages = new Set();
|
||||
const virtualStore = join(apiRoot, 'node_modules/.pnpm');
|
||||
for (const entry of readdirSync(virtualStore, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && existsSync(join(virtualStore, entry.name, 'node_modules'))) {
|
||||
packagesIn(join(virtualStore, entry.name, 'node_modules'), name => testedPackages.add(name));
|
||||
}
|
||||
}
|
||||
packagesIn(join(output, 'api/node_modules'), name => {
|
||||
if (!testedPackages.has(name)) throw new Error(`Deployment resolved an untested dependency: ${name}. Review and update the locked install before staging again.`);
|
||||
});
|
||||
cpSync(join(webRoot, 'dist'), join(output, 'web'), { recursive: true });
|
||||
function revision(cwd) {
|
||||
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8' });
|
||||
return result.status === 0 ? result.stdout.trim() : 'uncommitted';
|
||||
}
|
||||
writeFileSync(join(output, 'release.json'), JSON.stringify({
|
||||
builtAt: new Date().toISOString(), api: revision(apiRoot), web: revision(webRoot),
|
||||
shared: JSON.parse(readFileSync(join(apiRoot, 'package.json'), 'utf8')).dependencies['@model/shared'],
|
||||
}, null, 2) + '\n');
|
||||
console.log('Staged production API dependencies and PWA assets in .image/. Build the standalone Dockerfile next.');
|
||||
Reference in new issue
Block a user