Files
memind/release-gate/artifact.mjs
T
john fb6865638a
Memind CI / Test, build, and release guards (pull_request) Successful in 1m32s
fix: align DeepSeek canary tool rounds with gate
2026-07-26 23:49:32 +08:00

166 lines
5.0 KiB
JavaScript

import crypto from 'node:crypto';
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import path from 'node:path';
export const REQUIRED_PORTAL_RUNTIME_PATHS = Object.freeze([
'server.mjs',
'memind-canary-proxy.mjs',
'deepseek-no-think-proxy.mjs',
'wechat-mp.bundle.mjs',
'mindspace-sandbox-mcp.mjs',
'tkmind-search-mcp.mjs',
'tkmind-excel-mcp.mjs',
'mindspace-public-links.mjs',
'dist',
'package.json',
'scripts/run-memind-portal-prod.sh',
'scripts/run-memind-portal-candidate.sh',
'scripts/run-memind-canary-proxy-prod.sh',
'scripts/run-deepseek-compat-proxy-candidate.sh',
'scripts/goosed-canary.compose.yml',
'scripts/check-mindspace-public-links.mjs',
'scripts/load-env.mjs',
'scripts/wechat-mp-menu.mjs',
'scripts/memind-portal-tunnel.sh',
]);
export const FORBIDDEN_PORTAL_RUNTIME_PATHS = Object.freeze([
'.env',
'.git',
'data',
'dist/dev',
'dist/hello-john.html',
'users',
'logs',
'MindSpace',
'.tailscale',
'public/plaza-covers',
]);
export async function removeForbiddenPortalRuntimePaths(artifactPath) {
const root = path.resolve(artifactPath);
const segments = root.split(path.sep);
if (path.basename(root) !== 'portal' || !segments.includes('.runtime')) {
throw new Error('Refusing to sanitize a path outside an explicit .runtime/portal directory');
}
for (const relative of FORBIDDEN_PORTAL_RUNTIME_PATHS) {
await fsp.rm(path.join(root, relative), { recursive: true, force: true });
}
}
export function assertPortalRuntimePath(artifactPath, {
repoRoot,
} = {}) {
const expected = path.resolve(repoRoot, '.runtime', 'portal');
const actual = path.resolve(artifactPath);
if (actual !== expected) {
throw new Error(`Portal release gate artifact must be ${expected}`);
}
return actual;
}
async function sha256File(filePath) {
const hash = crypto.createHash('sha256');
await new Promise((resolve, reject) => {
const stream = fs.createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('error', reject);
stream.on('end', resolve);
});
return hash.digest('hex');
}
async function walk(root, current = root) {
const entries = await fsp.readdir(current, { withFileTypes: true });
entries.sort((a, b) => a.name.localeCompare(b.name));
const result = [];
for (const entry of entries) {
const absolute = path.join(current, entry.name);
const relative = path.relative(root, absolute).split(path.sep).join('/');
const stat = await fsp.lstat(absolute);
if (stat.isDirectory()) {
result.push({ type: 'dir', path: relative, mode: stat.mode & 0o777 });
result.push(...await walk(root, absolute));
} else if (stat.isFile()) {
result.push({
type: 'file',
path: relative,
mode: stat.mode & 0o777,
size: stat.size,
sha256: await sha256File(absolute),
});
} else if (stat.isSymbolicLink()) {
const target = await fsp.readlink(absolute);
const resolved = path.resolve(path.dirname(absolute), target);
const relativeResolved = path.relative(root, resolved);
if (path.isAbsolute(target) || relativeResolved.startsWith(`..${path.sep}`) || relativeResolved === '..') {
throw new Error(`Artifact symlink escapes root: ${relative}`);
}
result.push({ type: 'symlink', path: relative, target });
} else {
throw new Error(`Unsupported artifact entry: ${relative}`);
}
}
return result;
}
export async function hashArtifact(artifactPath) {
const absolute = path.resolve(artifactPath);
const stat = await fsp.stat(absolute);
if (stat.isFile()) {
return {
sha256: await sha256File(absolute),
kind: 'file',
files: 1,
bytes: stat.size,
};
}
if (!stat.isDirectory()) {
throw new Error(`Artifact must be a file or directory: ${absolute}`);
}
const entries = await walk(absolute);
const hash = crypto.createHash('sha256');
for (const entry of entries) {
hash.update(JSON.stringify(entry));
hash.update('\n');
}
return {
sha256: hash.digest('hex'),
kind: 'directory-tree',
files: entries.filter((entry) => entry.type === 'file').length,
bytes: entries.reduce((sum, entry) => sum + (entry.size ?? 0), 0),
entries,
};
}
export async function inspectPortalRuntime(artifactPath) {
const root = path.resolve(artifactPath);
const stat = await fsp.stat(root);
if (!stat.isDirectory()) {
throw new Error('Portal runtime inspection requires a directory artifact');
}
const missing = [];
for (const relative of REQUIRED_PORTAL_RUNTIME_PATHS) {
try {
await fsp.access(path.join(root, relative));
} catch {
missing.push(relative);
}
}
const forbidden = [];
for (const relative of FORBIDDEN_PORTAL_RUNTIME_PATHS) {
try {
await fsp.access(path.join(root, relative));
forbidden.push(relative);
} catch {
// Expected: runtime artifacts must not contain persisted production state.
}
}
return {
missing,
forbidden,
passed: missing.length === 0 && forbidden.length === 0,
};
}