feat: add guarded portal canary release
Memind CI / Test, build, and release guards (push) Failing after 2m14s
Memind CI / Test, build, and release guards (push) Failing after 2m14s
This commit is contained in:
@@ -5,6 +5,7 @@ import path from 'node:path';
|
||||
|
||||
export const REQUIRED_PORTAL_RUNTIME_PATHS = Object.freeze([
|
||||
'server.mjs',
|
||||
'memind-canary-proxy.mjs',
|
||||
'wechat-mp.bundle.mjs',
|
||||
'mindspace-sandbox-mcp.mjs',
|
||||
'tkmind-search-mcp.mjs',
|
||||
@@ -13,6 +14,9 @@ export const REQUIRED_PORTAL_RUNTIME_PATHS = Object.freeze([
|
||||
'dist',
|
||||
'package.json',
|
||||
'scripts/run-memind-portal-prod.sh',
|
||||
'scripts/run-memind-portal-candidate.sh',
|
||||
'scripts/run-memind-canary-proxy-prod.sh',
|
||||
'scripts/goosed-canary.compose.yml',
|
||||
'scripts/check-mindspace-public-links.mjs',
|
||||
'scripts/load-env.mjs',
|
||||
'scripts/wechat-mp-menu.mjs',
|
||||
|
||||
@@ -979,12 +979,14 @@ export const AUTOMATION_SUITES = Object.freeze([
|
||||
cases: {
|
||||
'REL-10': [
|
||||
'release contract: block new Agent runs, drain active work within a bound, checksum and back up, then recheck zero active runs immediately before swap',
|
||||
'canary contract: immutable user and WeChat identities route to an isolated candidate, while missing identity, resolver errors, and candidate failure fall back to stable',
|
||||
],
|
||||
},
|
||||
command: [
|
||||
process.execPath,
|
||||
'--test',
|
||||
'release-gate/canary-routing.test.mjs',
|
||||
'memind-canary-proxy.test.mjs',
|
||||
'release-gate/release-runtime-safety.test.mjs',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
const CANARY_RELEASE = path.join(ROOT, 'scripts', 'release-portal-canary-prod.sh');
|
||||
|
||||
test('production release verifies gate report before any 103 connection', async () => {
|
||||
const source = await fs.readFile(
|
||||
@@ -77,3 +78,51 @@ test('production release rejects --skip-tests before repository or network prefl
|
||||
assert.match(result.stderr, /禁止 --skip-tests/);
|
||||
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /103 只读预检/);
|
||||
});
|
||||
|
||||
test('production canary verifies the exact Gate artifact before any 103 preflight or upload', async () => {
|
||||
const source = await fs.readFile(CANARY_RELEASE, 'utf8');
|
||||
const gateIndex = source.indexOf('verify-release-gate-report.mjs');
|
||||
const preflightIndex = source.indexOf('Run 103 read-only preflight');
|
||||
const uploadIndex = source.indexOf('Upload the verified candidate bundle');
|
||||
assert.ok(gateIndex > 0, 'missing canary Gate verifier');
|
||||
assert.ok(preflightIndex > gateIndex, '103 preflight must follow Gate verification');
|
||||
assert.ok(uploadIndex > preflightIndex, 'upload must follow read-only preflight');
|
||||
assert.match(source, /branch.*!= "main"/);
|
||||
assert.match(source, /rev-parse origin\/main/);
|
||||
assert.match(source, /Production canary release forbids/);
|
||||
});
|
||||
|
||||
test('production canary keeps stable 8081 live and switches only after verified backups and fallback', async () => {
|
||||
const source = await fs.readFile(CANARY_RELEASE, 'utf8');
|
||||
const fullBackup = source.indexOf('Create and verify the full stable backup');
|
||||
const persistBackup = source.indexOf('Create and verify the persisted-data backup');
|
||||
const goosedStart = source.indexOf('Start an isolated goosed candidate on 18015');
|
||||
const candidateStart = source.indexOf('Start the passive candidate Portal on 18081');
|
||||
const proxyStart = source.indexOf('Start the fail-closed identity router on 18080');
|
||||
const tunnelSwitch = source.indexOf('Switch only the reverse tunnel');
|
||||
const fallbackProbe = source.indexOf('route-probe?username=john');
|
||||
assert.ok(fullBackup > 0);
|
||||
assert.ok(persistBackup > fullBackup);
|
||||
assert.ok(goosedStart > persistBackup);
|
||||
assert.ok(candidateStart > goosedStart);
|
||||
assert.ok(proxyStart > candidateStart);
|
||||
assert.ok(tunnelSwitch > proxyStart);
|
||||
assert.ok(fallbackProbe > tunnelSwitch);
|
||||
assert.match(source, /write_tunnel_plist 8081/);
|
||||
assert.match(source, /CANARY_USERNAMES="john"/);
|
||||
assert.match(source, /CANARY_WECHAT_USER_IDS="wx_ul610et8"/);
|
||||
assert.match(source, /route-probe\?\$\{probe_query\}/);
|
||||
assert.match(source, /candidate_healthy/);
|
||||
assert.match(source, /MEMIND_CANARY_RELEASE_ID/);
|
||||
assert.doesNotMatch(source, /lsof -tiTCP:8081/);
|
||||
assert.doesNotMatch(source, /bootout.*cn\.tkmind\.memind-portal/);
|
||||
});
|
||||
|
||||
test('release readiness ignores only the generated canary artifact cover path', async () => {
|
||||
const source = await fs.readFile(
|
||||
path.join(ROOT, 'scripts', 'check-release-ready.sh'),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(source, /:\(exclude\)\.runtime\/portal\/public\/plaza-covers\/\*\*/);
|
||||
assert.doesNotMatch(source, /:\(exclude\)\.runtime\/\*\*/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user