Skip to content

Commit db3cb0e

Browse files
committed
fix(ci): handle published package boundary
1 parent 202e18e commit db3cb0e

8 files changed

Lines changed: 148 additions & 23 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ jobs:
151151
node-version: 24.17.0
152152
cache: npm
153153
- run: npm ci
154-
- run: npm run release:dry-run:all
154+
- run: npm run release:dry-run:all -- --allow-existing-version
155155
- run: npm run test:pack-clean
156156

157157
aggregate:

‎changelogs/unreleased.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- **治理台账 resolver、immutable 分片与 GR 试点**:新增 `GovernanceLedgerManifestV1`、共享 resolver、锁内单调 `nextSequence`、可重建 `GovernanceLedgerIndexV1` 与 `governance ledger init|plan|apply|rollback|index`。普通 `init/update` 只做零搬迁 manifest/index 初始化;active 是唯一 writer,archive 只读,reopened 通过 manifest overlay 表达。真实 active-root 的最终 GR 试点把 39 个自包含终态 H2 移入摘要绑定 shard,canonical 记录总数迁移前后均为 990;混合容器 GR-068 被负向探针排除。两次早期异常均由事务恢复或 exact rollback 收敛,未引用 immutable shard按审计策略保留。
99
- **P1 产物打开 action/readback 闭环**:`ArtifactDeliveryResolver` 现在以 `presentationSurface + capability evidence` 为 renderer 主决策键,`hostSurface` 只验证 adapter 匹配;Codex Desktop 从无动作的“使用文件面板打开”文字改为真实本地文件 Markdown action。每个可见目标新增 `ArtifactDeliveryAttemptV1`,逐项记录 actionId、attempted、actionStatus、readback、status 与 fallbackReason;仅 action 与 readback 均成功才声明 opened,renderer-only、失败、不可回读或未知 surface 立即降级为绝对路径。报告/记忆相对内链仍由 `LinkCapabilityDecisionV1` 独立维护,不能冒充最终对话打开证据。
1010
- **P0 自适应流程、入口与语言连续性**:新增 `WorkflowPlanDecisionV1`,将流程仪式、方案深度、验证等级和强制义务分离;优先级固定为用户当前明确意图 > Profile `workflowRouting` 配置 > 项目事实智能识别 > 回退,并在入口初判与项目读取后二次判断。`LanguageContextV2` 让确认码、yes/no、版本、路径、代码与引用文本不再误切主语言。入口升级为 `EntryCheckModelV3` / `DevCodexVisibleEnvelopeV3` 的 PC0~PC10:PC0 显示 installed/runtime/source identity,PC8~PC10 显示流程与方案、验证预算/耗时、后续阶段和用户动作;V1/V2 仅保留读取兼容。
11-
- **P0 CI 与发布关键路径**:GitHub Actions 现在由 `CiValidationPlanV1` 直接复用 `ValidationImpactGraphV2` 选择 affected、package boundary 或 full;固定兼容矩阵回到 validation manifest 单一真相源。普通 push/PR 不再无条件跑 full、全平台矩阵和安装边界;nightly、release、手动 full 或 planner 不能证明完整影响面时才升级。aggregate 校验 required node receipts,planner 失败显式 BLOCK。
11+
- **P0 CI 与发布关键路径**:GitHub Actions 现在由 `CiValidationPlanV1` 直接复用 `ValidationImpactGraphV2` 选择 affected、package boundary 或 full;固定兼容矩阵回到 validation manifest 单一真相源。普通 push/PR 不再无条件跑 full、全平台矩阵和安装边界;nightly、release、手动 full 或 planner 不能证明完整影响面时才升级。aggregate 校验 required node receipts,planner 失败显式 BLOCK。普通 push/PR 的 package boundary 以显式 `--allow-existing-version` 仅接受 npmjs 对精确当前版本返回的已发布版本冲突,正式 release dry-run 继续严格;SkillRoute runtime 摘要补入 `workflow-plan-decision-v1.cjs`,并以当前候选隔离 Codex S15 刷新 portable capability evidence。
1212
- **发布成功与 finalize 解耦**:`qualify → publish → finalize` 三段分别保存精确候选与 `PublishedArtifactReceiptV1`。`npm publish` 成功后立即固化不可逆回执;registry 传播延迟、provenance 回读和 GitHub Release 可通过 `finalize-only` 恢复,禁止重复 publish。npm metadata 缺少可选 `gitHead` 只 WARN,返回冲突值、integrity/shasum/provenance 不一致仍 BLOCK。该修复对应实际 Publish run `33151952863`:publish 已成功,旧 postcheck 因 registry 延迟且强制要求 `gitHead` 形成假红。
1313
- **P0 context / Daily / CP 连续性**:`ContextSnapshotV1` 将稳定的 plan/source 内容身份与 `ContextObservationLeaseV1` 的 epoch/session/turn 交付态分离;同 host session、snapshot 未漂移的普通轮次可重绑,compact/stale、跨 session 与 source drift 仍失败关闭。Daily/SUMMARY/CP 四类 writer 统一 canonical physical path 锁键,并以 `MemoryTransactionConflictReceiptV2` 对同一纯操作最多有界协调一次,无法证明外部 writer 身份时保持 `UNVERIFIED`。未绑定 artifact path+digest 的 `memory_cp_confirm` 现以 `MEMORY_CP_CONFIRMATION_UNBOUND` 零写入拒绝,不再制造伪确认。
1414
- **P0 admission→owner 连续性(`CSD-context-admission-continuity-v1`)**:Hook 在 verified ingress 仍完整时写入 digest-bound `AdmissionIngressSnapshotV1`;memory MCP 可在 PostToolUse、只读工具或进程重启后按原 `ingressRef` 精确回读。`memory_task_admit_v2` 默认在同一调用内完成 admission、route bind、owner acquire 与 finalize;CP pending 时 owner 保持 `mutationAuthority=false`。分步兼容路径使用 30 分钟、task/project/root/session/route 绑定的单用途 `AdmissionContinuationLeaseV1`,成功后立即 consumed;跨域、过期、缺失和错误快照返回 `TASK_ADMISSION_CONTINUATION_*`,不再要求用户重复确认。

