mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-07 19:19:53 +08:00
273 lines
8.8 KiB
JavaScript
273 lines
8.8 KiB
JavaScript
// A separate first-hop package for installed format-2 verifiers. Its database
|
|
// backend and dependencies remain the pinned legacy implementation. Only the
|
|
// updater and frontend are replaced; the next download uses runtimePackages.
|
|
const fs = require('node:fs/promises');
|
|
const { createReadStream } = require('node:fs');
|
|
const path = require('node:path');
|
|
const http = require('node:http');
|
|
const assert = require('node:assert/strict');
|
|
const { pipeline } = require('node:stream/promises');
|
|
const { once } = require('node:events');
|
|
const { collectBuildFiles } = require('../docker/build-manifest.cjs');
|
|
const { buildBundle } = require('./build-upgrade-bundle.cjs');
|
|
const artifacts = require('../cli/src/internal/maintenance/upgradeArtifacts.cjs');
|
|
const bundle = require('../cli/src/internal/maintenance/platformBundle.cjs');
|
|
const baseline = require('../docker/migration-bridge-images.json');
|
|
const { rebuildMigrationSqlite } = require('./rebuild-migration-sqlite.cjs');
|
|
const read = async (file) => JSON.parse(await fs.readFile(file, 'utf8'));
|
|
|
|
async function prepareBridge(legacy, current, destination) {
|
|
const oldBuild = await artifacts.verifyUpgrade(
|
|
legacy,
|
|
path.join(legacy, 'static'),
|
|
);
|
|
const build = await artifacts.verifyUpgrade(
|
|
current,
|
|
path.join(current, 'static'),
|
|
);
|
|
const pkg = await read(path.join(legacy, 'package.json'));
|
|
const next = await read(path.join(current, 'package.json'));
|
|
if (
|
|
oldBuild.sourceCommit !== baseline.sourceCommit ||
|
|
pkg.qinglongUpgradeFormat !== 2 ||
|
|
!pkg.dependencies?.sqlite3 ||
|
|
!pkg.dependencies?.sequelize ||
|
|
!next.dependencies?.['@mikro-orm/sql']
|
|
)
|
|
throw new Error(
|
|
'Migration bridge requires the pinned legacy backend and a modern runtime',
|
|
);
|
|
// Never copy user state, even if this builder is used outside Docker.
|
|
const excluded = new Set(['.git', '.tmp', '.env', 'data', 'output']);
|
|
await fs.cp(legacy, destination, {
|
|
recursive: true,
|
|
verbatimSymlinks: true,
|
|
filter: (file) =>
|
|
!excluded.has(path.relative(legacy, file).split(path.sep)[0]),
|
|
});
|
|
const maintenance = 'cli/src/internal/maintenance';
|
|
await fs.rm(path.join(destination, maintenance), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
await fs.cp(
|
|
path.join(current, maintenance),
|
|
path.join(destination, maintenance),
|
|
{ recursive: true },
|
|
);
|
|
await fs.rm(path.join(destination, 'static/dist'), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
await fs.cp(
|
|
path.join(current, 'static/dist'),
|
|
path.join(destination, 'static/dist'),
|
|
{ recursive: true },
|
|
);
|
|
const migrationBridge = {
|
|
format: 1,
|
|
legacySourceCommit: oldBuild.sourceCommit,
|
|
sourceCommit: build.sourceCommit,
|
|
targetVersion: next.version,
|
|
};
|
|
await fs.writeFile(
|
|
path.join(destination, 'package.json'),
|
|
JSON.stringify(
|
|
{
|
|
...pkg,
|
|
qinglongRuntimeMinimum: '22.17.0',
|
|
qinglongMigrationBridge: migrationBridge,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
await fs.writeFile(
|
|
path.join(destination, 'static/build-info.json'),
|
|
JSON.stringify({
|
|
...oldBuild,
|
|
sourceCommit: build.sourceCommit,
|
|
files: collectBuildFiles(path.join(destination, 'static')),
|
|
}),
|
|
);
|
|
// No backend bytes or dependency lock may change in the updater hop.
|
|
assert.equal(
|
|
bundle.sha256(
|
|
await fs.readFile(path.join(destination, 'static/build/app.js')),
|
|
),
|
|
oldBuild.files['build/app.js'],
|
|
);
|
|
assert.equal(
|
|
bundle.sha256(await fs.readFile(path.join(destination, 'pnpm-lock.yaml'))),
|
|
oldBuild.lockfileSha256,
|
|
);
|
|
return migrationBridge;
|
|
}
|
|
|
|
async function verifyBothDownloads(legacy, current, directory, records) {
|
|
let index, workspace;
|
|
const files = new Map(
|
|
records.map((r) => ['/' + r.filename, path.join(directory, r.filename)]),
|
|
);
|
|
const server = http.createServer((req, res) => {
|
|
if (!index) {
|
|
res.writeHead(503);
|
|
res.end();
|
|
return;
|
|
}
|
|
if (req.url === '/latest.json') {
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify(index));
|
|
} else if (files.has(req.url)) {
|
|
const record = records.find((r) => '/' + r.filename === req.url);
|
|
res.setHeader('Content-Length', record.size);
|
|
pipeline(createReadStream(files.get(req.url)), res).catch(() =>
|
|
res.destroy(),
|
|
);
|
|
} else {
|
|
res.writeHead(404);
|
|
res.end();
|
|
}
|
|
});
|
|
try {
|
|
server.listen(0, '127.0.0.1');
|
|
await once(server, 'listening');
|
|
const base = `http://127.0.0.1:${server.address().port}`;
|
|
const entry = (r) => ({
|
|
size: r.size,
|
|
sha256: r.sha256,
|
|
url: base + '/' + r.filename,
|
|
});
|
|
const [bridge, runtime] = records;
|
|
index = {
|
|
format: 2,
|
|
sourceCommit: runtime.sourceCommit,
|
|
packages: { [bridge.target]: entry(bridge) },
|
|
runtimePackages: { [runtime.target]: entry(runtime) },
|
|
};
|
|
workspace = await fs.mkdtemp(path.join(directory, '.legacy-client-'));
|
|
const env = {
|
|
...process.env,
|
|
QL_BRANCH: 'develop',
|
|
QL_UPGRADE_INDEX_URL: base + '/latest.json',
|
|
QL_UPGRADE_FILE: '',
|
|
NO_PROXY: '127.0.0.1',
|
|
no_proxy: '127.0.0.1',
|
|
};
|
|
const old = require(path.join(
|
|
legacy,
|
|
'cli/src/internal/maintenance/upgradeArtifacts.cjs',
|
|
));
|
|
const firstTmp = path.join(workspace, 'first');
|
|
await fs.mkdir(firstTmp);
|
|
// These are the unchanged, genuinely installed old download, dependency
|
|
// smoke, prestart and readiness verifiers. No injected probe or verifier.
|
|
const selected = await old.stageUpgrade({
|
|
root: legacy,
|
|
tmp: firstTmp,
|
|
env,
|
|
run: (program, args, capture) =>
|
|
old.runProcess(env, program, args, capture),
|
|
});
|
|
await old.selectedUpgrade(firstTmp, 'develop', env);
|
|
assert(
|
|
(await read(path.join(selected.source, 'package.json')))
|
|
.qinglongMigrationBridge,
|
|
);
|
|
const upgraded = require(path.join(
|
|
selected.source,
|
|
'cli/src/internal/maintenance/upgradeArtifacts.cjs',
|
|
));
|
|
const secondTmp = path.join(workspace, 'second');
|
|
await fs.mkdir(secondTmp);
|
|
const next = await upgraded.stageUpgrade({
|
|
root: selected.source,
|
|
tmp: secondTmp,
|
|
env,
|
|
run: (program, args, capture) =>
|
|
upgraded.runProcess(env, program, args, capture),
|
|
});
|
|
await upgraded.selectedUpgrade(secondTmp, 'develop', env);
|
|
const pkg = await read(path.join(next.source, 'package.json'));
|
|
assert(pkg.dependencies['@mikro-orm/sql']);
|
|
assert.equal(pkg.qinglongMigrationBridge, undefined);
|
|
await assert.rejects(
|
|
fs.access(path.join(next.source, 'node_modules/sqlite3')),
|
|
{ code: 'ENOENT' },
|
|
);
|
|
await assert.rejects(
|
|
fs.access(path.join(next.source, 'node_modules/sequelize')),
|
|
{ code: 'ENOENT' },
|
|
);
|
|
const oldBundle = require(path.join(
|
|
legacy,
|
|
'cli/src/internal/maintenance/platformBundle.cjs',
|
|
));
|
|
const manifest = await read(
|
|
path.join(path.dirname(selected.source), 'bundle.json'),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
oldBundle.assertCompatible(
|
|
manifest,
|
|
{ ...manifest.target, libcVersion: manifest.target.libcMinimum },
|
|
'20.18.1',
|
|
),
|
|
/requires Node/,
|
|
);
|
|
return {
|
|
legacyOnlineVerified: true,
|
|
modernOnlineVerified: true,
|
|
legacyNode20Rejected: true,
|
|
};
|
|
} finally {
|
|
server.closeAllConnections();
|
|
if (server.listening) await new Promise((resolve) => server.close(resolve));
|
|
if (workspace) await fs.rm(workspace, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function buildMigrationBridge(legacy, current, output) {
|
|
legacy = path.resolve(legacy);
|
|
current = path.resolve(current);
|
|
output = path.resolve(output);
|
|
await fs.mkdir(output, { recursive: true });
|
|
const workspace = await fs.mkdtemp(path.join(output, '.migration-'));
|
|
let bridge;
|
|
try {
|
|
const root = path.join(workspace, 'root');
|
|
await prepareBridge(legacy, current, root);
|
|
await rebuildMigrationSqlite(root);
|
|
const runtime = await buildBundle(current, output);
|
|
bridge = await buildBundle(root, output);
|
|
const verification = await verifyBothDownloads(legacy, current, output, [
|
|
bridge,
|
|
runtime,
|
|
]);
|
|
bridge = { ...bridge, migrationVerified: true, ...verification };
|
|
await fs.writeFile(
|
|
path.join(output, bridge.filename + '.json'),
|
|
JSON.stringify(bridge) + '\n',
|
|
);
|
|
console.log(JSON.stringify(bridge));
|
|
return bridge;
|
|
} catch (error) {
|
|
if (bridge)
|
|
await fs.rm(path.join(output, bridge.filename + '.json'), {
|
|
force: true,
|
|
});
|
|
throw error;
|
|
} finally {
|
|
await fs.rm(workspace, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
module.exports = { prepareBridge, verifyBothDownloads, buildMigrationBridge };
|
|
if (require.main === module)
|
|
buildMigrationBridge(process.argv[2], process.argv[3], process.argv[4]).catch(
|
|
(error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
},
|
|
);
|