fix: harden release gate and page delivery
Memind CI / Test, build, and release guards (push) Failing after 12m3s

This commit is contained in:
john
2026-07-26 14:32:01 +08:00
parent d3141a3e91
commit b1577a16e9
61 changed files with 6017 additions and 89 deletions
+53 -19
View File
@@ -5,6 +5,8 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
import { removeForbiddenPortalRuntimePaths } from '../release-gate/artifact.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(__dirname, '..');
const runtimeRoot = path.join(root, '.runtime', 'portal');
@@ -18,6 +20,17 @@ const skipNodeModules = process.argv.includes('--skip-node-modules');
const runtimeNodeTarget = process.env.PORTAL_RUNTIME_NODE_TARGET || 'node24';
const runtimeInstallMode = process.env.PORTAL_RUNTIME_INSTALL_MODE || 'bundle-node-modules';
const linuxArm64ResvgPackage = '@resvg/resvg-js-linux-arm64-gnu';
const linuxArm64Argon2Package = '@node-rs/argon2-linux-arm64-gnu';
const linuxArm64NativePackages = [
{
hostPackage: '@resvg/resvg-js',
targetPackage: linuxArm64ResvgPackage,
},
{
hostPackage: '@node-rs/argon2',
targetPackage: linuxArm64Argon2Package,
},
];
const externalPackages = [
'@img/sharp-darwin-arm64',
@@ -217,6 +230,8 @@ async function copyRuntimeAssets() {
await copyDir(publicDir, path.join(runtimeRoot, 'public'));
}
await removeForbiddenPortalRuntimePaths(runtimeRoot);
if (await exists(schemaFile)) {
await fs.copyFile(schemaFile, path.join(runtimeRoot, 'schema.sql'));
}
@@ -237,41 +252,51 @@ async function copyNodeModules() {
console.log('==> 拷贝生产运行依赖 node_modules');
await copyDir(resolvedNodeModulesDir, path.join(runtimeRoot, 'node_modules'));
await rewriteNodeModulesSymlinks(path.join(runtimeRoot, 'node_modules'), resolvedNodeModulesDir);
await installLinuxArm64ResvgBinary();
await installLinuxArm64NativePackages();
}
async function readLinuxArm64NativePackageVersion({ hostPackage, targetPackage }) {
const hostPackageJson = JSON.parse(
await fs.readFile(path.join(nodeModulesDir, hostPackage, 'package.json'), 'utf8'),
);
const version = hostPackageJson.optionalDependencies?.[targetPackage];
if (!version) {
throw new Error(`无法从 ${hostPackage} 确定 ${targetPackage} 的版本`);
}
return version;
}
// The runtime artifact is assembled on macOS but runs in a Linux ARM64 Colima VM.
// pnpm therefore copies the macOS optional resvg binary only. Keep the target
// native package alongside the copied node_modules so the dynamic require in
// @resvg/resvg-js resolves in production.
async function installLinuxArm64ResvgBinary() {
const resvgPackageJson = JSON.parse(
await fs.readFile(path.join(nodeModulesDir, '@resvg', 'resvg-js', 'package.json'), 'utf8'),
);
const version = resvgPackageJson.optionalDependencies?.[linuxArm64ResvgPackage];
if (!version) {
throw new Error(`无法确定 ${linuxArm64ResvgPackage} 的版本`);
}
const stagingDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-resvg-linux-arm64-'));
const targetDir = path.join(runtimeRoot, 'node_modules', '@resvg', 'resvg-js-linux-arm64-gnu');
// pnpm copies the host optional binaries only, so explicitly materialize every
// Linux ARM64 native package that an externalized runtime dependency loads.
async function installLinuxArm64NativePackage(nativePackage) {
const { targetPackage } = nativePackage;
const version = await readLinuxArm64NativePackageVersion(nativePackage);
const stagingDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-native-linux-arm64-'));
const targetDir = path.join(runtimeRoot, 'node_modules', ...targetPackage.split('/'));
try {
console.log(`==> 补齐 Linux ARM64 resvg 原生依赖 (${version})`);
await run('npm', ['pack', `${linuxArm64ResvgPackage}@${version}`, '--pack-destination', stagingDir]);
console.log(`==> 补齐 Linux ARM64 原生依赖 ${targetPackage} (${version})`);
await run('npm', ['pack', `${targetPackage}@${version}`, '--pack-destination', stagingDir]);
const archive = (await fs.readdir(stagingDir)).find((name) => name.endsWith('.tgz'));
if (!archive) throw new Error(`未生成 ${linuxArm64ResvgPackage} 安装包`);
if (!archive) throw new Error(`未生成 ${targetPackage} 安装包`);
await remove(targetDir);
await fs.mkdir(targetDir, { recursive: true });
await run('tar', ['-xzf', path.join(stagingDir, archive), '--strip-components=1', '-C', targetDir]);
const files = await fs.readdir(targetDir);
if (!files.some((name) => name.endsWith('.node'))) {
throw new Error(`${linuxArm64ResvgPackage} 缺少原生 .node 文件`);
throw new Error(`${targetPackage} 缺少原生 .node 文件`);
}
} finally {
await remove(stagingDir);
}
}
async function installLinuxArm64NativePackages() {
for (const nativePackage of linuxArm64NativePackages) {
await installLinuxArm64NativePackage(nativePackage);
}
}
async function rewriteNodeModulesSymlinks(runtimeNodeModulesDir, sourceNodeModulesDir) {
console.log('==> 重写 node_modules 顶层符号链接为 runtime 内相对路径');
const localNodeModulesRoot = `${sourceNodeModulesDir}${path.sep}`;
@@ -325,6 +350,14 @@ async function writeMetadata() {
.filter((pkg) => packageJson.dependencies?.[pkg])
.map((pkg) => [pkg, packageJson.dependencies[pkg]]),
);
const runtimeOptionalDependencies = Object.fromEntries(
await Promise.all(
linuxArm64NativePackages.map(async (nativePackage) => [
nativePackage.targetPackage,
await readLinuxArm64NativePackageVersion(nativePackage),
]),
),
);
const runtimePackageJson = {
name: `${packageJson.name}-portal-runtime`,
private: true,
@@ -333,6 +366,7 @@ async function writeMetadata() {
node: '>=22',
},
dependencies: runtimeDependencies,
optionalDependencies: runtimeOptionalDependencies,
};
await writeFile(path.join(runtimeRoot, 'package.json'), `${JSON.stringify(runtimePackageJson, null, 2)}\n`);
const tunnelScript = path.join(root, 'scripts', 'memind-portal-tunnel.sh');