Add one-command dev-to-prod release with clear SUCCESS/FAILED banners.

Introduce devtoprod:release orchestrator, config file, CLI result footers on deploy scripts, auto-thumb regeneration on curator fixes, and updated deploy documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-08 14:54:56 +03:00
co-authored by Cursor
parent 313666a4ab
commit 21e3e41e48
20 changed files with 839 additions and 86 deletions
+17 -1
View File
@@ -9,6 +9,7 @@ const {
loadProdPgConfig,
rootDir,
} = require('./db-env');
const { printCliResult } = require('./lib/cli-result');
const { Client } = pg;
@@ -124,12 +125,27 @@ async function main() {
{ stdio: 'inherit' },
);
const relTxtPath = path.relative(rootDir, txtPath).replace(/\\/g, '/');
console.log(`Backup written: ${txtPath}`);
console.log(`Archive written: ${zipPath}`);
console.log(`Tables: ${tablesRes.rowCount}, rows: ${totalRows}`);
printCliResult({
script: 'backup-db-data',
ok: true,
summary: `Backup complete for "${dbName}".`,
details: [
`File: ${relTxtPath}`,
`Tables: ${tablesRes.rowCount}, rows: ${totalRows}`,
],
});
}
main().catch((error) => {
console.error(error);
process.exit(1);
printCliResult({
script: 'backup-db-data',
ok: false,
summary: error.message || String(error),
});
});
+24
View File
@@ -0,0 +1,24 @@
/**
* Print a standardized final SUCCESS/FAILED banner and exit.
* Always the last stdout before process exit in deploy scripts.
*/
const WIDTH = 72;
function printCliResult({ script, ok, summary, details = [] }) {
const label = ok ? 'SUCCESS' : 'FAILED';
const banner = `===== ${label}: ${script} =====`;
const pad = Math.max(0, WIDTH - banner.length);
const line = banner + '='.repeat(pad);
console.log('');
console.log(line);
if (summary) console.log(summary);
for (const detail of details) {
if (detail) console.log(detail);
}
console.log('='.repeat(WIDTH));
process.exit(ok ? 0 : 1);
}
module.exports = { printCliResult };
+27 -1
View File
@@ -7,6 +7,7 @@ const fs = require('fs');
const path = require('path');
const pool = require('../server/db');
const { generateThumbnailFromFull } = require('./image-fetcher');
const { printCliResult } = require('./lib/cli-result');
const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || './data/images');
const PORTRAIT_THUMB_WIDTH = 256;
@@ -70,7 +71,32 @@ async function generatePortraitThumb(row) {
}
await pool.end();
if (errors.length) {
printCliResult({
script: 'regenerate-portrait-thumbs',
ok: false,
summary: `Portrait thumbnail regeneration finished with ${errors.length} error(s).`,
details: [
`Generated: ${generated}, skipped: ${skipped}`,
...errors.slice(0, 5).map((e) => ` #${e.id} ${e.name}: ${e.error}`),
],
});
}
printCliResult({
script: 'regenerate-portrait-thumbs',
ok: true,
summary: 'Portrait thumbnail regeneration complete.',
details: [`Generated: ${generated}, skipped: ${skipped}`],
});
})().catch((err) => {
console.error(err);
pool.end().finally(() => process.exit(1));
pool.end().finally(() => {
printCliResult({
script: 'regenerate-portrait-thumbs',
ok: false,
summary: err.message || String(err),
});
});
});
+28 -1
View File
@@ -7,6 +7,7 @@ const path = require('path');
const sharp = require('sharp');
const pool = require('../server/db');
const { generateThumbnailFromFull } = require('./image-fetcher');
const { printCliResult } = require('./lib/cli-result');
const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || './data/images');
@@ -91,7 +92,33 @@ async function aspectRatio(filePath) {
}
await pool.end();
if (errors.length) {
printCliResult({
script: 'regenerate-thumbnails',
ok: false,
summary: `Painting thumbnail regeneration finished with ${errors.length} error(s).`,
details: [
`Regenerated: ${regenerated}, skipped: ${skipped}`,
...errors.slice(0, 5).map((e) => ` #${e.id} ${e.title}: ${e.error}`),
],
});
}
printCliResult({
script: 'regenerate-thumbnails',
ok: true,
summary: 'Painting thumbnail regeneration complete.',
details: [
`Regenerated: ${regenerated}, skipped: ${skipped}`,
`Aspect-ratio mismatches before fix: ${mismatchesBefore}`,
],
});
})().catch((err) => {
console.error(err);
process.exit(1);
printCliResult({
script: 'regenerate-thumbnails',
ok: false,
summary: err.message || String(err),
});
});
+13 -1
View File
@@ -14,6 +14,7 @@ const {
loadDevPgConfig,
loadProdPgConfig,
} = require('./db-env');
const { printCliResult } = require('./lib/cli-result');
const { Client } = pg;
@@ -185,9 +186,20 @@ async function main() {
await client.end();
console.log(`Restore complete: ${restored} statements into "${dbName}".`);
printCliResult({
script: 'restore-db-data',
ok: true,
summary: `Restore complete into "${dbName}".`,
details: [`Statements restored: ${restored}`],
});
}
main().catch((error) => {
console.error(error);
process.exit(1);
printCliResult({
script: 'restore-db-data',
ok: false,
summary: error.message || String(error),
});
});
+45
View File
@@ -0,0 +1,45 @@
/**
* Run painting + portrait thumbnail regeneration and print one combined result.
*/
const { spawnSync } = require('child_process');
const path = require('path');
const { printCliResult } = require('./lib/cli-result');
const rootDir = path.join(__dirname, '..');
const scripts = [
{ name: 'regenerate-thumbnails', file: 'regenerate-thumbnails.js' },
{ name: 'regenerate-portrait-thumbs', file: 'regenerate-portrait-thumbs.js' },
];
function runScript(script) {
const result = spawnSync(process.execPath, [path.join(__dirname, script.file)], {
cwd: rootDir,
stdio: 'inherit',
env: process.env,
});
return result.status ?? 1;
}
let failed = null;
for (const script of scripts) {
const code = runScript(script);
if (code !== 0) {
failed = script.name;
break;
}
}
if (failed) {
printCliResult({
script: 'devtoprod-thumbnails',
ok: false,
summary: `Thumbnail regeneration failed at ${failed}.`,
});
}
printCliResult({
script: 'devtoprod-thumbnails',
ok: true,
summary: 'Painting and portrait thumbnail regeneration complete.',
details: ['Both regenerate-thumbnails and regenerate-portrait-thumbs succeeded.'],
});