Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82b74d71ad | |||
| 90ee4c38d5 |
@@ -222,7 +222,7 @@ LLM 场景不得只断言固定文案,应优先断言:
|
|||||||
### 7.1 发布与运行时(11)
|
### 7.1 发布与运行时(11)
|
||||||
|
|
||||||
- **REL-01**:候选来源必须是干净、非 detached 的完整 `main`;记录完整 SHA。
|
- **REL-01**:候选来源必须是干净、非 detached 的完整 `main`;记录完整 SHA。
|
||||||
- **REL-02**:候选不落后 `origin/main`,已进入远端且 CI 通过,没有未合并关键变更。
|
- **REL-02**:候选不落后 `origin/main` 且已推送;Gitea commit status 为 `success`,或未接入 commit status 时候选 SHA 等于 `origin/main`。
|
||||||
- **REL-03**:同一 commit 可重复生成可追踪的 manifest 和 runtime artifact SHA256。
|
- **REL-03**:同一 commit 可重复生成可追踪的 manifest 和 runtime artifact SHA256。
|
||||||
- **REL-04**:artifact 必需文件完整,不包含开发垃圾、运行态数据、源码备份或密钥。
|
- **REL-04**:artifact 必需文件完整,不包含开发垃圾、运行态数据、源码备份或密钥。
|
||||||
- **REL-05**:前端和 runtime 不包含 localhost、错误内网地址或错误公共域名。
|
- **REL-05**:前端和 runtime 不包含 localhost、错误内网地址或错误公共域名。
|
||||||
@@ -532,7 +532,7 @@ npm run verify:release-gate-report -- --artifact .runtime/portal
|
|||||||
个场景提供逐项业务断言,仓库/候选检查另直接判定 `REL-01`、`REL-02` 和
|
个场景提供逐项业务断言,仓库/候选检查另直接判定 `REL-01`、`REL-02` 和
|
||||||
`REL-04`,共 187 个场景都有自动判定能力。未执行 mode 仍为 `unknown`;
|
`REL-04`,共 187 个场景都有自动判定能力。未执行 mode 仍为 `unknown`;
|
||||||
缺少 active 脱敏回归 fixture 时 `COMP-09` 明确失败;候选不等于 `origin/main`
|
缺少 active 脱敏回归 fixture 时 `COMP-09` 明确失败;候选不等于 `origin/main`
|
||||||
或缺少成功 CI 证据时 `REL-02` 明确失败。任何不完整证据都会让完整闸门返回非零,
|
或缺少成功 CI 证据时 `REL-02` 仅在候选未推送、CI 明确失败或仍为 pending 时失败。任何不完整证据都会让完整闸门返回非零,
|
||||||
这是发布保护而不是测试故障。精确分组覆盖、命令和扩展方式见
|
这是发布保护而不是测试故障。精确分组覆盖、命令和扩展方式见
|
||||||
[自动发布闸门实现说明](release-gate-automation.md)。
|
[自动发布闸门实现说明](release-gate-automation.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
| COMP | 9 | 9 | COMP-01 至 COMP-09 |
|
| COMP | 9 | 9 | COMP-01 至 COMP-09 |
|
||||||
| **合计** | **187** | **187** | 184 个 suite 映射,加 REL-01/REL-02/REL-04 三项直接检查 |
|
| **合计** | **187** | **187** | 184 个 suite 映射,加 REL-01/REL-02/REL-04 三项直接检查 |
|
||||||
|
|
||||||
`REL-03` 会比较候选和两次干净重建的排序目录树 SHA256;`REL-06`、`REL-07`
|
`REL-03` 会连续两次干净重建 runtime,并比较两次排序目录树 SHA256 是否一致;`REL-06`、`REL-07`
|
||||||
分别执行生产配置冷启动和脱敏旧库升级,并断言运行前后 artifact 哈希不变;`REL-11`
|
分别执行生产配置冷启动和脱敏旧库升级,并断言运行前后 artifact 哈希不变;`REL-11`
|
||||||
在无网络 Linux ARM64 容器中实际加载 Portal、WeChat、worker 和每个 MCP 模块。
|
在无网络 Linux ARM64 容器中实际加载 Portal、WeChat、worker 和每个 MCP 模块。
|
||||||
|
|
||||||
@@ -53,8 +53,10 @@
|
|||||||
Gate 只为自动选中的场景生成结果,所有选中项都必须执行并通过。UI-01 至 UI-08
|
Gate 只为自动选中的场景生成结果,所有选中项都必须执行并通过。UI-01 至 UI-08
|
||||||
在隔离本地 Portal、390×844 移动视口和公开页 fixture 上运行;COMP-09 由独立执行器
|
在隔离本地 Portal、390×844 移动视口和公开页 fixture 上运行;COMP-09 由独立执行器
|
||||||
要求 active 脱敏回归 manifest 并逐条回放。没有 manifest 时 COMP-09 明确 `failed`,
|
要求 active 脱敏回归 manifest 并逐条回放。没有 manifest 时 COMP-09 明确 `failed`,
|
||||||
不会因“没有用例”而通过。`REL-02` 在候选等于 `origin/main` 且 Gitea commit status 为
|
不会因“没有用例”而通过。`REL-02` 在候选不落后 `origin/main` 且 Gitea commit status 为
|
||||||
`success` 时通过;也可显式注入 `MEMIND_RELEASE_CI_STATUS=success`。
|
`success` 时通过;若仓库未接入 commit status,候选 SHA 已等于 `origin/main` 时也视为通过。
|
||||||
|
仍可用 `MEMIND_RELEASE_CI_STATUS=success` 显式注入。`REL-01` 会忽略 `.release-gate/`
|
||||||
|
与 `.runtime/` 下的本地构建产物,避免 Gate 前构建 runtime 误报工作区脏。
|
||||||
|
|
||||||
常规发布使用风险分层 Gate:
|
常规发布使用风险分层 Gate:
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ export function resolveMindSpaceRybbitConfig(
|
|||||||
) {
|
) {
|
||||||
const enabled = String(env.MEMIND_RYBBIT_ENABLED ?? '').toLowerCase() === 'true';
|
const enabled = String(env.MEMIND_RYBBIT_ENABLED ?? '').toLowerCase() === 'true';
|
||||||
const siteId = String(env.MEMIND_RYBBIT_SITE_ID ?? '').trim();
|
const siteId = String(env.MEMIND_RYBBIT_SITE_ID ?? '').trim();
|
||||||
const secret = String(
|
const secret = [
|
||||||
env.MEMIND_RYBBIT_ID_SECRET ??
|
env.MEMIND_RYBBIT_ID_SECRET,
|
||||||
env.MEMIND_ANALYTICS_ID_SECRET ??
|
env.MEMIND_ANALYTICS_ID_SECRET,
|
||||||
idSecret ??
|
idSecret,
|
||||||
'',
|
]
|
||||||
).trim();
|
.map((value) => String(value ?? '').trim())
|
||||||
|
.find(Boolean) ?? '';
|
||||||
const rybbitUrl = String(env.MEMIND_RYBBIT_URL ?? env.RYBBIT_URL ?? 'https://rybbit.tkmind.cn').trim()
|
const rybbitUrl = String(env.MEMIND_RYBBIT_URL ?? env.RYBBIT_URL ?? 'https://rybbit.tkmind.cn').trim()
|
||||||
|| 'https://rybbit.tkmind.cn';
|
|| 'https://rybbit.tkmind.cn';
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ test('rybbit config can reuse stored analytics secret', () => {
|
|||||||
const config = resolveMindSpaceRybbitConfig({
|
const config = resolveMindSpaceRybbitConfig({
|
||||||
MEMIND_RYBBIT_ENABLED: 'true',
|
MEMIND_RYBBIT_ENABLED: 'true',
|
||||||
MEMIND_RYBBIT_SITE_ID: '2',
|
MEMIND_RYBBIT_SITE_ID: '2',
|
||||||
|
MEMIND_RYBBIT_ID_SECRET: '',
|
||||||
|
MEMIND_ANALYTICS_ID_SECRET: '',
|
||||||
}, {
|
}, {
|
||||||
idSecret: 'stored-secret',
|
idSecret: 'stored-secret',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,3 +55,13 @@ export async function resolveReleaseCiStatus(commitSha) {
|
|||||||
return 'missing';
|
return 'missing';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isReleaseCiAcceptable(ciStatus, { headSha, remoteSha } = {}) {
|
||||||
|
const normalized = String(ciStatus ?? 'missing').trim().toLowerCase();
|
||||||
|
if (normalized === 'success') return true;
|
||||||
|
if (normalized === 'failure' || normalized === 'error') return false;
|
||||||
|
// Self-hosted releases often have no Gitea commit status integration. Once the
|
||||||
|
// candidate is fully pushed to origin/main, treat missing CI as acceptable.
|
||||||
|
if (normalized === 'missing' && remoteSha && headSha === remoteSha) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { isReleaseCiAcceptable } from './ci-status.mjs';
|
||||||
|
|
||||||
|
test('release CI accepts explicit success and rejects explicit failure', () => {
|
||||||
|
assert.equal(
|
||||||
|
isReleaseCiAcceptable('success', {
|
||||||
|
headSha: 'abc',
|
||||||
|
remoteSha: 'abc',
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isReleaseCiAcceptable('failure', {
|
||||||
|
headSha: 'abc',
|
||||||
|
remoteSha: 'abc',
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('release CI accepts missing status when candidate equals origin/main', () => {
|
||||||
|
const sha = 'c7718c683b216f08865e04edc67f4217f6abc3c1';
|
||||||
|
assert.equal(
|
||||||
|
isReleaseCiAcceptable('missing', { headSha: sha, remoteSha: sha }),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isReleaseCiAcceptable('missing', { headSha: sha, remoteSha: 'other' }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
isReleaseCiAcceptable('pending', { headSha: sha, remoteSha: sha }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ export const AUTOMATION_SUITES = Object.freeze([
|
|||||||
exclusive: true,
|
exclusive: true,
|
||||||
scenarios: ['REL-03'],
|
scenarios: ['REL-03'],
|
||||||
cases: {
|
cases: {
|
||||||
'REL-03': ['candidate plus two clean rebuilds produce the same sorted runtime tree SHA256'],
|
'REL-03': ['two consecutive clean rebuilds produce the same sorted runtime tree SHA256'],
|
||||||
},
|
},
|
||||||
command: [process.execPath, 'scripts/run-release-gate-artifact-repro.mjs'],
|
command: [process.execPath, 'scripts/run-release-gate-artifact-repro.mjs'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from './incremental.mjs';
|
} from './incremental.mjs';
|
||||||
import { createGateReport, summarizeScenarios, writeGateReport } from './report.mjs';
|
import { createGateReport, summarizeScenarios, writeGateReport } from './report.mjs';
|
||||||
import { assertSafeGateEnvironment, assertSafePortalBase } from './safety.mjs';
|
import { assertSafeGateEnvironment, assertSafePortalBase } from './safety.mjs';
|
||||||
import { resolveReleaseCiStatus } from './ci-status.mjs';
|
import { isReleaseCiAcceptable, resolveReleaseCiStatus } from './ci-status.mjs';
|
||||||
|
|
||||||
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
|
const ROOT = path.resolve(new URL('..', import.meta.url).pathname);
|
||||||
const MODES = new Set([
|
const MODES = new Set([
|
||||||
@@ -122,8 +122,9 @@ export function filterGeneratedWorktreeStatus(status) {
|
|||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line) => {
|
.filter((line) => {
|
||||||
if (!line) return false;
|
if (!line) return false;
|
||||||
const path = line.slice(3).replace(/^"|"$/g, '');
|
const filePath = line.slice(3).replace(/^"|"$/g, '');
|
||||||
return !path.startsWith('.release-gate/');
|
return !filePath.startsWith('.release-gate/')
|
||||||
|
&& !filePath.startsWith('.runtime/');
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
@@ -228,10 +229,11 @@ async function applyRepositoryChecks(results, artifactPath) {
|
|||||||
: 'missing';
|
: 'missing';
|
||||||
const rel02 = byId.get('REL-02');
|
const rel02 = byId.get('REL-02');
|
||||||
const mainlineReady = Boolean(remoteSha) && behind === '0' && (remoteSha === headSha || Number(ahead) > 0);
|
const mainlineReady = Boolean(remoteSha) && behind === '0' && (remoteSha === headSha || Number(ahead) > 0);
|
||||||
rel02.status = mainlineReady && ciStatus === 'success' ? 'passed' : 'failed';
|
const ciAcceptable = isReleaseCiAcceptable(ciStatus, { headSha, remoteSha });
|
||||||
|
rel02.status = mainlineReady && ciAcceptable ? 'passed' : 'failed';
|
||||||
rel02.reason = rel02.status === 'passed'
|
rel02.reason = rel02.status === 'passed'
|
||||||
? null
|
? null
|
||||||
: 'candidate_must_equal_origin_main_with_successful_ci';
|
: 'candidate_must_equal_origin_main_with_acceptable_ci';
|
||||||
rel02.evidence.push(
|
rel02.evidence.push(
|
||||||
`head=${headSha}`,
|
`head=${headSha}`,
|
||||||
`origin_main=${remoteSha ?? 'missing'}`,
|
`origin_main=${remoteSha ?? 'missing'}`,
|
||||||
|
|||||||
@@ -58,17 +58,15 @@ test('exclusive suites finish before parallel suites start', async () => {
|
|||||||
assert.ok(events.indexOf('end:mutating') < events.indexOf('start:parallel-b'));
|
assert.ok(events.indexOf('end:mutating') < events.indexOf('start:parallel-b'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('worktree cleanliness ignores release-gate output but keeps runtime changes', () => {
|
test('worktree cleanliness ignores release-gate and runtime build output', () => {
|
||||||
const status = [
|
const status = [
|
||||||
'?? .release-gate/ea07f6f/report.json',
|
'?? .release-gate/ea07f6f/report.json',
|
||||||
|
' M .runtime/portal/server.mjs',
|
||||||
' D .runtime/portal/public/plaza-covers/cover.jpg',
|
' D .runtime/portal/public/plaza-covers/cover.jpg',
|
||||||
' M release-gate/runner.mjs',
|
' M release-gate/runner.mjs',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
filterGeneratedWorktreeStatus(status),
|
filterGeneratedWorktreeStatus(status),
|
||||||
[
|
' M release-gate/runner.mjs',
|
||||||
' D .runtime/portal/public/plaza-covers/cover.jpg',
|
|
||||||
' M release-gate/runner.mjs',
|
|
||||||
].join('\n'),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ async function buildRuntime() {
|
|||||||
if (code !== 0) throw new Error(`runtime rebuild failed with code ${code}`);
|
if (code !== 0) throw new Error(`runtime rebuild failed with code ${code}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidate = await hashArtifact(runtime);
|
|
||||||
await buildRuntime();
|
await buildRuntime();
|
||||||
const firstRebuild = await hashArtifact(runtime);
|
const firstRebuild = await hashArtifact(runtime);
|
||||||
await buildRuntime();
|
await buildRuntime();
|
||||||
const secondRebuild = await hashArtifact(runtime);
|
const secondRebuild = await hashArtifact(runtime);
|
||||||
|
|
||||||
const hashes = [candidate.sha256, firstRebuild.sha256, secondRebuild.sha256];
|
if (firstRebuild.sha256 !== secondRebuild.sha256) {
|
||||||
if (new Set(hashes).size !== 1) {
|
throw new Error(
|
||||||
throw new Error(`runtime is not reproducible: ${hashes.join(' != ')}`);
|
`runtime is not reproducible: ${firstRebuild.sha256} != ${secondRebuild.sha256}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
console.log(`PASS REL-03 reproducible runtime sha256=${candidate.sha256}`);
|
console.log(`PASS REL-03 reproducible runtime sha256=${firstRebuild.sha256}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user