mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-04 00:42:01 +08:00
Use MikroORM's official Node SQLite dialect with bounded database isolation, preserve legacy schema and data for rollback, and require Node 22.17 or newer. Update Debian runtimes and retain ARMv6 musl compatibility without a SQLite addon. Complete short archive writes, stop orphaned SQL workers after owner death, preserve buffered startup requests, and prestart the archived backend on every platform. Cover real package installation, migration, failure recovery, and legacy database compatibility. Keep authentication storage failures distinct from expired sessions with a pinned Keyv runtime, propagated cache errors, and retryable HTTP 503 responses. Preserve genuine 401 failures and verify legacy cache bytes, initialization safety, and session recovery.
152 lines
5.2 KiB
JavaScript
152 lines
5.2 KiB
JavaScript
const fs = require('node:fs/promises');
|
|
const { createWriteStream } = require('node:fs');
|
|
const path = require('node:path');
|
|
const { pipeline } = require('node:stream/promises');
|
|
const tar = require('tar');
|
|
const bundle = require('../cli/src/internal/maintenance/platformBundle.cjs');
|
|
const {
|
|
verifyUpgrade,
|
|
} = require('../cli/src/internal/maintenance/upgradeArtifacts.cjs');
|
|
const {
|
|
prestartBackend,
|
|
} = require('../cli/src/internal/maintenance/upgradePrestart.cjs');
|
|
|
|
async function normalizeModes(directory) {
|
|
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
const filename = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) {
|
|
await fs.chmod(filename, 0o755);
|
|
await normalizeModes(filename);
|
|
} else if (entry.isFile()) {
|
|
const stat = await fs.stat(filename);
|
|
await fs.chmod(filename, 0o644 | (stat.mode & 0o111));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function writeBundleArchive(workspace, filename) {
|
|
// Core streams complete partial writes; tar's file option uses fs-minipass,
|
|
// whose writer can silently discard a successful short write/writev result.
|
|
await pipeline(
|
|
tar.c({ gzip: true, portable: true, noMtime: true, cwd: workspace }, [
|
|
'qinglong',
|
|
]),
|
|
createWriteStream(filename, { highWaterMark: 256 * 1024 }),
|
|
);
|
|
}
|
|
|
|
async function buildBundle(root, output) {
|
|
root = path.resolve(root);
|
|
output = path.resolve(output);
|
|
const target = await bundle.runtimeTarget();
|
|
if (process.platform !== 'linux')
|
|
throw new Error('Release bundles must be built and probed on Linux');
|
|
const build = await verifyUpgrade(root, path.join(root, 'static'));
|
|
await bundle.smokeDependencies(root);
|
|
await fs.mkdir(output, { recursive: true });
|
|
const workspace = await fs.mkdtemp(path.join(output, '.bundle-'));
|
|
try {
|
|
const payload = path.join(workspace, 'qinglong');
|
|
const source = path.join(payload, 'source'),
|
|
staticRoot = path.join(payload, 'static');
|
|
await fs.mkdir(source, { recursive: true });
|
|
const excluded = new Set([
|
|
'.git',
|
|
'.tmp',
|
|
'.env',
|
|
'data',
|
|
'static',
|
|
'output',
|
|
'node_modules',
|
|
]);
|
|
for (const name of await fs.readdir(root)) {
|
|
if (excluded.has(name)) continue;
|
|
await fs.cp(path.join(root, name), path.join(source, name), {
|
|
recursive: true,
|
|
verbatimSymlinks: true,
|
|
});
|
|
}
|
|
await fs.cp(
|
|
path.join(root, 'node_modules'),
|
|
path.join(source, 'node_modules'),
|
|
{ recursive: true, verbatimSymlinks: true },
|
|
);
|
|
await fs.cp(path.join(root, 'static'), staticRoot, {
|
|
recursive: true,
|
|
verbatimSymlinks: true,
|
|
});
|
|
await normalizeModes(source);
|
|
await normalizeModes(staticRoot);
|
|
const manifest = {
|
|
format: 2,
|
|
sourceCommit: build.sourceCommit,
|
|
version: JSON.parse(
|
|
await fs.readFile(path.join(root, 'package.json'), 'utf8'),
|
|
).version,
|
|
lockfileSha256: build.lockfileSha256,
|
|
target: {
|
|
platform: target.platform,
|
|
arch: target.arch,
|
|
libc: target.libc,
|
|
libcMinimum: target.libcVersion,
|
|
endianness: target.endianness,
|
|
armFloatAbi: target.armFloatAbi,
|
|
},
|
|
nodeMinimum: '22.17.0',
|
|
nodeApi: 6,
|
|
files: await bundle.inventory({ source, static: staticRoot }),
|
|
};
|
|
await fs.writeFile(
|
|
path.join(payload, 'bundle.json'),
|
|
JSON.stringify(manifest),
|
|
);
|
|
console.log('Bundle inventory verified; writing archive');
|
|
const filename = `qinglong-${build.sourceCommit}-${bundle.targetKey(
|
|
target,
|
|
)}.tar.gz`;
|
|
await writeBundleArchive(workspace, path.join(output, filename));
|
|
// Verify the bytes users receive, not just the pre-archive build directory.
|
|
const roundtrip = path.join(workspace, 'roundtrip');
|
|
await fs.mkdir(roundtrip);
|
|
console.log('Verifying archived paths, permissions and checksums');
|
|
await bundle.extractArchive(path.join(output, filename), roundtrip);
|
|
const extracted = {
|
|
source: path.join(roundtrip, 'qinglong/source'),
|
|
static: path.join(roundtrip, 'qinglong/static'),
|
|
};
|
|
await bundle.verifyPayload(extracted, manifest, target);
|
|
await verifyUpgrade(extracted.source, extracted.static);
|
|
await bundle.smokeDependencies(extracted.source);
|
|
console.log('Archived production dependencies and SQLite verified');
|
|
await prestartBackend(extracted, { target });
|
|
console.log('Archived backend database, gRPC and HTTP startup verified');
|
|
const bytes = await fs.readFile(path.join(output, filename));
|
|
const record = {
|
|
format: 2,
|
|
sourceCommit: build.sourceCommit,
|
|
version: manifest.version,
|
|
target: bundle.targetKey(target),
|
|
filename,
|
|
baseImage: process.env.QL_BUNDLE_BASE_IMAGE,
|
|
size: bytes.length,
|
|
sha256: bundle.sha256(bytes),
|
|
};
|
|
await fs.writeFile(
|
|
path.join(output, filename + '.json'),
|
|
JSON.stringify(record) + '\n',
|
|
);
|
|
console.log(JSON.stringify(record));
|
|
return record;
|
|
} finally {
|
|
await fs.rm(workspace, { recursive: true, force: true });
|
|
}
|
|
}
|
|
module.exports = { buildBundle, normalizeModes, writeBundleArchive };
|
|
if (require.main === module)
|
|
buildBundle(process.argv[2] || '/ql', process.argv[3] || '/out').catch(
|
|
(e) => {
|
|
console.error(e);
|
|
process.exitCode = 1;
|
|
},
|
|
);
|