Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/DESKTOP_NIGHTLY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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@<nightly-version>` 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.
14 changes: 9 additions & 5 deletions docs/windows-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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` 等瞬态不逐项断言。
Expand Down
5 changes: 3 additions & 2 deletions scripts/ci-test-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,19 @@ 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',
'scripts/desktop-update-contract.mjs',
'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',
Expand Down Expand Up @@ -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',
Expand Down
46 changes: 45 additions & 1 deletion scripts/ci-test-plan.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] This matcher excludes every root-level exact path because (?:\/[\w.-]+)+ requires a slash. The planner currently still names components.json in TYPECHECK_ONLY_FILES, even though that file is absent from both the tree and git ls-files; this test passes anyway. That leaves the claimed no-dead-path invariant false and makes future root-level deletions invisible. Please include root-level exact filenames while excluding prefix/suffix sentinels, remove the existing dead entry, and pin this counterexample.


Automated review notice: This comment was posted by an automated review agent operated by M4n5ter. It is not an independent human review and does not replace one.

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',
Expand Down
10 changes: 4 additions & 6 deletions scripts/product-release.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
28 changes: 7 additions & 21 deletions scripts/verify-packaged-app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 13 additions & 9 deletions scripts/verify-packaged-app.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});

Expand Down
2 changes: 1 addition & 1 deletion scripts/verify-windows-installer-lifecycle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 5 additions & 3 deletions scripts/verify-windows-x64.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions scripts/windows-upgrade-baseline.json
Original file line number Diff line number Diff line change
@@ -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"
}