Files
memind/scripts/run-release-gate-runtime-upgrade.mjs
T
john b1577a16e9
Memind CI / Test, build, and release guards (push) Failing after 12m3s
fix: harden release gate and page delivery
2026-07-26 14:32:01 +08:00

136 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs/promises';
import path from 'node:path';
import mysql from 'mysql2/promise';
import pg from 'pg';
import { assertPortalRuntimePath, hashArtifact } from '../release-gate/artifact.mjs';
import { createLocalGateStack } from '../release-gate/local-stack.mjs';
const { Client: PgClient } = pg;
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
const runtimeRoot = assertPortalRuntimePath(path.join(ROOT, '.runtime', 'portal'), { repoRoot: ROOT });
const artifactBefore = await hashArtifact(runtimeRoot);
const runId = `upgrade-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}`;
const markerUserId = '00000000-0000-4000-8000-000000000701';
const markerUsername = 'gate_upgrade_fixture';
const markerValue = 'sanitized-page-data-fixture';
const stack = await createLocalGateStack({
root: ROOT,
runId,
port: Number(process.env.MEMIND_RELEASE_GATE_UPGRADE_PORT ?? 19085),
portalRoot: runtimeRoot,
nodeEnv: 'production',
runtimeProfile: 'production',
async beforePortalStart({ mysqlUrl, pgUrl }) {
const schemaSql = await fs.readFile(path.join(ROOT, 'schema.sql'), 'utf8');
const mysqlConnection = await mysql.createConnection({
uri: mysqlUrl,
multipleStatements: true,
});
try {
await mysqlConnection.query(schemaSql);
const now = Date.now();
await mysqlConnection.query(
`INSERT INTO h5_users
(id, username, slug, email, display_name, salt, password_hash, password_algorithm,
role, status, plan_type, workspace_root, low_balance_gift_eligible,
low_balance_gift_granted_at, created_at, updated_at)
VALUES (?, ?, ?, NULL, ?, ?, ?, 'pbkdf2-sha512',
'user', 'active', 'free', ?, 0, NULL, ?, ?)`,
[
markerUserId,
markerUsername,
markerUsername,
'脱敏升级测试用户',
'fixture-salt',
'fixture-hash',
`/sanitized/${markerUserId}`,
now,
now,
],
);
await mysqlConnection.query('ALTER TABLE h5_users DROP INDEX uq_h5_users_slug');
await mysqlConnection.query('ALTER TABLE h5_users DROP INDEX uq_h5_users_email');
await mysqlConnection.query(
`ALTER TABLE h5_users
DROP COLUMN slug,
DROP COLUMN email,
DROP COLUMN password_algorithm,
DROP COLUMN plan_type`,
);
} finally {
await mysqlConnection.end();
}
const pgClient = new PgClient({ connectionString: pgUrl });
await pgClient.connect();
try {
await pgClient.query(
`CREATE TABLE gate_sanitized_upgrade_fixture (
id TEXT PRIMARY KEY,
value TEXT NOT NULL
)`,
);
await pgClient.query(
'INSERT INTO gate_sanitized_upgrade_fixture (id, value) VALUES ($1, $2)',
['fixture-1', markerValue],
);
} finally {
await pgClient.end();
}
},
});
try {
async function verifyPreservedState() {
const mysqlConnection = await mysql.createConnection(stack.mysqlUrl);
try {
const [users] = await mysqlConnection.query(
`SELECT username, slug, password_algorithm, plan_type
FROM h5_users
WHERE id = ?`,
[markerUserId],
);
if (users.length !== 1) throw new Error('sanitized MySQL user fixture was lost');
if (users[0].slug !== markerUsername) throw new Error('legacy user slug was not migrated');
if (users[0].password_algorithm !== 'pbkdf2-sha512') {
throw new Error('legacy password algorithm default was not restored');
}
if (users[0].plan_type !== 'free') throw new Error('legacy plan default was not restored');
} finally {
await mysqlConnection.end();
}
const pgClient = new PgClient({ connectionString: stack.pgUrl });
await pgClient.connect();
try {
const result = await pgClient.query(
'SELECT value FROM gate_sanitized_upgrade_fixture WHERE id = $1',
['fixture-1'],
);
if (result.rows[0]?.value !== markerValue) {
throw new Error('sanitized PostgreSQL fixture was lost');
}
} finally {
await pgClient.end();
}
}
await verifyPreservedState();
await stack.restartPortal();
await verifyPreservedState();
console.log('PASS REL-07 legacy MySQL columns migrate and sanitized user data is preserved');
console.log('PASS REL-07 Page Data PostgreSQL fixture survives two idempotent packaged-runtime starts');
} finally {
await stack.cleanup();
const artifactAfter = await hashArtifact(runtimeRoot);
if (artifactAfter.sha256 !== artifactBefore.sha256) {
throw new Error(
`packaged runtime mutated its own artifact during upgrade verification: ${artifactBefore.sha256} -> ${artifactAfter.sha256}`,
);
}
}