From adf7a0d54ce84b13c1c79494556bc12fd2d7f986 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 10:20:53 +0800 Subject: [PATCH 1/2] fix(release): pin the Windows upgrade baseline to a published build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate downloaded `v0.1.9`, whose GitHub Release — like every other `v0.1.x` — no longer exists: the tags remain on `apache/maka`, but the binary releases were removed when the project moved to ASF incubation, where only a voted source release is official. `gh release download` answered `release not found`, so `package` failed on every pull request that touched the Windows release path. Repin to the newest published Windows build, the `v0.2.0-dev.11.20260831` Nightly prerelease, verified by its own SHA-256. That build postdates the removal of bundled Git (#3457), so the reduced baseline artifact contract can no longer require it. The `legacy-required` bundled-Git branch is unreachable — no published build carries Git — and is removed, making the absence rule apply to every artifact this verifier can be pointed at. The contract's remaining relaxations still hold: a baseline may predate resources the candidate must carry, and it carries its own update channel and dependency closure, so it is renamed from `legacy-baseline` to `upgrade-baseline`. Generated-by: Claude Code --- .github/DESKTOP_NIGHTLY.md | 2 +- docs/windows-support.md | 14 ++++++---- scripts/product-release.test.mjs | 10 +++---- scripts/verify-packaged-app.mjs | 28 +++++-------------- scripts/verify-packaged-app.test.mjs | 22 +++++++++------ .../verify-windows-installer-lifecycle.mjs | 2 +- scripts/verify-windows-x64.mjs | 8 ++++-- scripts/windows-upgrade-baseline.json | 8 +++--- 8 files changed, 44 insertions(+), 50 deletions(-) diff --git a/.github/DESKTOP_NIGHTLY.md b/.github/DESKTOP_NIGHTLY.md index 4e0c537dee..854ede602d 100644 --- a/.github/DESKTOP_NIGHTLY.md +++ b/.github/DESKTOP_NIGHTLY.md @@ -36,6 +36,6 @@ Nightly currently uses the same application identity as the formal Desktop. Inst The npm schedule starts at 18:17 UTC. Before changing the npm tag, the workflow requires its run number to exceed the current `nightly` version. Desktop assembles and verifies a draft before one publish mutation; a packaging, attestation, tag, upload, or digest failure leaves no partially published GitHub Release. Never rerun a failed workflow attempt in place; dispatch a fresh npm Nightly with a newer version. -GitHub Release retention is intentionally outside this workflow. Do not delete an old Nightly prerelease or its tag while any installed client may need its payload or blockmap. Disabling `DESKTOP_NIGHTLY_ENABLED` stops new Desktop publication without mutating tags or releases. +GitHub Release retention is intentionally outside this workflow. Do not delete an old Nightly prerelease or its tag while any installed client may need its payload or blockmap. One Nightly is additionally pinned by tag, asset name, and SHA-256 in `scripts/windows-upgrade-baseline.json` as the Windows upgrade gate's baseline: deleting that prerelease fails the gate on every pull request that touches the Windows release path until the pin moves to another published build. Disabling `DESKTOP_NIGHTLY_ENABLED` stops new Desktop publication without mutating tags or releases. Remote Runtime Host setup uses the exact `maka-agent@` package embedded in the Desktop manifest. The npm package is verified before Desktop artifacts become visible, so clean remote setup never depends on an unpublished Runtime Host version. diff --git a/docs/windows-support.md b/docs/windows-support.md index 07865a1436..9a1dc1d1a0 100644 --- a/docs/windows-support.md +++ b/docs/windows-support.md @@ -42,9 +42,12 @@ Only use Windows assets attached to a Maka GitHub Release. The NSIS installer is `winget install BurntSushi.ripgrep.MSVC` if Runtime's `Grep` tool is needed. Restart Maka after changing `PATH`. -The release gate installs a pinned v0.1.9 build, fully smokes it, upgrades the same installation to -the candidate, fully smokes the candidate, waits for installed processes to exit, and runs the real -uninstaller. A second gate proves the automatic, running-app upgrade path: the installed candidate, +The release gate installs the previously published build pinned in +`scripts/windows-upgrade-baseline.json` — by tag, asset name, and SHA-256 — fully smokes it, upgrades +the same installation to the candidate, fully smokes the candidate, waits for installed processes to +exit, and runs the real uninstaller. The baseline is verified against the contract that build shipped +under, not the candidate's: it may predate resources the candidate must carry, and it carries its own +update channel. A second gate proves the automatic, running-app upgrade path: the installed candidate, running, discovers a newer build through its packaged electron-updater against a loopback test feed, downloads it in the background, hands off to the NSIS installer, relaunches as the new version, and passes the full packaged smoke — with the feed requests (including the differential-download probe), @@ -105,8 +108,9 @@ workspace data first; the preview does not yet claim business-data migration gua 5. 启动 Maka,在 **设置 → 模型**中配置模型。需要 Runtime `Grep` 工具时,执行 `winget install BurntSushi.ripgrep.MSVC`,并在 `PATH` 更新后重启 Maka。 -发布门禁会安装固定的 v0.1.9、执行完整 smoke、在同一目录升级候选版本、再次完整 smoke、等待安装目录内 -进程退出,并运行真实卸载器。另一个门禁证明**运行中的自动更新路径**:已安装且正在运行的候选版本通过打包的 +发布门禁会安装 `scripts/windows-upgrade-baseline.json` 中按 tag、资产名与 SHA-256 固定的既往已发布构建, +执行完整 smoke、在同一目录升级候选版本、再次完整 smoke、等待安装目录内进程退出,并运行真实卸载器。基线按 +其发布时所处的契约校验,而非候选版本的契约:它可能不含候选版本必须携带的资源,也带着自己的更新通道。另一个门禁证明**运行中的自动更新路径**:已安装且正在运行的候选版本通过打包的 electron-updater 从 loopback 测试 feed 发现新版本、后台下载、交接给 NSIS 安装器、以新版本自动重启并通过 完整打包 smoke——feed 请求(含差量下载探测)、`downloaded` 状态及其精确版本对、最终安装版本均逐项断言; `checking`/`downloading` 等瞬态不逐项断言。 diff --git a/scripts/product-release.test.mjs b/scripts/product-release.test.mjs index 1c695d2f28..9d20b28c7e 100644 --- a/scripts/product-release.test.mjs +++ b/scripts/product-release.test.mjs @@ -283,15 +283,13 @@ test('a successful Windows upgrade invalidates stale backup authority before bes assert.ok(snapshotRemoval > markerInvalidation); }); -test('platform package verifiers keep Git checks out of current artifacts', async () => { +test('platform package verifiers keep Git checks out of every artifact', async () => { const windowsSource = await readFile(join(repoRoot, 'scripts', 'verify-windows-x64.mjs'), 'utf8'); + assert.doesNotMatch(windowsSource, /bundledGitContract/u); + assert.doesNotMatch(windowsSource, /requirePath\(join\(resources, ['"]git['"]/u); assert.match( windowsSource, - /bundledGitContract: requiresCurrentContract \? ['"]forbidden['"] : ['"]legacy-required['"]/u, - ); - assert.match( - windowsSource, - /if \(requiresCurrentContract\) \{\s*await assertPackagedUpdateConfiguration\(resources, \{\s*channel: environment\.MAKA_DESKTOP_NIGHTLY_VERSION \? ['"]nightly['"] : ['"]release['"],\s*\}\);\s*await assertPackagedDependencyClosure\(resources\);\s*\}\s*else await requirePath\(join\(resources, ['"]git['"]/u, + /if \(requiresCurrentContract\) \{\s*await assertPackagedUpdateConfiguration\(resources, \{\s*channel: environment\.MAKA_DESKTOP_NIGHTLY_VERSION \? ['"]nightly['"] : ['"]release['"],\s*\}\);\s*await assertPackagedDependencyClosure\(resources\);\s*\}/u, ); const macosSource = await readFile( diff --git a/scripts/verify-packaged-app.mjs b/scripts/verify-packaged-app.mjs index bdb84e538b..9936628fb4 100644 --- a/scripts/verify-packaged-app.mjs +++ b/scripts/verify-packaged-app.mjs @@ -1002,11 +1002,6 @@ export async function assertPackagedResources( requirePath, forbidPath = assertMissing, requireWindowsSandbox = process.platform === 'win32', - // Current ASF artifacts must not carry Git. The Windows upgrade lane also - // verifies a previously released installer, whose historical contract did - // require the bundled distribution and its compliance files; keep that - // baseline explicit instead of judging old bytes by today's absence rule. - bundledGitContract = 'forbidden', // The upgrade-lifecycle check runs this against a previously released // build, which predates the disclaimer being packaged. Requiring it there // would fail a release that was correct when it shipped. @@ -1027,22 +1022,9 @@ export async function assertPackagedResources( requireDirectPeerArtifact = true, } = {}, ) { - if (bundledGitContract !== 'forbidden' && bundledGitContract !== 'legacy-required') { - throw new Error(`Unknown bundled Git artifact contract: ${bundledGitContract}`); - } - const requiresLegacyBundledGit = bundledGitContract === 'legacy-required'; const required = [ 'app.asar', 'bundled-tools.json', - ...(requiresLegacyBundledGit - ? [ - 'bundled-git.json', - join('licenses', 'git', 'LICENSE.txt'), - join('licenses', 'git', 'SOURCE_OFFER.txt'), - join('licenses', 'dugite', 'LICENSE'), - join('licenses', 'git', 'NOTICE.txt'), - ] - : []), ...(requireCanonicalIcon ? [join('assets', 'icon.png')] : []), join('workers', 'filesystem-worker.js'), ...(requireDirectPeerArtifact @@ -1087,9 +1069,13 @@ export async function assertPackagedResources( await requirePath(join(resourcesPath, path)); } const forbidden = [ - ...(requiresLegacyBundledGit - ? [] - : ['git', 'bundled-git.json', join('licenses', 'dugite'), join('licenses', 'git')]), + // No packaged artifact carries Git any more, and no baseline that did is + // still published, so the absence rule applies to every build this + // verifier can be pointed at. + 'git', + 'bundled-git.json', + join('licenses', 'dugite'), + join('licenses', 'git'), join('tools', 'officecli'), join('licenses', 'officecli'), // cua-driver is gone from this repository, and these two forbids stay for the diff --git a/scripts/verify-packaged-app.test.mjs b/scripts/verify-packaged-app.test.mjs index 6db7792b70..6918e09e3c 100644 --- a/scripts/verify-packaged-app.test.mjs +++ b/scripts/verify-packaged-app.test.mjs @@ -49,34 +49,38 @@ test('packaged resources forbid the retired bundled Git distribution', async () } }); -test('legacy packaged resources require the historical bundled Git contract', async () => { +test('the upgrade baseline keeps the Git absence rule while relaxing newer resources', async () => { const required = []; const forbidden = []; await assertPackagedResources('resources', { requirePath: async (path) => required.push(path), forbidPath: async (path) => forbidden.push(path), requireWindowsSandbox: false, - bundledGitContract: 'legacy-required', + requireDisclaimer: false, requireCanonicalIcon: false, + requireAppIconCatalog: false, requireDirectPeerArtifact: false, }); + // A pinned baseline may predate any of these; none of them may be demanded + // of bytes that were correct when they shipped. for (const path of [ - join('resources', 'bundled-git.json'), - join('resources', 'licenses', 'dugite', 'LICENSE'), - join('resources', 'licenses', 'git', 'LICENSE.txt'), - join('resources', 'licenses', 'git', 'NOTICE.txt'), - join('resources', 'licenses', 'git', 'SOURCE_OFFER.txt'), + join('resources', 'assets', 'icon.png'), + join('resources', 'licenses', 'maka', 'DISCLAIMER-WIP'), + join('resources', 'runtime-host-peer', 'maka_runtime_host_peer.node'), + join('resources', 'licenses', 'runtime-host-peer', 'THIRD_PARTY_NOTICES.txt'), ]) { - assert.equal(required.includes(path), true); + assert.equal(required.includes(path), false); } + // Git is not one of them: no published build still carries it. for (const path of [ join('resources', 'git'), join('resources', 'bundled-git.json'), join('resources', 'licenses', 'dugite'), join('resources', 'licenses', 'git'), ]) { - assert.equal(forbidden.includes(path), false); + assert.equal(required.includes(path), false); + assert.equal(forbidden.includes(path), true); } }); diff --git a/scripts/verify-windows-installer-lifecycle.mjs b/scripts/verify-windows-installer-lifecycle.mjs index 54b0f5b7d4..54549211b2 100644 --- a/scripts/verify-windows-installer-lifecycle.mjs +++ b/scripts/verify-windows-installer-lifecycle.mjs @@ -424,7 +424,7 @@ export async function verifyWindowsInstallerLifecycle( await verifyApp(installDirectory, { workingDirectory: smokeDirectory, expectedVersion: previousVersion, - artifactContract: 'legacy-baseline', + artifactContract: 'upgrade-baseline', }); console.log('[verify-windows-installer] waiting for previous-version processes to exit'); await waitForProcessesToExit(installDirectory); diff --git a/scripts/verify-windows-x64.mjs b/scripts/verify-windows-x64.mjs index 486c6b3fe5..3b19751c04 100644 --- a/scripts/verify-windows-x64.mjs +++ b/scripts/verify-windows-x64.mjs @@ -129,7 +129,7 @@ export async function verifyPackagedWindowsApp( environment = process.env, } = {}, ) { - if (artifactContract !== 'current' && artifactContract !== 'legacy-baseline') { + if (artifactContract !== 'current' && artifactContract !== 'upgrade-baseline') { throw new Error(`Unknown packaged Windows artifact contract: ${artifactContract}`); } const requiresCurrentContract = artifactContract === 'current'; @@ -146,17 +146,19 @@ export async function verifyPackagedWindowsApp( forbidPath, requireWindowsSandbox: requiresCurrentContract, requireDisclaimer: requiresCurrentContract, - bundledGitContract: requiresCurrentContract ? 'forbidden' : 'legacy-required', requireCanonicalIcon: requiresCurrentContract, requireAppIconCatalog: requiresCurrentContract, requireDirectPeerArtifact: requiresCurrentContract, }); + // The upgrade baseline is a build that shipped on its own channel, from its + // own commit: its update feed and dependency closure are the ones that were + // right for it, not the ones this checkout expects. if (requiresCurrentContract) { await assertPackagedUpdateConfiguration(resources, { channel: environment.MAKA_DESKTOP_NIGHTLY_VERSION ? 'nightly' : 'release', }); await assertPackagedDependencyClosure(resources); - } else await requirePath(join(resources, 'git', 'cmd', 'git.exe')); + } step('reading the executable architecture'); const machine = await readMachine(executable); diff --git a/scripts/windows-upgrade-baseline.json b/scripts/windows-upgrade-baseline.json index c3f19aed4e..442ec14745 100644 --- a/scripts/windows-upgrade-baseline.json +++ b/scripts/windows-upgrade-baseline.json @@ -1,6 +1,6 @@ { - "version": "0.1.9", - "tag": "v0.1.9", - "assetName": "Maka-0.1.9-win-x64.exe", - "sha256": "ebda293ab835ec8434df2f5bbeea21bb3ba9c82bc8348ab8ee8d4915e4b6fd4b" + "version": "0.2.0-dev.11.20260831", + "tag": "v0.2.0-dev.11.20260831", + "assetName": "Maka-0.2.0-dev.11.20260831-win-x64.exe", + "sha256": "0c5362707776af9a6146b55e3284dc0a2389b3329e2bed94cfc386b0eac86709" } From a162216ce1873b950119f105f47ef229c87648de Mon Sep 17 00:00:00 2001 From: AstroHan Date: Tue, 1 Sep 2026 10:30:25 +0800 Subject: [PATCH 2/2] fix(ci): derive the planner's gate coverage instead of restating it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RELEASE_CONTRACT_FILES` named a `prepare-windows-upgrade-baseline.test.mjs` that has never existed — added speculatively in #3222 — and `STORAGE_STRESS_FILES` still named `agent-run-store.test.ts`, deleted by #1994 a month ago. A path that cannot match is dead weight nothing reports. In the other direction, three of the tests `check:release` runs reached no lane that runs it — `verify-packaged-app.test.mjs`, `third-party-closure.test.mjs`, and `qualify-released-cli-state-root.test.mjs` — so each could be edited green. `source-legal-inventory.test.mjs` and `sync-model-metadata.test.mjs` reach the ASF source lane, which runs them too, and stay as they are. Both directions are now derived rather than restated: one test walks the gate scripts in `package.json` and requires every test they name to select a lane that runs it, and one requires every path the planner names to still exist. Generated-by: Claude Code --- scripts/ci-test-plan.mjs | 5 ++-- scripts/ci-test-plan.test.mjs | 46 ++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index 3b19f8616f..3ac08342c2 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -56,10 +56,11 @@ const RELEASE_CONTRACT_FILES = new Set([ 'scripts/package-windows-x64.mjs', 'scripts/prepare-windows-upgrade-baseline.mjs', 'scripts/generate-third-party-notices.test.mjs', - 'scripts/prepare-windows-upgrade-baseline.test.mjs', 'scripts/product-release.test.mjs', + 'scripts/qualify-released-cli-state-root.test.mjs', 'scripts/release-eval-smoke-sitecustomize.py', 'scripts/release-version.mjs', + 'scripts/third-party-closure.test.mjs', 'scripts/verify-macos-arm64-cli.mjs', 'scripts/verify-macos-arm64-dmg.mjs', 'scripts/verify-macos-autoupdate.mjs', @@ -67,6 +68,7 @@ const RELEASE_CONTRACT_FILES = new Set([ 'scripts/product-nightly.mjs', 'scripts/product-nightly.test.mjs', 'scripts/verify-packaged-app.mjs', + 'scripts/verify-packaged-app.test.mjs', 'scripts/verify-windows-autoupdate.mjs', 'scripts/verify-windows-installer-lifecycle.mjs', 'scripts/verify-windows-x64.mjs', @@ -258,7 +260,6 @@ const STORAGE_STRESS_FILES = new Set([ 'packages/storage/src/sqlite-session-metadata-schema.ts', 'packages/storage/src/sqlite-usage-schema.ts', 'packages/storage/src/sqlite-workflow-schema.ts', - 'packages/storage/src/__tests__/agent-run-store.test.ts', 'packages/storage/src/__tests__/root-authority.test.ts', 'packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts', 'packages/storage/src/__tests__/fixtures/sqlite-recovery-concurrency-child.ts', diff --git a/scripts/ci-test-plan.test.mjs b/scripts/ci-test-plan.test.mjs index bc1a23c938..54cf9c5cf7 100644 --- a/scripts/ci-test-plan.test.mjs +++ b/scripts/ci-test-plan.test.mjs @@ -18,7 +18,7 @@ */ import assert from 'node:assert/strict'; -import { readdirSync, readFileSync } from 'node:fs'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; import test from 'node:test'; import { formatGitHubOutputs, loadWorkspaceGraph, planTests } from './ci-test-plan.mjs'; @@ -167,6 +167,50 @@ test('release authority changes select their dedicated contract gate', () => { assert.equal(planTests(['.github/RELEASE_CHECKLIST.md'], { graph }).releaseContract, false); }); +// Derived from the gate scripts themselves, because the sets above are hand +// maintained and drift silently in both directions: a test the gate runs but +// no lane selects can be edited green, and a listed path that no longer exists +// is dead weight nothing reports. Both had happened — three of the release +// gate's own tests reached no lane, and the set named a +// `prepare-windows-upgrade-baseline.test.mjs` that never existed. +test('every test a gate script runs reaches a lane that runs that gate', () => { + const { scripts } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + const lanesByTest = new Map(); + for (const [script, lane] of [ + ['check:release', 'releaseContract'], + ['check:asf-source', 'asfSource'], + ]) { + for (const file of scripts[script].match(/scripts\/[\w.-]+\.test\.mjs/gu) ?? []) { + lanesByTest.set(file, (lanesByTest.get(file) ?? new Set()).add(lane)); + } + } + assert.ok(lanesByTest.size > 0, 'no gate script names a test file'); + + // A test two gates share needs only one of them: either run executes it. + for (const [file, lanes] of lanesByTest) { + const plan = planTests([file], { graph }); + assert.ok( + [...lanes].some((lane) => plan[lane]), + `${file} reaches no ${[...lanes].join('/')}`, + ); + } +}); + +// The other direction of the same drift. Every literal path in the planner is +// matched against a changed file, so one that no longer exists can never match +// and nothing reports it: the phantom baseline test sat here for a month, and +// `agent-run-store.test.ts` stayed in the storage stress set for a month after +// #1994 deleted it. +test('the planner names no path that no longer exists', () => { + const source = readFileSync(new URL('ci-test-plan.mjs', import.meta.url), 'utf8'); + const paths = [...source.matchAll(/^ {2}'([\w.-]+(?:\/[\w.-]+)+)',$/gmu)].map(([, path]) => path); + assert.ok(paths.length > 0, 'the planner names no paths'); + + for (const path of paths) { + assert.ok(existsSync(new URL(`../${path}`, import.meta.url)), path); + } +}); + test('Product Nightly authority changes select the release contract gate', () => { for (const path of [ '.github/workflows/desktop-nightly.yml',