build: package persistent deployment and maintenance commands

This commit is contained in:
2026-09-15 21:48:12 +03:30
parent 9319c05710
commit ee9f49e4b7
7 changed files with 59 additions and 12 deletions

View File

@@ -0,0 +1,2 @@
import { build } from 'esbuild';
await build({ entryPoints: ['scripts/maintenance.ts'], outfile: '.next/standalone/maintenance.mjs', bundle: true, platform: 'node', target: 'node22', format: 'esm', packages: 'external' });

View File

@@ -125,9 +125,14 @@ fi
compose() {
local directory="$1"
shift
local runtime_environment=()
if [[ -f "$deploy_root/runtime.env" ]]; then
runtime_environment=(--env-file "$deploy_root/runtime.env")
fi
docker compose \
--project-name portfolio \
--file "$directory/compose.yaml" \
"${runtime_environment[@]}" \
--env-file "$directory/release.env" \
"$@"
}
@@ -187,12 +192,7 @@ if activate "$release_dir"; then
exit 0
fi
echo "Deployment failed; attempting rollback." >&2
if [[ -n "$previous_release" && -f "$previous_release/compose.yaml" && -f "$previous_release/release.env" ]]; then
activate "$previous_release"
echo "Rollback succeeded: $(basename "$previous_release")" >&2
else
echo "No image-based previous release is available for automatic rollback." >&2
fi
echo "Deployment failed. Automatic image rollback is disabled because startup may have migrated the persistent database." >&2
echo "Inspect the failed container. Restore the pre-upgrade backup into a new data directory before activating an older image; see SELF_HOSTING.md." >&2
exit 1

View File

@@ -1,10 +1,27 @@
import { cp } from 'node:fs/promises';
import { cp, readdir, rm } from 'node:fs/promises';
import path from 'node:path';
const projectRoot = path.resolve('.');
const standaloneRoot = path.join(projectRoot, '.next', 'standalone');
// Instrumentation traces can bypass Next's route-level exclusions. Runtime
// state belongs to the mounted volume, never to a distributable build.
async function removeRuntimeState(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const target = path.resolve(directory, entry.name);
if (!target.startsWith(`${standaloneRoot}${path.sep}`)) throw new Error('Invalid package path');
if (entry.name === 'node_modules') continue;
if (entry.name === 'data' || entry.name === 'backups' || entry.name === 'auth-secret' || /\.sqlite(?:-|$)/.test(entry.name) || /^\.env(?:\.|$)/.test(entry.name)) {
await rm(target, { recursive: entry.isDirectory(), force: true });
} else if (entry.isDirectory()) {
await removeRuntimeState(target);
}
}
}
await removeRuntimeState(standaloneRoot);
await cp(path.join(projectRoot, 'public'), path.join(standaloneRoot, 'public'), { recursive: true });
await cp(path.join(projectRoot, 'migrations'), path.join(standaloneRoot, 'migrations'), { recursive: true });
await cp(
path.join(projectRoot, '.next', 'static'),
path.join(standaloneRoot, '.next', 'static'),