feat(F-155): completed feature

This commit is contained in:
chattie
2026-08-21 22:37:03 +02:00
parent ad040ae3a9
commit a225d87645
17 changed files with 377 additions and 17 deletions

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import pg from 'pg';
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const envPath = resolve(projectDir, '.env');
if (existsSync(envPath)) {
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (key && process.env[key] === undefined) process.env[key] = value;
}
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error('DATABASE_URL not set');
const recreate = process.argv.includes('--recreate');
const token = process.env.PERSISTENCE_CHECK_TOKEN ?? `persistence-${Date.now()}`;
const pgKey = 'infra.persistence_check';
const redisKey = 'infra:persistence_check';
function run(command, args, options = {}) {
return execFileSync(command, args, {
cwd: projectDir,
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
encoding: 'utf8',
});
}
function dockerCompose(args, options) {
return run('docker', ['compose', ...args], options);
}
function dockerExec(container, args, options) {
return run('docker', ['exec', container, ...args], options);
}
function waitForInfrastructure() {
const deadline = Date.now() + 60_000;
while (Date.now() < deadline) {
try {
dockerExec('mdv-dev-postgres', ['pg_isready', '-U', 'mdv', '-d', 'mercadodevida'], {
capture: true,
});
dockerExec('mdv-dev-redis', ['redis-cli', 'ping'], { capture: true });
return;
} catch {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
}
}
throw new Error('PostgreSQL/Redis did not become ready in 60s');
}
async function writePostgresMarker() {
const pool = new pg.Pool({ connectionString: databaseUrl });
try {
await pool.query(
`INSERT INTO app_meta (key, value, updated_at)
VALUES ($1, $2, now())
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
[pgKey, token],
);
} finally {
await pool.end();
}
}
async function readPostgresMarker() {
const pool = new pg.Pool({ connectionString: databaseUrl });
try {
const result = await pool.query('SELECT value FROM app_meta WHERE key = $1', [pgKey]);
return result.rows[0]?.value;
} finally {
await pool.end();
}
}
function writeRedisMarker() {
dockerExec('mdv-dev-redis', ['redis-cli', 'SET', redisKey, token]);
}
function readRedisMarker() {
return dockerExec('mdv-dev-redis', ['redis-cli', 'GET', redisKey], { capture: true }).trim();
}
function printMounts() {
const pgMount = run(
'docker',
[
'inspect',
'mdv-dev-postgres',
'--format',
'{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}',
],
{ capture: true },
).trim();
const redisMount = run(
'docker',
[
'inspect',
'mdv-dev-redis',
'--format',
'{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}',
],
{ capture: true },
).trim();
const redisAppendOnly = dockerExec(
'mdv-dev-redis',
['redis-cli', 'CONFIG', 'GET', 'appendonly'],
{
capture: true,
},
).trim();
console.log(`PostgreSQL mount: ${pgMount}`);
console.log(`Redis mount: ${redisMount}`);
console.log(`Redis appendonly config:\n${redisAppendOnly}`);
}
console.log('[INFO] Starting infrastructure through docker compose...');
dockerCompose(['up', '-d', 'postgres', 'redis']);
waitForInfrastructure();
printMounts();
console.log(`[INFO] Writing persistence marker: ${token}`);
await writePostgresMarker();
writeRedisMarker();
if (recreate) {
console.log('[INFO] Recreating PostgreSQL and Redis containers while keeping named volumes...');
dockerCompose(['stop', 'postgres', 'redis']);
dockerCompose(['rm', '-f', 'postgres', 'redis']);
dockerCompose(['up', '-d', 'postgres', 'redis']);
} else {
console.log('[INFO] Restarting PostgreSQL and Redis containers...');
dockerCompose(['restart', 'postgres', 'redis']);
}
waitForInfrastructure();
printMounts();
const pgValue = await readPostgresMarker();
const redisValue = readRedisMarker();
if (pgValue !== token) {
throw new Error(`PostgreSQL marker mismatch: expected ${token}, got ${pgValue ?? '<missing>'}`);
}
if (redisValue !== token) {
throw new Error(`Redis marker mismatch: expected ${token}, got ${redisValue || '<missing>'}`);
}
console.log('[OK] PostgreSQL and Redis persistence verified');

View File

@@ -165,21 +165,21 @@ start_infrastructure() {
command -v docker >/dev/null 2>&1 || { echo '[FAIL] Docker is required' >&2; exit 1; }
echo '[INFO] Starting PostgreSQL and Redis...'
local service container
for service in postgres redis; do
container="mdv-dev-$service"
if docker inspect "$container" >/dev/null 2>&1; then
docker start "$container" >/dev/null
else
(cd "$PROJECT_DIR" && docker compose up -d "$service")
fi
done
# Always go through Compose instead of plain `docker start` so changes such
# as Redis AOF or explicit volume names are reconciled without losing named
# volumes. Compose recreates containers when needed but keeps
# project_mdv_pg_data/project_mdv_redis_data attached.
(cd "$PROJECT_DIR" && docker compose up -d postgres redis)
local deadline=$((SECONDS + 60))
until docker exec mdv-dev-postgres pg_isready -U mdv -d mercadodevida >/dev/null 2>&1; do
(( SECONDS < deadline )) || { echo '[FAIL] PostgreSQL did not become ready' >&2; exit 1; }
sleep 1
done
until docker exec mdv-dev-redis redis-cli ping >/dev/null 2>&1; do
(( SECONDS < deadline )) || { echo '[FAIL] Redis did not become ready' >&2; exit 1; }
sleep 1
done
}
install_dependencies() {