‎hooks/_runtime/evidence/codex-skill-route-pass.v1.json‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,24 @@
55
"hostVariant": "codex-cli/exec-user-global-local-stdio",
66
"testedVersion": "codex-cli 0.145.0 / source candidate 1.19.3",
77
"protocol": "MCP 2024-11-05",
8-
"runtimeContractDigest": "eebc6633fef059fafb3e1b6c184da58b19b0671f164c7e3048fbc73ab2148d8e",
9-
"hostAdapterDigest": "97e7ca11d4c0f96608f97c587ddc259d85687187e8e9415657dafeebc09fa64b",
10-
"sourceEvidenceDigest": "9b45405809477d9d5bd66de2cfff09665f9a037c9aeda8e3263e718e40413090",
8+
"runtimeContractDigest": "81dd84a946cb8e32cb8601f3374b0898aa8d6db0cd7c5ea45120d0609c6f46aa",
9+
"hostAdapterDigest": "03817425929c8c5e3f56374c25fc728422dacee5f4b3afcdc46c247a493cf891",
10+
"sourceEvidenceDigest": "653f19410586ff245ee803b525e4c47c95626a96a3a6a9cbe6635f68e4e4b641",
1111
"sourceProbe": {
1212
"schemaVersion": "SkillRouteS15EvidenceV1",
13-
"probeRunId": "s15-codex-probe-c3351d10-6f70-4bf6-9a36-926f1a07a3d2",
13+
"probeRunId": "s15-codex-probe-7eae3fc1-dbbc-48d8-86e7-c4ff6a4aa3b6",
1414
"authorizationSource": "isolated-probe-authority",
1515
"contextSource": "host-hooks",
1616
"observationMode": "hook-post-history",
1717
"receiptStatus": "relevant-complete",
18-
"completedAt": "2026-08-28T10:51:17.952Z"
18+
"completedAt": "2026-08-29T09:39:52.904Z"
1919
},
2020
"runtimeBinding": {
2121
"source": "isolated-source-candidate",
22-
"expectedDigest": "eebc6633fef059fafb3e1b6c184da58b19b0671f164c7e3048fbc73ab2148d8e",
23-
"generationDigest": "eebc6633fef059fafb3e1b6c184da58b19b0671f164c7e3048fbc73ab2148d8e",
24-
"modeReceiptDigest": "eebc6633fef059fafb3e1b6c184da58b19b0671f164c7e3048fbc73ab2148d8e",
25-
"routeEnvelopeDigest": "eebc6633fef059fafb3e1b6c184da58b19b0671f164c7e3048fbc73ab2148d8e"
22+
"expectedDigest": "81dd84a946cb8e32cb8601f3374b0898aa8d6db0cd7c5ea45120d0609c6f46aa",
23+
"generationDigest": "81dd84a946cb8e32cb8601f3374b0898aa8d6db0cd7c5ea45120d0609c6f46aa",
24+
"modeReceiptDigest": "81dd84a946cb8e32cb8601f3374b0898aa8d6db0cd7c5ea45120d0609c6f46aa",
25+
"routeEnvelopeDigest": "81dd84a946cb8e32cb8601f3374b0898aa8d6db0cd7c5ea45120d0609c6f46aa"
2626
},
2727
"probe": {
2828
"schemaVersion": "SkillRouteProbeSummaryV1",

‎hooks/_runtime/host-skill-route-capabilities.v1.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"status": "PASS",
1414
"testedVersion": "codex-cli 0.145.0 / source candidate 1.19.3",
1515
"protocol": "MCP 2024-11-05",
16-
"runtimeContractDigest": "eebc6633fef059fafb3e1b6c184da58b19b0671f164c7e3048fbc73ab2148d8e",
17-
"hostAdapterDigest": "97e7ca11d4c0f96608f97c587ddc259d85687187e8e9415657dafeebc09fa64b",
16+
"runtimeContractDigest": "81dd84a946cb8e32cb8601f3374b0898aa8d6db0cd7c5ea45120d0609c6f46aa",
17+
"hostAdapterDigest": "03817425929c8c5e3f56374c25fc728422dacee5f4b3afcdc46c247a493cf891",
1818
"evidenceRef": "hooks/_runtime/evidence/codex-skill-route-pass.v1.json",
19-
"evidenceDigest": "7a34a34da9cdedd6e0708e05feb9082cb0a488136295102f25b560a4100740f9",
19+
"evidenceDigest": "69cfe64bea1bd0e9ae7901c57ed6217d5ca807e59ff097674c33c949b7da1123",
2020
"entrySurface": "codex exec --ephemeral",
2121
"bootstrapDelivery": "stable user-global Hook launcher UserPromptSubmit or profile_context_plan fallback",
2222
"defaultEligible": true

‎hooks/_runtime/skill-route-mode.cjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ const RUNTIME_CONTRACT_FILES = Object.freeze([
9191
'task-recovery-store-v5.cjs',
9292
'workflow-operational-write-lease.cjs',
9393
'workflow-root-registry.v2.json',
94+
'workflow-plan-decision-v1.cjs',
9495
'workflow-route-decision-v2.cjs',
9596
'workspace-session-route-index-v1.cjs'
9697
])

‎scripts/publish-dry-run.js‎

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ const path = require('path')
77
const { runChecked } = require('./lib/checked-command')
88

99
const ROOT = path.resolve(__dirname, '..')
10-
const PACKAGE_NAME = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).name
10+
const PACKAGE_METADATA = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'))
11+
const PACKAGE_NAME = PACKAGE_METADATA.name
12+
const PACKAGE_VERSION = PACKAGE_METADATA.version
1113
const targets = {
1214
npmjs: { registry: 'https://registry.npmjs.org/', access: 'public' },
1315
github: { registry: 'https://npm.pkg.github.com/', access: 'restricted' }
@@ -62,6 +64,52 @@ function buildPublishArgs(name, packageName = PACKAGE_NAME, packageArtifact = '.
6264
return args
6365
}
6466

67+
function allowsExistingVersion(argv) {
68+
return argv.includes('--allow-existing-version')
69+
}
70+
71+
function isPublishedVersionCollision(error, packageVersion = PACKAGE_VERSION) {
72+
const evidence = error?.evidence
73+
if (!evidence || evidence.command !== 'npm' || evidence.exitCode !== 1 ||
74+
!Array.isArray(evidence.args) || evidence.args[0] !== 'publish' ||
75+
!evidence.args.includes('--dry-run') ||
76+
!evidence.args.includes(`--registry=${targets.npmjs.registry}`)) {
77+
return false
78+
}
79+
const collisionLine = `npm error You cannot publish over the previously published versions: ${packageVersion}.`
80+
const errorLines = String(evidence.stderr || '')
81+
.split(/\r?\n/)
82+
.map(line => line.trim())
83+
.filter(line => /^npm error\b/i.test(line))
84+
return errorLines.includes(collisionLine) && errorLines.every(line =>
85+
line === collisionLine || /^npm error A complete log of this run can be found in:/.test(line)
86+
)
87+
}
88+
89+
function runPublishDryRun(name, packageName, packageVersion, packageArtifact, options = {}) {
90+
const runner = options.run || runChecked
91+
const label = `publish-dry-run:${name}`
92+
try {
93+
return {
94+
label,
95+
...runner('npm', buildPublishArgs(name, packageName, packageArtifact), {
96+
cwd: options.cwd || ROOT,
97+
timeoutMs: options.timeoutMs || 120000
98+
})
99+
}
100+
} catch (error) {
101+
if (options.allowExistingVersion === true && name === 'npmjs' &&
102+
isPublishedVersionCollision(error, packageVersion)) {
103+
return {
104+
label,
105+
...error.evidence,
106+
acceptedFailure: 'PUBLISHED_VERSION_EXISTS'
107+
}
108+
}
109+
throw error
110+
}
111+
}
112+
65113
function parsePackJson(stdout) {
66114
const text = String(stdout || '').trim()
67115
const match = text.match(/(\[\s*\{[\s\S]*\}\s*\])\s*$/)
@@ -96,6 +144,7 @@ function createCandidateTarball(tempRoot) {
96144

97145
function main(argv = process.argv.slice(2)) {
98146
const selected = readTarget(argv)
147+
const allowExistingVersion = allowsExistingVersion(argv)
99148
if (!['npmjs', 'github', 'all'].includes(selected)) {
100149
console.error(`Unknown registry target: ${selected || '(missing)'}`)
101150
return 2
@@ -112,16 +161,20 @@ function main(argv = process.argv.slice(2)) {
112161
...candidate.evidence
113162
}]
114163
for (const name of names) {
115-
evidence.push({
116-
label: `publish-dry-run:${name}`,
117-
...runChecked('npm', buildPublishArgs(name, PACKAGE_NAME, candidate.tarballPath), {
118-
cwd: ROOT,
119-
timeoutMs: 120000
120-
})
121-
})
164+
evidence.push(runPublishDryRun(
165+
name,
166+
PACKAGE_NAME,
167+
PACKAGE_VERSION,
168+
candidate.tarballPath,
169+
{ allowExistingVersion }
170+
))
122171
}
123172
for (const item of evidence) {
124-
console.log(`✓ ${item.label}: exitCode=${item.exitCode} durationMs=${item.durationMs}`)
173+
if (item.acceptedFailure) {
174+
console.log(`✓ ${item.label}: acceptedFailure=${item.acceptedFailure} originalExitCode=${item.exitCode} durationMs=${item.durationMs}`)
175+
} else {
176+
console.log(`✓ ${item.label}: exitCode=${item.exitCode} durationMs=${item.durationMs}`)
177+
}
125178
}
126179
return 0
127180
} catch (error) {
@@ -140,13 +193,16 @@ function main(argv = process.argv.slice(2)) {
140193
if (require.main === module) process.exitCode = main()
141194

142195
module.exports = {
196+
allowsExistingVersion,
143197
assertTargetSupported,
144198
buildPublishArgs,
145199
createCandidateTarball,
200+
isPublishedVersionCollision,
146201
main,
147202
packageScope,
148203
parsePackJson,
149204
readTarget,
205+
runPublishDryRun,
150206
supportedTargets,
151207
targets
152208
}

‎scripts/test-checked-command.js‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ const {
1515
} = require('./lib/checked-command')
1616
const {
1717
assertTargetSupported,
18+
allowsExistingVersion,
1819
buildPublishArgs,
20+
isPublishedVersionCollision,
1921
packageScope,
2022
parsePackJson,
23+
runPublishDryRun,
2124
supportedTargets
2225
} = require('./publish-dry-run')
2326

@@ -159,6 +162,70 @@ try {
159162
parsePackJson('npm notice ignored\n[{"filename":"devcodex-1.16.4.tgz"}]\n'),
160163
{ filename: 'devcodex-1.16.4.tgz' }
161164
)
165+
assert.strictEqual(allowsExistingVersion(['--registry', 'all']), false)
166+
assert.strictEqual(allowsExistingVersion(['--allow-existing-version']), true)
167+
168+
const publishedVersion = '1.19.3'
169+
const collisionEvidence = {
170+
code: 'ECOMMAND',
171+
command: 'npm',
172+
resolvedCommand: 'npm',
173+
args: [
174+
'publish',
175+
candidateTarball,
176+
'--dry-run',
177+
'--json',
178+
'--registry=https://registry.npmjs.org/',
179+
'--access=public'
180+
],
181+
cwd: root,
182+
exitCode: 1,
183+
signal: null,
184+
durationMs: 12,
185+
stdout: '',
186+
stderr: [
187+
'npm warn This command requires you to be logged in to https://registry.npmjs.org/ (dry-run)',
188+
`npm error You cannot publish over the previously published versions: ${publishedVersion}.`,
189+
'npm error A complete log of this run can be found in: /tmp/npm-debug.log'
190+
].join('\n')
191+
}
192+
const collisionError = new CheckedCommandError('published version collision', collisionEvidence)
193+
assert.strictEqual(isPublishedVersionCollision(collisionError, publishedVersion), true)
194+
const acceptedCollision = runPublishDryRun(
195+
'npmjs',
196+
'devcodex',
197+
publishedVersion,
198+
candidateTarball,
199+
{
200+
cwd: root,
201+
allowExistingVersion: true,
202+
run: () => { throw collisionError }
203+
}
204+
)
205+
assert.strictEqual(acceptedCollision.acceptedFailure, 'PUBLISHED_VERSION_EXISTS')
206+
assert.strictEqual(acceptedCollision.exitCode, 1)
207+
assert.throws(
208+
() => runPublishDryRun('npmjs', 'devcodex', publishedVersion, candidateTarball, {
209+
cwd: root,
210+
allowExistingVersion: false,
211+
run: () => { throw collisionError }
212+
}),
213+
error => error === collisionError
214+
)
215+
const unrelatedFailure = new CheckedCommandError('authentication failed', {
216+
...collisionEvidence,
217+
stderr: 'npm error code ENEEDAUTH\nnpm error need auth This command requires you to be logged in.'
218+
})
219+
assert.strictEqual(isPublishedVersionCollision(unrelatedFailure, publishedVersion), false)
220+
assert.throws(
221+
() => runPublishDryRun('npmjs', 'devcodex', publishedVersion, candidateTarball, {
222+
cwd: root,
223+
allowExistingVersion: true,
224+
run: () => { throw unrelatedFailure }
225+
}),
226+
error => error === unrelatedFailure
227+
)
228+
assert.strictEqual(isPublishedVersionCollision(collisionError, '1.19.4'), false)
162229

163230
console.log('✓ checked-command fail-fast, literal-glob and registry fixtures passed')
164231
} finally {

‎scripts/test-release-metadata.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ expect(compatibility.some(item => item.node === '26.x'), 'nightly/manual full ma
6565
expect(compatibility.some(item => item.os === 'windows-latest' && item.command === 'test:windows-control-plane'), 'nightly/manual full matrix 必须在 Windows 运行控制面路线')
6666
expect(publicCi.includes('name: Full quality (Node 24.17)'), '公共 CI 全量质量门必须使用发布 Node 24.17')
6767
expect(publicCi.includes('name: Package boundary (Node 24.17)'), '公共 CI package job 必须只声明实际执行的 package boundary')
68+
expect(publicCi.includes('npm run release:dry-run:all -- --allow-existing-version'), '公共 CI package job 必须显式接受精确已发布版本冲突,正式 dry-run 默认仍保持严格')
6869
expect(!publicCi.includes('Website and package'), '公共 CI 不得把条件缺席的网站构建表述为绿色证据')
6970
expect(publicCi.includes('Build ValidationImpactGraphV2 CI plan'), '公共 CI 必须先运行 affected planner')
7071
expect(publicCi.includes('fromJSON(needs.plan.outputs.matrix)'), 'affected job 必须消费 planner matrix')

0 commit comments

Comments
 (0)