From 0d3f2da42264fb5e005c37d21cc55c364851e2d5 Mon Sep 17 00:00:00 2001 From: Hsin <2129830748@qq.com> Date: Wed, 2 Sep 2026 12:46:04 +0800 Subject: [PATCH 1/4] fix(release): bind Desktop and CLI Runtime Host identities Carry the exact source commit and compatibility epoch through Desktop, npm, and macOS CLI artifacts, then validate packaged hosts and cross-artifact handshakes. Document the supported Client/Host matrix for issue #3973. Generated-by: Codex --- .github/workflows/cli-package-validation.yml | 6 + .github/workflows/desktop-nightly.yml | 1 + .github/workflows/release.yml | 4 + apps/desktop/electron-builder.config.mjs | 34 ++- docs/cli-npm-release.md | 18 ++ docs/cli-npm-release.zh-CN.md | 16 ++ package.json | 2 +- scripts/package-macos-arm64-cli.mjs | 22 +- scripts/product-release.test.mjs | 76 ++++++- scripts/release-cli-compatibility.mjs | 89 ++++++++ scripts/release-cli-compatibility.test.mjs | 134 ++++++++++++ scripts/release-cli-package.mjs | 35 ++- scripts/smoke-release-cli-package.mjs | 215 ++++++++++++++++++- scripts/verify-macos-arm64-cli.mjs | 32 ++- scripts/verify-macos-arm64-dmg.mjs | 10 + scripts/verify-packaged-app.mjs | 30 ++- scripts/verify-windows-x64.mjs | 18 +- 17 files changed, 722 insertions(+), 20 deletions(-) create mode 100644 scripts/release-cli-compatibility.mjs create mode 100644 scripts/release-cli-compatibility.test.mjs diff --git a/.github/workflows/cli-package-validation.yml b/.github/workflows/cli-package-validation.yml index efd62c0b0b..003c0477d9 100644 --- a/.github/workflows/cli-package-validation.yml +++ b/.github/workflows/cli-package-validation.yml @@ -32,6 +32,7 @@ on: - 'packages/cli/src/runtime-host-peer-*' - 'packages/cli/src/runtime-host-service-*' - 'packages/runtime-host/package.json' + - 'packages/runtime-host/src/protocol/**' - 'packages/runtime-host/src/client/peer-client.ts' - 'packages/runtime-host/src/peer-mesh/**' - 'packages/runtime-host/src/server/peer-listener.ts' @@ -40,6 +41,8 @@ on: - 'packages/storage/src/file-lifetime-owner.ts' - 'packages/storage/src/native-file-lock.ts' - 'scripts/generate-runtime-host-peer-*' + - 'scripts/release-cli-compatibility.mjs' + - 'scripts/release-cli-compatibility.test.mjs' - 'scripts/release-cli-package.mjs' - 'scripts/qualify-released-cli-state-root.mjs' - 'scripts/qualify-released-cli-state-root.test.mjs' @@ -232,6 +235,7 @@ jobs: - name: Build the release tarball once env: MAKA_CLI_NIGHTLY_VERSION: ${{ inputs.package_version }} + MAKA_RELEASE_SOURCE_COMMIT: ${{ inputs.source_commit || github.sha }} MAKA_RUNTIME_HOST_PEER_PREBUILDS: ${{ runner.temp }}/runtime-host-peer-prebuilds run: npm run release:cli:pack - name: Upload the immutable release candidate @@ -303,6 +307,8 @@ jobs: artifact-ids: ${{ needs.build.outputs.release_candidate_artifact_id }} path: packages/cli/release - name: Validate the installed tarball + env: + MAKA_RELEASE_SOURCE_COMMIT: ${{ inputs.source_commit || github.sha }} run: node scripts/smoke-release-cli-package.mjs - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 if: matrix.second_node != '' diff --git a/.github/workflows/desktop-nightly.yml b/.github/workflows/desktop-nightly.yml index fb03a3b485..346e146919 100644 --- a/.github/workflows/desktop-nightly.yml +++ b/.github/workflows/desktop-nightly.yml @@ -98,6 +98,7 @@ jobs: shell: bash env: MAKA_DESKTOP_NIGHTLY_VERSION: ${{ needs.identity.outputs.version }} + MAKA_RELEASE_SOURCE_COMMIT: ${{ needs.identity.outputs.source_commit }} steps: - name: Reject in-place workflow reruns if: github.run_attempt != 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 565f571f88..3a1f2c1844 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -101,6 +101,8 @@ jobs: runner: windows-2025 runs-on: ${{ matrix.runner }} environment: release + env: + MAKA_RELEASE_SOURCE_COMMIT: ${{ needs.release-identity.outputs.source_commit }} timeout-minutes: 75 defaults: run: @@ -268,6 +270,8 @@ jobs: needs: release-identity runs-on: macos-15 environment: release + env: + MAKA_RELEASE_SOURCE_COMMIT: ${{ needs.release-identity.outputs.source_commit }} timeout-minutes: 45 defaults: run: diff --git a/apps/desktop/electron-builder.config.mjs b/apps/desktop/electron-builder.config.mjs index 652e0cd08d..3c82ed38c9 100644 --- a/apps/desktop/electron-builder.config.mjs +++ b/apps/desktop/electron-builder.config.mjs @@ -17,15 +17,20 @@ * under the License. */ +import { execFileSync } from 'node:child_process'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { resolveDesktopBuildVersion, resolveRuntimeHostSetupPackage, } from '../../scripts/desktop-nightly.mjs'; import { workspaceReleaseManifest } from '../../scripts/release-cli-file-policy.mjs'; import { resolveProductManifestIdentity } from '../../scripts/product-release-identity.mjs'; +import { resolveMakaReleaseIdentity } from '../../scripts/release-cli-compatibility.mjs'; + +const repoRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); function readManifest(relativePath) { return JSON.parse(readFileSync(new URL(relativePath, import.meta.url), 'utf8')); @@ -61,11 +66,16 @@ async function stageReleaseManifests({ packager }) { } const rootManifest = readManifest('../../package.json'); -const { runtimeHostSetupPackage } = resolveProductManifestIdentity({ +const { runtimeHostSetupPackage, version: productVersion } = resolveProductManifestIdentity({ rootManifest, desktopManifest: readManifest('./package.json'), cliManifest: readManifest('../../packages/cli/package.json'), }); +const makaReleaseIdentity = resolveMakaReleaseIdentity({ + version: productVersion, + sourceCommit: resolvePackagingSourceCommit(), + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), +}); const baseDesktopBuilderConfig = { appId: 'com.maka.desktop', @@ -73,7 +83,11 @@ const baseDesktopBuilderConfig = { artifactName: 'Maka-${version}-mac-${arch}.${ext}', asar: true, beforePack: stageReleaseManifests, - extraMetadata: { runtimeHostSetupPackage, makaUpdateChannel: 'release' }, + extraMetadata: { + runtimeHostSetupPackage, + makaReleaseIdentity, + makaUpdateChannel: 'release', + }, directories: { output: 'release', }, @@ -299,10 +313,26 @@ export function resolveDesktopBuilderConfig(environment = process.env) { ...baseDesktopBuilderConfig.extraMetadata, version, runtimeHostSetupPackage: resolveRuntimeHostSetupPackage(rootManifest.version, environment), + makaReleaseIdentity: { ...makaReleaseIdentity, version }, makaUpdateChannel: 'nightly', }, publish: [{ provider: 'github', owner: 'apache', repo: 'maka', channel: 'dev' }], }; } +function resolvePackagingSourceCommit(environment = process.env) { + const configured = environment.MAKA_RELEASE_SOURCE_COMMIT?.trim(); + if (configured) return configured; + try { + return execFileSync('git', ['rev-parse', 'HEAD'], { + cwd: repoRoot, + encoding: 'utf8', + }).trim(); + } catch (error) { + const githubSha = environment.GITHUB_SHA?.trim(); + if (githubSha) return githubSha; + throw new Error('Desktop packaging requires an exact source commit SHA', { cause: error }); + } +} + export default resolveDesktopBuilderConfig(); diff --git a/docs/cli-npm-release.md b/docs/cli-npm-release.md index 0b5fc3f60d..ad4004a805 100644 --- a/docs/cli-npm-release.md +++ b/docs/cli-npm-release.md @@ -49,6 +49,24 @@ retaining Maka's stronger protected-Environment, staged-publishing, 2FA, and Fin - Do not rebuild between validation, staging, approval, and finalization. - Never reuse a public version. Formal product fixes require a new patch, minor, or major version. +## Runtime Host compatibility matrix + +Desktop and npm CLI release artifacts carry the same `makaReleaseIdentity`: schema version, +product version, exact source commit, and Runtime Host compatibility epoch. Release validation +requires this identity to match across artifacts. The network handshake then requires an exact +epoch match before admitting any Domain command. + +| Client artifact | Runtime Host artifact | Result | +| --- | --- | --- | +| Desktop/CLI from the same product source identity | Runtime Host from that same identity | Supported | +| Any client | Runtime Host with a different compatibility epoch | Rejected as `incompatible` during handshake | +| Desktop `0.1.11` (epoch 25) | `maka-agent@0.1.0-beta.1` (epoch 24) | Unsupported; upgrade to a newly published matching release | + +The source commit and version are release metadata, not additional wire-handshake fields; equal +epochs alone do not make independently built artifacts a supported release pair. Because public +npm versions are immutable, do not overwrite `0.1.0-beta.1`; publish a new version from one exact +approved source commit and update the remote Host package. + The workflow boundaries are: 1. [npm publication](../.github/workflows/npm-publication.yml) is the only Trusted Publisher caller. diff --git a/docs/cli-npm-release.zh-CN.md b/docs/cli-npm-release.zh-CN.md index 989a59f04d..3c8a78b014 100644 --- a/docs/cli-npm-release.zh-CN.md +++ b/docs/cli-npm-release.zh-CN.md @@ -43,6 +43,22 @@ source RC 阶段的 [npm 预检](../.github/ASF_NPM_RELEASE.md) 是更早执行 - validation、staging、approval 和 finalization 之间不得重新构建; - 已公开的版本不得复用。正式产品修复必须使用新的 patch、minor 或 major 版本。 +## Runtime Host 兼容性矩阵 + +Desktop 与 npm CLI 发布物携带同一个 `makaReleaseIdentity`:schema version、产品版本、精确 +source commit 以及 Runtime Host compatibility epoch。发布校验要求这些身份在各 artifact +之间一致;网络握手随后要求 epoch 精确相等,并在接纳任何 Domain command 前完成检查。 + +| Client artifact | Runtime Host artifact | 结果 | +| --- | --- | --- | +| 来自同一产品 source identity 的 Desktop/CLI | 来自同一 identity 的 Runtime Host | 支持 | +| 任意 Client | compatibility epoch 不同的 Runtime Host | 握手阶段返回 `incompatible` 并拒绝 | +| Desktop `0.1.11`(epoch 25) | `maka-agent@0.1.0-beta.1`(epoch 24) | 不支持;升级到新发布的匹配版本 | + +source commit 和版本属于发布元数据,不是额外的 wire-handshake 字段;仅 epoch 相等并不使 +独立构建的 artifact 自动成为受支持的版本组合。由于公开 npm 版本不可覆盖,不要改写 +`0.1.0-beta.1`;应从一个精确批准的 source commit 发布新版本,并更新远程 Host 包。 + workflow 边界分别是: 1. [npm publication](../.github/workflows/npm-publication.yml) 是唯一的 Trusted Publisher diff --git a/package.json b/package.json index 9a845842a5..41e6a3b51a 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "check:runtime-host-peer-dependencies": "node scripts/generate-runtime-host-peer-dependencies.mjs --check", "generate:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs", "check:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs --check", - "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-nightly.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", + "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-nightly.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-compatibility.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", "package:macos-arm64": "node scripts/package-macos-arm64.mjs", "verify:macos-arm64": "node scripts/verify-macos-arm64-dmg.mjs", "package:macos-autoupdate-next": "node scripts/package-macos-autoupdate-next.mjs", diff --git a/scripts/package-macos-arm64-cli.mjs b/scripts/package-macos-arm64-cli.mjs index dde3561b17..ef733896b0 100644 --- a/scripts/package-macos-arm64-cli.mjs +++ b/scripts/package-macos-arm64-cli.mjs @@ -40,6 +40,10 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'nod import { fileURLToPath, pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; import { resolveProductReleaseIdentity } from './product-release-identity.mjs'; +import { + assertRuntimeHostCompatibilityEpoch, + resolveMakaReleaseIdentity, +} from './release-cli-compatibility.mjs'; import { isMakaDevelopmentArtifact, isThirdPartyDevelopmentArtifact, @@ -862,15 +866,21 @@ export async function packageMacosArm64Cli({ readFile(join(repoRoot, 'apps', 'desktop', 'package.json'), 'utf8').then(JSON.parse), readFile(join(repoRoot, 'packages', 'cli', 'package.json'), 'utf8').then(JSON.parse), resolveCliWorkspacePackages(), - inspect('git', ['rev-parse', 'HEAD']), + env.MAKA_RELEASE_SOURCE_COMMIT?.trim() || inspect('git', ['rev-parse', 'HEAD']), ]); - const sourceCommit = sourceCommitResult.stdout.trim(); + const sourceCommit = + typeof sourceCommitResult === 'string' ? sourceCommitResult : sourceCommitResult.stdout.trim(); const identity = resolveProductReleaseIdentity({ rootManifest, desktopManifest, cliManifest, sha: sourceCommit, }); + const runtimeHostReleaseIdentity = resolveMakaReleaseIdentity({ + version: identity.version, + sourceCommit, + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + }); if (releaseSigning) assertReleaseSigningEnvironment(env); if (!nodeArchivePath) { throw new Error(`Set MAKA_CLI_NODE_ARCHIVE to the verified ${identity.nodeArchive} path.`); @@ -961,6 +971,13 @@ export async function packageMacosArm64Cli({ chmod(join(binDirectory, 'maka'), 0o755), ]); await assertWorkspaceLinks(archiveRoot, workspacePackages); + assertRuntimeHostCompatibilityEpoch({ + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + packagedPath: join( + archiveRoot, + 'libexec/node_modules/@maka/runtime-host/dist/protocol/index.js', + ), + }); const thirdPartyNoticesPath = join(archiveRoot, 'THIRD_PARTY_NOTICES.txt'); await copyFile( @@ -976,6 +993,7 @@ export async function packageMacosArm64Cli({ product: 'Maka', version, sourceCommit, + makaReleaseIdentity: runtimeHostReleaseIdentity, platform: 'macos', architecture: 'arm64', publicCommands: identity.publicCommands, diff --git a/scripts/product-release.test.mjs b/scripts/product-release.test.mjs index 9d20b28c7e..763179cfce 100644 --- a/scripts/product-release.test.mjs +++ b/scripts/product-release.test.mjs @@ -18,7 +18,7 @@ */ import assert from 'node:assert/strict'; -import { execFile } from 'node:child_process'; +import { execFile, execFileSync } from 'node:child_process'; import { access, chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { isAbsolute, join } from 'node:path'; @@ -50,6 +50,7 @@ import { standaloneInstallRootManifest, } from './package-macos-arm64-cli.mjs'; import { resolveWorkspaceReleaseFiles } from './release-cli-file-policy.mjs'; +import { readRuntimeHostCompatibilityEpoch } from './release-cli-compatibility.mjs'; import { isTuiReadyOutput, verifyQuarantinedExecution } from './verify-macos-arm64-cli.mjs'; import { makePtyProbe } from './verify-packaged-app.mjs'; import { ensureProductTag } from './product-release-tag.mjs'; @@ -168,6 +169,15 @@ test('Desktop packaging derives the Runtime Host setup package from product mani const checkedRootManifest = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8')); assert.deepEqual(desktopBuilderConfig.extraMetadata, { runtimeHostSetupPackage: `maka-agent@${checkedRootManifest.version}`, + makaReleaseIdentity: { + schemaVersion: 1, + product: 'Maka', + version: checkedRootManifest.version, + sourceCommit: execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(), + compatibilityEpoch: readRuntimeHostCompatibilityEpoch( + join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + ), + }, makaUpdateChannel: 'release', }); assert.deepEqual(desktopBuilderConfig.publish, [ @@ -304,30 +314,80 @@ test('the packaged-app probe rejects a mismatched Runtime Host setup package', a try { const manifestPath = join(fixture, 'package.json'); const ptyDirectory = join(fixture, 'node_modules', 'node-pty'); + const runtimeHostDirectory = join(fixture, 'node_modules', '@maka', 'runtime-host'); + const releaseIdentity = { + schemaVersion: 1, + product: 'Maka', + version: '1.2.3', + sourceCommit: 'a'.repeat(40), + compatibilityEpoch: 72, + }; await mkdir(ptyDirectory, { recursive: true }); + await mkdir(join(runtimeHostDirectory, 'dist', 'protocol'), { recursive: true }); await Promise.all([ - writeFile(manifestPath, JSON.stringify({ runtimeHostSetupPackage: 'maka-agent@1.2.3' })), + writeFile( + manifestPath, + JSON.stringify({ + runtimeHostSetupPackage: 'maka-agent@1.2.3', + makaReleaseIdentity: releaseIdentity, + }), + ), writeFile( join(ptyDirectory, 'index.js'), `module.exports = { spawn() { return { onData(listener) { queueMicrotask(() => listener('maka-node-pty-ok')); }, onExit(listener) { setImmediate(() => listener({ exitCode: 0 })); }, -}; } };\n`, + }; } };\n`, + ), + writeFile( + join(runtimeHostDirectory, 'package.json'), + JSON.stringify({ + name: '@maka/runtime-host', + type: 'module', + exports: { './protocol': './dist/protocol/index.js' }, + }), + ), + writeFile( + join(runtimeHostDirectory, 'dist', 'protocol', 'index.js'), + 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 72;\n', ), ]); await execFileAsync(process.execPath, [ '-e', - makePtyProbe('/bin/echo', ['maka-node-pty-ok'], 'maka-agent@1.2.3'), + makePtyProbe('/bin/echo', ['maka-node-pty-ok'], 'maka-agent@1.2.3', releaseIdentity), manifestPath, ]); await assert.rejects( execFileAsync(process.execPath, [ '-e', - makePtyProbe('/bin/echo', ['maka-node-pty-ok'], 'maka-agent@1.2.4'), + makePtyProbe('/bin/echo', ['maka-node-pty-ok'], 'maka-agent@1.2.4', releaseIdentity), manifestPath, ]), /Packaged Runtime Host setup package mismatch/u, ); + await assert.rejects( + execFileAsync(process.execPath, [ + '-e', + makePtyProbe('/bin/echo', ['maka-node-pty-ok'], 'maka-agent@1.2.3', { + ...releaseIdentity, + sourceCommit: 'b'.repeat(40), + }), + manifestPath, + ]), + /Packaged Maka release identity mismatch/u, + ); + await writeFile( + join(runtimeHostDirectory, 'dist', 'protocol', 'index.js'), + 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 71;\n', + ); + await assert.rejects( + execFileAsync(process.execPath, [ + '-e', + makePtyProbe('/bin/echo', ['maka-node-pty-ok'], 'maka-agent@1.2.3', releaseIdentity), + manifestPath, + ]), + /Packaged Runtime Host compatibility epoch mismatch/u, + ); } finally { await rm(fixture, { recursive: true, force: true }); } @@ -715,6 +775,12 @@ test('one product workflow gates one draft release on every required artifact', ); assert.equal(checkout.with.ref, '${{ needs.release-identity.outputs.source_commit }}'); } + for (const name of ['desktop', 'cli-macos-arm64']) { + assert.equal( + jobs[name].env.MAKA_RELEASE_SOURCE_COMMIT, + '${{ needs.release-identity.outputs.source_commit }}', + ); + } const desktopStepNames = jobs.desktop.steps.map((step) => step.name); const uploadIndex = desktopStepNames.indexOf('Upload the verified release assets'); diff --git a/scripts/release-cli-compatibility.mjs b/scripts/release-cli-compatibility.mjs new file mode 100644 index 0000000000..5ac86df988 --- /dev/null +++ b/scripts/release-cli-compatibility.mjs @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { readFileSync } from 'node:fs'; + +const EPOCH_DECLARATION = + /^export const RUNTIME_HOST_COMPATIBILITY_EPOCH\s*=\s*(\d+)(?:\s+as const)?\s*;/gmu; +const RELEASE_IDENTITY_KEYS = [ + 'schemaVersion', + 'product', + 'version', + 'sourceCommit', + 'compatibilityEpoch', +]; + +export function resolveMakaReleaseIdentity({ version, sourceCommit, sourcePath }) { + if (typeof version !== 'string' || version.trim() === '') { + throw new Error('Maka release identity requires a non-empty version'); + } + if (typeof sourceCommit !== 'string' || !/^[0-9a-f]{40}$/u.test(sourceCommit)) { + throw new Error('Maka release identity requires an exact 40-character source commit SHA'); + } + return { + schemaVersion: 1, + product: 'Maka', + version, + sourceCommit, + compatibilityEpoch: readRuntimeHostCompatibilityEpoch(sourcePath), + }; +} + +export function assertMakaReleaseIdentity({ expected, actual, label = 'Maka release identity' }) { + if (!actual || typeof actual !== 'object' || Array.isArray(actual)) { + throw new Error(`${label} is missing`); + } + const actualKeys = Object.keys(actual).sort(); + const expectedKeys = [...RELEASE_IDENTITY_KEYS].sort(); + if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) { + throw new Error(`${label} has an unexpected shape`); + } + for (const key of RELEASE_IDENTITY_KEYS) { + if (actual[key] !== expected[key]) { + throw new Error( + `${label} mismatch for ${key}: expected ${JSON.stringify(expected[key])}, found ${JSON.stringify(actual[key])}`, + ); + } + } + return actual; +} + +export function readRuntimeHostCompatibilityEpoch(filePath) { + const content = readFileSync(filePath, 'utf8'); + const matches = [...content.matchAll(EPOCH_DECLARATION)]; + if (matches.length !== 1) { + throw new Error(`Runtime Host compatibility epoch declaration is not unique in ${filePath}`); + } + const epoch = Number(matches[0][1]); + if (!Number.isSafeInteger(epoch) || epoch < 1) { + throw new Error(`Runtime Host compatibility epoch is invalid in ${filePath}`); + } + return epoch; +} + +export function assertRuntimeHostCompatibilityEpoch({ sourcePath, packagedPath }) { + const sourceEpoch = readRuntimeHostCompatibilityEpoch(sourcePath); + const packagedEpoch = readRuntimeHostCompatibilityEpoch(packagedPath); + if (sourceEpoch !== packagedEpoch) { + throw new Error( + `Runtime Host compatibility epoch mismatch: source ${sourceEpoch} (${sourcePath}) vs packaged ${packagedEpoch} (${packagedPath})`, + ); + } + return { sourceEpoch, packagedEpoch }; +} diff --git a/scripts/release-cli-compatibility.test.mjs b/scripts/release-cli-compatibility.test.mjs new file mode 100644 index 0000000000..687abe02e0 --- /dev/null +++ b/scripts/release-cli-compatibility.test.mjs @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { + assertMakaReleaseIdentity, + assertRuntimeHostCompatibilityEpoch, + readRuntimeHostCompatibilityEpoch, + resolveMakaReleaseIdentity, +} from './release-cli-compatibility.mjs'; + +const sourceCommit = 'a'.repeat(40); + +test('accepts matching Runtime Host compatibility epochs in TypeScript and JavaScript', () => { + const root = mkdtempSync(join(tmpdir(), 'maka-release-compatibility-')); + try { + const sourcePath = join(root, 'source.ts'); + const packagedPath = join(root, 'packaged.js'); + writeFileSync(sourcePath, 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 25 as const;\n'); + writeFileSync(packagedPath, 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 25;\n'); + + assert.equal(readRuntimeHostCompatibilityEpoch(sourcePath), 25); + assert.deepEqual(assertRuntimeHostCompatibilityEpoch({ sourcePath, packagedPath }), { + sourceEpoch: 25, + packagedEpoch: 25, + }); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('rejects a packaged Runtime Host compatibility epoch from an older release', () => { + const root = mkdtempSync(join(tmpdir(), 'maka-release-compatibility-')); + try { + const sourcePath = join(root, 'source.ts'); + const packagedPath = join(root, 'packaged.js'); + writeFileSync(sourcePath, 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 25 as const;\n'); + writeFileSync(packagedPath, 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 24;\n'); + + assert.throws( + () => assertRuntimeHostCompatibilityEpoch({ sourcePath, packagedPath }), + (error) => { + assert.match(error.message, /compatibility epoch mismatch/u); + assert.match(error.message, /source 25/u); + assert.match(error.message, /packaged 24/u); + return true; + }, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('rejects missing or duplicate Runtime Host compatibility epoch declarations', () => { + const root = mkdtempSync(join(tmpdir(), 'maka-release-compatibility-')); + try { + const missingPath = join(root, 'missing.js'); + const duplicatePath = join(root, 'duplicate.js'); + writeFileSync(missingPath, 'export const RUNTIME_HOST_PROTOCOL_VERSION = 0;\n'); + writeFileSync( + duplicatePath, + [ + 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 25;', + 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 26;', + ].join('\n'), + ); + + assert.throws(() => readRuntimeHostCompatibilityEpoch(missingPath), /not unique/u); + assert.throws(() => readRuntimeHostCompatibilityEpoch(duplicatePath), /not unique/u); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('requires Desktop and CLI release artifacts to carry the same source identity', () => { + const root = mkdtempSync(join(tmpdir(), 'maka-release-identity-')); + try { + const sourcePath = join(root, 'source.ts'); + writeFileSync(sourcePath, 'export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 25 as const;\n'); + const expected = resolveMakaReleaseIdentity({ + version: '0.2.0', + sourceCommit, + sourcePath, + }); + + assertMakaReleaseIdentity({ expected, actual: { ...expected } }); + assert.throws( + () => + assertMakaReleaseIdentity({ + expected, + actual: { ...expected, sourceCommit: 'b'.repeat(40) }, + }), + /mismatch for sourceCommit/u, + ); + assert.throws( + () => + assertMakaReleaseIdentity({ + expected, + actual: { ...expected, compatibilityEpoch: 24 }, + }), + /mismatch for compatibilityEpoch/u, + ); + assert.throws( + () => + assertMakaReleaseIdentity({ + expected, + actual: { ...expected, unexpected: true }, + }), + /unexpected shape/u, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/scripts/release-cli-package.mjs b/scripts/release-cli-package.mjs index c3976e694c..e4360cc18b 100644 --- a/scripts/release-cli-package.mjs +++ b/scripts/release-cli-package.mjs @@ -38,6 +38,11 @@ import { tmpdir } from 'node:os'; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; import { npmSpawnOptions } from './npm-spawn.mjs'; import { validateCliReleaseArtifactMetrics } from './release-cli-artifact-policy.mjs'; +import { + assertMakaReleaseIdentity, + assertRuntimeHostCompatibilityEpoch, + resolveMakaReleaseIdentity, +} from './release-cli-compatibility.mjs'; import { assertProductNightlyVersion } from './release-version.mjs'; import { isCurrentDevelopmentJavaScript, @@ -638,9 +643,17 @@ function writeReleaseManifest(cli, publishable) { 'CLI manifest must define a positive managed Runtime Host update compatibility', ); } + const version = publishable && nightlyVersion ? nightlyVersion : source.version; + const releaseIdentity = publishable + ? resolveMakaReleaseIdentity({ + version, + sourceCommit: process.env.MAKA_RELEASE_SOURCE_COMMIT?.trim(), + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + }) + : undefined; const manifest = { name: source.name, - version: publishable && nightlyVersion ? nightlyVersion : source.version, + version, description: nightlyVersion ? 'Apache Maka (Incubating) developer snapshot; not an Apache release.' : 'Apache Maka (Incubating), a local-first agent workspace for the terminal.', @@ -649,6 +662,7 @@ function writeReleaseManifest(cli, publishable) { exports: {}, bin: source.bin, maka: { managedRuntimeHostUpdateCompatibility: updateCompatibility }, + ...(releaseIdentity ? { makaReleaseIdentity: releaseIdentity } : {}), engines: root.engines, repository: { type: 'git', @@ -716,6 +730,7 @@ function validateStaging(publishable) { 'RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv', 'RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt', 'node_modules/@maka/runtime/dist/workers/filesystem-worker.js', + 'node_modules/@maka/runtime-host/dist/protocol/index.js', 'node_modules/@maka/runtime-host/dist/execution-candidate-main.js', 'node_modules/@maka/eval/dist/harbor-external-subject.js', 'node_modules/@maka/eval/harbor/relay_agent.py', @@ -742,6 +757,22 @@ function validateStaging(publishable) { if (!existsSync(join(stageRoot, path))) throw new Error(`Required release file is missing: ${path}`); } + assertRuntimeHostCompatibilityEpoch({ + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + packagedPath: join(stageRoot, 'node_modules/@maka/runtime-host/dist/protocol/index.js'), + }); + if (publishable) { + const manifest = readJson(join(stageRoot, 'package.json')); + assertMakaReleaseIdentity({ + expected: resolveMakaReleaseIdentity({ + version: manifest.version, + sourceCommit: process.env.MAKA_RELEASE_SOURCE_COMMIT?.trim(), + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + }), + actual: manifest.makaReleaseIdentity, + label: 'CLI release identity', + }); + } assertPatchedFile( 'node_modules/node-pty/lib/unixTerminal.js', 'CustomWriteStream.prototype._ownsFileDescriptor', @@ -817,9 +848,11 @@ function validatePackedFiles(files, expectedDependencyManifests, publishable) { } } const requiredPacked = [ + 'package.json', 'dist/cli.js', 'DISCLAIMER-WIP', 'node_modules/@maka/runtime/dist/workers/filesystem-worker.js', + 'node_modules/@maka/runtime-host/dist/protocol/index.js', 'node_modules/@maka/runtime-host/dist/execution-candidate-main.js', 'node_modules/@maka/eval/harbor/relay_agent.py', ...(publishable || privatePeerTarget !== 'none' ? ['native/runtime-host-peer/prebuilds/'] : []), diff --git a/scripts/smoke-release-cli-package.mjs b/scripts/smoke-release-cli-package.mjs index 774a1832d8..89ff14fa68 100644 --- a/scripts/smoke-release-cli-package.mjs +++ b/scripts/smoke-release-cli-package.mjs @@ -37,6 +37,11 @@ import { tmpdir } from 'node:os'; import { basename, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { validateCliReleaseArtifactMetrics } from './release-cli-artifact-policy.mjs'; +import { + assertMakaReleaseIdentity, + assertRuntimeHostCompatibilityEpoch, + resolveMakaReleaseIdentity, +} from './release-cli-compatibility.mjs'; import { findReleaseTarball } from './release-cli-eval-support.mjs'; import { collectRuntimeHostFailureDiagnostic, @@ -143,6 +148,13 @@ async function validateInstalledProduct(root) { const maka = process.platform === 'win32' ? join(prefix, 'maka.cmd') : join(prefix, 'bin/maka'); const cliEntrypoint = join(packageRoot, 'dist/cli.js'); const manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')); + const releaseIdentity = manifest.makaReleaseIdentity + ? resolveMakaReleaseIdentity({ + version: manifest.version, + sourceCommit: resolveSmokeSourceCommit(), + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + }) + : undefined; const crossSpawnModule = await importInstalled(packageRoot, 'node_modules/cross-spawn/index.js'); const crossSpawn = crossSpawnModule.default ?? crossSpawnModule; if (typeof crossSpawn.sync !== 'function') { @@ -159,7 +171,7 @@ async function validateInstalledProduct(root) { runSync(crossSpawn.sync, maka, ['eval', '--help'], baseEnvironment, root), 'usage: maka eval run', ); - validateInstalledRuntimeFiles(packageRoot); + validateInstalledRuntimeFiles(packageRoot, releaseIdentity); logStep('checking installed Eval spec decoding and framework preflight'); smokeEvalPreflight({ crossSpawn: crossSpawn.sync, environment: baseEnvironment, maka, root }); @@ -188,6 +200,15 @@ async function validateInstalledProduct(root) { root: join(root, 'runtime-host-service'), }); + if (releaseIdentity) { + logStep('checking the cross-artifact Runtime Host handshake'); + await smokeCrossArtifactRuntimeHostCompatibility({ + packageRoot, + releaseIdentity, + root: join(root, 'cross-artifact-runtime-host'), + }); + } + logStep('checking a filesystem-backed controlled model turn'); await smokeControlledRun({ packageRoot, @@ -392,12 +413,13 @@ function validateReleaseArtifact(path) { }); } -function validateInstalledRuntimeFiles(packageRoot) { +function validateInstalledRuntimeFiles(packageRoot, expectedReleaseIdentity) { for (const path of [ // Incubator policy: the installed package carries the incubating // disclaimer next to LICENSE/NOTICE, like every other Maka release. 'DISCLAIMER-WIP', 'node_modules/@maka/runtime/dist/workers/filesystem-worker.js', + 'node_modules/@maka/runtime-host/dist/protocol/index.js', 'node_modules/@maka/runtime-host/dist/execution-candidate-main.js', 'node_modules/@maka/eval/dist/index.js', 'node_modules/@maka/eval/harbor/relay_agent.py', @@ -407,6 +429,18 @@ function validateInstalledRuntimeFiles(packageRoot) { throw new Error(`Installed runtime file is missing: ${path}`); } } + assertRuntimeHostCompatibilityEpoch({ + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + packagedPath: join(packageRoot, 'node_modules/@maka/runtime-host/dist/protocol/index.js'), + }); + if (expectedReleaseIdentity) { + const manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')); + assertMakaReleaseIdentity({ + expected: expectedReleaseIdentity, + actual: manifest.makaReleaseIdentity, + label: 'Installed CLI release identity', + }); + } assertOutput( readFileSync(join(packageRoot, 'node_modules/node-pty/lib/unixTerminal.js'), 'utf8'), 'CustomWriteStream.prototype._ownsFileDescriptor', @@ -618,6 +652,183 @@ async function smokeRuntimeHostService({ packageRoot, cliEntrypoint, ptySpawn, r ); } +async function smokeCrossArtifactRuntimeHostCompatibility({ packageRoot, releaseIdentity, root }) { + const server = await importInstalled( + packageRoot, + 'node_modules/@maka/runtime-host/dist/server/index.js', + ); + const hostProtocol = await importInstalled( + packageRoot, + 'node_modules/@maka/runtime-host/dist/protocol/index.js', + ); + // The smoke job deliberately does not build the source workspaces. The + // checked-out source identity supplies the Client epoch, while this raw + // canonical hello supplies the equivalent Client wire probe; the Host and + // response decoder come from the installed npm artifact. + const access = await importInstalled(packageRoot, 'dist/runtime-host-access-command.js'); + const wsModule = await importInstalled(packageRoot, 'node_modules/ws/index.js'); + const WebSocket = wsModule.default ?? wsModule.WebSocket ?? wsModule; + if (typeof WebSocket !== 'function') throw new Error('Installed ws WebSocket API is unavailable'); + + const hostRoot = join(root, 'host'); + mkdirSync(hostRoot, { recursive: true }); + let host; + const sockets = []; + try { + host = await server.startExecutionRuntimeHostService({ + rootPath: hostRoot, + websocket: { host: '127.0.0.1', port: 0 }, + }); + const issued = await access.issueRuntimeHostAccessCredential({ + rootPath: hostRoot, + expectedRootId: host.rootId, + principalKind: 'remote_owner', + principalId: 'release-cross-artifact-client', + operationGrants: [], + canPublishClientCapabilities: false, + canUseHostPaths: false, + preset: 'terminal-client', + }); + const endpoint = host.websocketEndpoints[0]; + if (!endpoint) throw new Error('Cross-artifact Runtime Host has no WebSocket endpoint'); + + const matchingSocket = await openReleaseSmokeWebSocket(WebSocket, endpoint, issued.credential); + sockets.push(matchingSocket); + matchingSocket.send( + encodeReleaseSmokeFrame(releaseSmokeHello(releaseIdentity.compatibilityEpoch, 'matching')), + ); + const accepted = await readReleaseSmokeHandshake(matchingSocket, hostProtocol.decodeHostFrame); + if ( + accepted.kind !== 'accepted' || + accepted.rootId !== host.rootId || + accepted.compatibilityEpoch !== releaseIdentity.compatibilityEpoch + ) { + throw new Error('Cross-artifact Runtime Host handshake did not accept the matching identity'); + } + await closeReleaseSmokeWebSocket(matchingSocket); + + const mismatchedEpoch = + releaseIdentity.compatibilityEpoch > 1 + ? releaseIdentity.compatibilityEpoch - 1 + : releaseIdentity.compatibilityEpoch + 1; + const mismatchedSocket = await openReleaseSmokeWebSocket( + WebSocket, + endpoint, + issued.credential, + ); + sockets.push(mismatchedSocket); + mismatchedSocket.send( + encodeReleaseSmokeFrame(releaseSmokeHello(mismatchedEpoch, 'mismatched')), + ); + const incompatible = await readReleaseSmokeHandshake( + mismatchedSocket, + hostProtocol.decodeHostFrame, + ); + if ( + incompatible.kind !== 'incompatible' || + incompatible.compatibilityEpoch !== releaseIdentity.compatibilityEpoch + ) { + throw new Error( + 'Cross-artifact Runtime Host handshake did not reject the mismatched epoch before admission', + ); + } + await closeReleaseSmokeWebSocket(mismatchedSocket); + } finally { + for (const socket of sockets) await closeReleaseSmokeWebSocket(socket).catch(() => undefined); + await host?.close().catch(() => undefined); + } +} + +function releaseSmokeHello(compatibilityEpoch, suffix) { + return { + kind: 'hello', + clientInstanceId: `release-cross-artifact-${suffix}`, + surface: 'desktop', + protocolMin: 0, + protocolMax: 0, + compatibilityEpoch, + compositionId: 'maka.interactive', + }; +} + +function encodeReleaseSmokeFrame(frame) { + return Buffer.from(JSON.stringify(frame), 'utf8'); +} + +function openReleaseSmokeWebSocket(WebSocket, endpoint, credential) { + return new Promise((resolvePromise, reject) => { + const socket = new WebSocket(endpoint, { + headers: { authorization: `Bearer ${credential}` }, + handshakeTimeout: 10_000, + perMessageDeflate: false, + }); + const fail = (error) => { + socket.terminate(); + reject(error); + }; + const onOpen = () => { + socket.off('error', fail); + socket.once('error', () => undefined); + resolvePromise(socket); + }; + socket.once('open', onOpen); + socket.once('error', fail); + }); +} + +function readReleaseSmokeHandshake(socket, decodeHostFrame) { + return new Promise((resolvePromise, reject) => { + const onMessage = (data) => { + cleanup(); + try { + resolvePromise(decodeHostFrame(JSON.parse(data.toString()))); + } catch (error) { + reject(error); + } + }; + const onError = (error) => { + cleanup(); + reject(error); + }; + const onClose = () => { + cleanup(); + reject(new Error('Cross-artifact Runtime Host closed before its handshake response')); + }; + const cleanup = () => { + socket.off('message', onMessage); + socket.off('error', onError); + socket.off('close', onClose); + }; + socket.once('message', onMessage); + socket.once('error', onError); + socket.once('close', onClose); + }); +} + +function closeReleaseSmokeWebSocket(socket) { + if (!socket || socket.readyState === socket.CLOSED) return Promise.resolve(); + return new Promise((resolvePromise) => { + const timer = setTimeout(() => { + socket.terminate(); + resolvePromise(); + }, 5_000); + socket.once('close', () => { + clearTimeout(timer); + resolvePromise(); + }); + socket.close(); + }); +} + +function resolveSmokeSourceCommit() { + const configured = process.env.MAKA_RELEASE_SOURCE_COMMIT?.trim(); + if (configured) return configured; + return execFileSync('git', ['rev-parse', 'HEAD'], { + cwd: repoRoot, + encoding: 'utf8', + }).trim(); +} + async function allocateLoopbackPort() { const server = createServer(); await new Promise((resolve, reject) => { diff --git a/scripts/verify-macos-arm64-cli.mjs b/scripts/verify-macos-arm64-cli.mjs index 12c187b970..017d0c4033 100644 --- a/scripts/verify-macos-arm64-cli.mjs +++ b/scripts/verify-macos-arm64-cli.mjs @@ -49,6 +49,11 @@ import { resolveMacosArm64CliArtifactPaths, } from './package-macos-arm64-cli.mjs'; import { resolveProductReleaseIdentity } from './product-release-identity.mjs'; +import { + assertMakaReleaseIdentity, + assertRuntimeHostCompatibilityEpoch, + resolveMakaReleaseIdentity, +} from './release-cli-compatibility.mjs'; const execFileAsync = promisify(execFile); const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); @@ -525,6 +530,7 @@ export async function verifyMacosArm64Cli( run = runCommand, smokeTui = smokeTuiInPty, requireReleaseSigning = process.env.MAKA_CLI_REQUIRE_RELEASE_SIGNING === '1', + environment = process.env, } = {}, ) { assertMacosArm64CliHost(platform, arch); @@ -532,13 +538,21 @@ export async function verifyMacosArm64Cli( readFile(join(repoRoot, 'package.json'), 'utf8').then(JSON.parse), readFile(join(repoRoot, 'apps', 'desktop', 'package.json'), 'utf8').then(JSON.parse), readFile(join(repoRoot, 'packages', 'cli', 'package.json'), 'utf8').then(JSON.parse), - run('git', ['rev-parse', 'HEAD'], { cwd: repoRoot }), + environment.MAKA_RELEASE_SOURCE_COMMIT?.trim() || + run('git', ['rev-parse', 'HEAD'], { cwd: repoRoot }), ]); + const sourceCommit = + typeof sourceCommitResult === 'string' ? sourceCommitResult : sourceCommitResult.stdout.trim(); const identity = resolveProductReleaseIdentity({ rootManifest, desktopManifest, cliManifest, - sha: sourceCommitResult.stdout.trim(), + sha: sourceCommit, + }); + const runtimeHostReleaseIdentity = resolveMakaReleaseIdentity({ + version: identity.version, + sourceCommit: identity.sourceCommit, + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), }); const version = identity.version; const expectedPaths = resolveMacosArm64CliArtifactPaths(version); @@ -570,11 +584,16 @@ export async function verifyMacosArm64Cli( const nodePath = join(archiveRoot, 'libexec', 'node', 'bin', 'node'); const makaPath = join(archiveRoot, 'bin', 'maka'); const metadataPath = join(archiveRoot, 'RELEASE.json'); + const runtimeHostProtocolPath = join( + archiveRoot, + 'libexec/node_modules/@maka/runtime-host/dist/protocol/index.js', + ); const thirdPartyNoticesPath = join(archiveRoot, 'THIRD_PARTY_NOTICES.txt'); const requiredPaths = [ nodePath, makaPath, metadataPath, + runtimeHostProtocolPath, thirdPartyNoticesPath, join(archiveRoot, 'DISCLAIMER-WIP'), join(archiveRoot, 'LICENSE'), @@ -614,6 +633,15 @@ export async function verifyMacosArm64Cli( ) { throw new Error('CLI release metadata does not match the product release identity.'); } + assertMakaReleaseIdentity({ + expected: runtimeHostReleaseIdentity, + actual: metadata.makaReleaseIdentity, + label: 'CLI release Runtime Host identity', + }); + assertRuntimeHostCompatibilityEpoch({ + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + packagedPath: runtimeHostProtocolPath, + }); if (JSON.stringify(metadata.dependencyPatches) !== JSON.stringify(expectedDependencyPatches)) { throw new Error('CLI release metadata does not match the repository dependency patches.'); } diff --git a/scripts/verify-macos-arm64-dmg.mjs b/scripts/verify-macos-arm64-dmg.mjs index 1d9fa1f1ac..c738707080 100644 --- a/scripts/verify-macos-arm64-dmg.mjs +++ b/scripts/verify-macos-arm64-dmg.mjs @@ -32,6 +32,7 @@ import { basename, dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { FILESYSTEM_WORKER_PROTOCOL_VERSION } from '../packages/runtime/dist/filesystem-worker/protocol.js'; import { readProductManifestIdentity } from './product-release-identity.mjs'; +import { resolveMakaReleaseIdentity } from './release-cli-compatibility.mjs'; import { assertPackagedUpdateConfiguration } from './desktop-update-contract.mjs'; import { resolveDesktopBuildVersion, resolveRuntimeHostSetupPackage } from './desktop-nightly.mjs'; import { @@ -144,6 +145,14 @@ export async function verifyPackagedMacApp( if (version !== expectedVersion) { throw new Error(`Expected app version ${expectedVersion}, found ${version}.`); } + const sourceCommit = + environment.MAKA_RELEASE_SOURCE_COMMIT?.trim() || + (await run('git', ['rev-parse', 'HEAD'])).stdout.trim(); + const makaReleaseIdentity = resolveMakaReleaseIdentity({ + version: expectedVersion, + sourceCommit, + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + }); const executableName = await readPlistValue(run, infoPlist, 'CFBundleExecutable'); const executable = join(contents, 'MacOS', executableName); const filesystemWorker = join(resources, 'workers', 'filesystem-worker.js'); @@ -166,6 +175,7 @@ export async function verifyPackagedMacApp( '/bin/echo', ['maka-node-pty-ok'], resolveRuntimeHostSetupPackage(product.version, environment), + makaReleaseIdentity, ); await run(executable, ['-e', ptyProbe, join(appAsar, 'package.json')], { env: { diff --git a/scripts/verify-packaged-app.mjs b/scripts/verify-packaged-app.mjs index 9936628fb4..a07766d3d8 100644 --- a/scripts/verify-packaged-app.mjs +++ b/scripts/verify-packaged-app.mjs @@ -627,12 +627,13 @@ export async function stopChild(child) { } } -export function makePtyProbe(shellFile, shellArgs, runtimeHostSetupPackage) { +export function makePtyProbe(shellFile, shellArgs, runtimeHostSetupPackage, releaseIdentity) { return String.raw` const { createRequire } = require('node:module'); const requireFromApp = createRequire(process.argv[1]); const appManifest = requireFromApp('./package.json'); const expectedRuntimeHostSetupPackage = ${JSON.stringify(runtimeHostSetupPackage)}; +const expectedMakaReleaseIdentity = ${JSON.stringify(releaseIdentity)}; if ( expectedRuntimeHostSetupPackage !== undefined && appManifest.runtimeHostSetupPackage !== expectedRuntimeHostSetupPackage @@ -645,6 +646,33 @@ if ( ); process.exit(1); } +if ( + expectedMakaReleaseIdentity !== undefined && + JSON.stringify(appManifest.makaReleaseIdentity) !== JSON.stringify(expectedMakaReleaseIdentity) +) { + console.error( + 'Packaged Maka release identity mismatch: expected ' + + JSON.stringify(expectedMakaReleaseIdentity) + + ', found ' + + JSON.stringify(appManifest.makaReleaseIdentity), + ); + process.exit(1); +} +if (expectedMakaReleaseIdentity !== undefined) { + const runtimeHostProtocol = requireFromApp('@maka/runtime-host/protocol'); + if ( + runtimeHostProtocol.RUNTIME_HOST_COMPATIBILITY_EPOCH !== + expectedMakaReleaseIdentity.compatibilityEpoch + ) { + console.error( + 'Packaged Runtime Host compatibility epoch mismatch: expected ' + + expectedMakaReleaseIdentity.compatibilityEpoch + + ', found ' + + runtimeHostProtocol.RUNTIME_HOST_COMPATIBILITY_EPOCH, + ); + process.exit(1); + } +} const pty = requireFromApp('node-pty'); const child = pty.spawn(${JSON.stringify(shellFile)}, ${JSON.stringify(shellArgs)}, { name: 'xterm-color', diff --git a/scripts/verify-windows-x64.mjs b/scripts/verify-windows-x64.mjs index 3b19751c04..b228399dec 100644 --- a/scripts/verify-windows-x64.mjs +++ b/scripts/verify-windows-x64.mjs @@ -23,6 +23,7 @@ import { tmpdir } from 'node:os'; import { basename, dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { readProductManifestIdentity } from './product-release-identity.mjs'; +import { resolveMakaReleaseIdentity } from './release-cli-compatibility.mjs'; import { assertPackagedUpdateConfiguration } from './desktop-update-contract.mjs'; import { resolveDesktopBuildVersion, resolveRuntimeHostSetupPackage } from './desktop-nightly.mjs'; import { @@ -138,6 +139,17 @@ export async function verifyPackagedWindowsApp( const executable = join(appDirectory, executableName); const appAsar = join(resources, 'app.asar'); const sandboxExecutable = join(resources, 'windows-sandbox', 'maka-windows-sandbox.exe'); + const expectedBuildVersion = + expectedVersion ?? resolveDesktopBuildVersion(product.version, environment); + const makaReleaseIdentity = requiresCurrentContract + ? resolveMakaReleaseIdentity({ + version: expectedBuildVersion, + sourceCommit: + environment.MAKA_RELEASE_SOURCE_COMMIT?.trim() || + (await run('git', ['rev-parse', 'HEAD'])).stdout.trim(), + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + }) + : undefined; step('checking packaged resources'); await requirePath(executable); @@ -246,10 +258,7 @@ export async function verifyPackagedWindowsApp( run, `(Get-Item -LiteralPath ${powerShellLiteral(executable)}).VersionInfo.ProductVersion`, ); - assertWindowsProductVersion( - stdout, - expectedVersion ?? resolveDesktopBuildVersion(product.version, environment), - ); + assertWindowsProductVersion(stdout, expectedBuildVersion); step('smoking node-pty through conpty'); const ptyProbe = makePtyProbe( @@ -258,6 +267,7 @@ export async function verifyPackagedWindowsApp( requiresCurrentContract ? resolveRuntimeHostSetupPackage(product.version, environment) : undefined, + makaReleaseIdentity, ); await run(executable, ['-e', ptyProbe, join(appAsar, 'package.json')], { env: { From 0f471a4ba1857e845fe26e46743325c664c1e0f4 Mon Sep 17 00:00:00 2001 From: Hsin <2129830748@qq.com> Date: Wed, 2 Sep 2026 13:26:15 +0800 Subject: [PATCH 2/4] fix(release): send handshake smoke frames as text --- package.json | 2 +- scripts/release-cli-websocket-smoke.mjs | 22 ++++++++++++++ scripts/release-cli-websocket-smoke.test.mjs | 30 ++++++++++++++++++++ scripts/smoke-release-cli-package.mjs | 5 +--- 4 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 scripts/release-cli-websocket-smoke.mjs create mode 100644 scripts/release-cli-websocket-smoke.test.mjs diff --git a/package.json b/package.json index 41e6a3b51a..c2795d40ec 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "check:runtime-host-peer-dependencies": "node scripts/generate-runtime-host-peer-dependencies.mjs --check", "generate:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs", "check:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs --check", - "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-nightly.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-compatibility.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", + "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-nightly.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-compatibility.test.mjs scripts/release-cli-websocket-smoke.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", "package:macos-arm64": "node scripts/package-macos-arm64.mjs", "verify:macos-arm64": "node scripts/verify-macos-arm64-dmg.mjs", "package:macos-autoupdate-next": "node scripts/package-macos-autoupdate-next.mjs", diff --git a/scripts/release-cli-websocket-smoke.mjs b/scripts/release-cli-websocket-smoke.mjs new file mode 100644 index 0000000000..e5e10f70db --- /dev/null +++ b/scripts/release-cli-websocket-smoke.mjs @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function encodeReleaseSmokeFrame(frame) { + return JSON.stringify(frame); +} diff --git a/scripts/release-cli-websocket-smoke.test.mjs b/scripts/release-cli-websocket-smoke.test.mjs new file mode 100644 index 0000000000..7aae2aa037 --- /dev/null +++ b/scripts/release-cli-websocket-smoke.test.mjs @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { encodeReleaseSmokeFrame } from './release-cli-websocket-smoke.mjs'; + +test('encodes the release Runtime Host handshake as a WebSocket text frame', () => { + const frame = { kind: 'hello', compatibilityEpoch: 93 }; + const encoded = encodeReleaseSmokeFrame(frame); + + assert.equal(typeof encoded, 'string'); + assert.deepEqual(JSON.parse(encoded), frame); +}); diff --git a/scripts/smoke-release-cli-package.mjs b/scripts/smoke-release-cli-package.mjs index 89ff14fa68..3825423335 100644 --- a/scripts/smoke-release-cli-package.mjs +++ b/scripts/smoke-release-cli-package.mjs @@ -48,6 +48,7 @@ import { renderRuntimeHostFailureDiagnostic, retireCollectedRuntimeHostStartupDiagnostic, } from './release-cli-runtime-host-diagnostics.mjs'; +import { encodeReleaseSmokeFrame } from './release-cli-websocket-smoke.mjs'; import { npmSpawnOptions } from './npm-spawn.mjs'; const MAX_OUTPUT_BYTES = 16 * 1024 * 1024; @@ -751,10 +752,6 @@ function releaseSmokeHello(compatibilityEpoch, suffix) { }; } -function encodeReleaseSmokeFrame(frame) { - return Buffer.from(JSON.stringify(frame), 'utf8'); -} - function openReleaseSmokeWebSocket(WebSocket, endpoint, credential) { return new Promise((resolvePromise, reject) => { const socket = new WebSocket(endpoint, { From 8755f9c66c5400d6831330f5e9772c2c4ba0eb18 Mon Sep 17 00:00:00 2001 From: Hsin <2129830748@qq.com> Date: Wed, 2 Sep 2026 14:13:51 +0800 Subject: [PATCH 3/4] fix(release): keep auto-update identity versions in sync Generated-by: Codex --- scripts/desktop-nightly.test.mjs | 13 +++++++++++++ scripts/package-macos-autoupdate-next.mjs | 1 + scripts/package-windows-autoupdate-next.mjs | 1 + 3 files changed, 15 insertions(+) diff --git a/scripts/desktop-nightly.test.mjs b/scripts/desktop-nightly.test.mjs index c65b595756..5a84b43ebb 100644 --- a/scripts/desktop-nightly.test.mjs +++ b/scripts/desktop-nightly.test.mjs @@ -20,6 +20,7 @@ import assert from 'node:assert/strict'; import { execFile } from 'node:child_process'; import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import { basename, dirname, join } from 'node:path'; import { test } from 'node:test'; @@ -195,3 +196,15 @@ test('packaging observes a valid nightly version without changing product manife 'maka-agent@0.2.0-dev.42.20260829', ); }); + +test('version-bumped auto-update builds carry the matching release identity version', async () => { + const scripts = await Promise.all( + ['package-windows-autoupdate-next.mjs', 'package-macos-autoupdate-next.mjs'].map((name) => + readFile(new URL(`./${name}`, import.meta.url), 'utf8'), + ), + ); + + for (const source of scripts) { + assert.match(source, /`-c\.extraMetadata\.makaReleaseIdentity\.version=\$\{nextVersion\}`/u); + } +}); diff --git a/scripts/package-macos-autoupdate-next.mjs b/scripts/package-macos-autoupdate-next.mjs index 169734599c..750ddb6e0a 100644 --- a/scripts/package-macos-autoupdate-next.mjs +++ b/scripts/package-macos-autoupdate-next.mjs @@ -62,6 +62,7 @@ export async function packageMacosAutoupdateNext({ '--publish', 'never', `-c.extraMetadata.version=${nextVersion}`, + `-c.extraMetadata.makaReleaseIdentity.version=${nextVersion}`, '-c.extraMetadata.makaUpdateTestProfile=true', '-c.mac.notarize=false', '-c.directories.output=release-autoupdate-next', diff --git a/scripts/package-windows-autoupdate-next.mjs b/scripts/package-windows-autoupdate-next.mjs index c48b783ab6..83ec1ecc17 100644 --- a/scripts/package-windows-autoupdate-next.mjs +++ b/scripts/package-windows-autoupdate-next.mjs @@ -84,6 +84,7 @@ export async function packageWindowsAutoupdateNext({ '--publish', 'never', `-c.extraMetadata.version=${nextVersion}`, + `-c.extraMetadata.makaReleaseIdentity.version=${nextVersion}`, '-c.directories.output=release-autoupdate-next', ]); From b2e47ec70598aacbe6973729b65829ef05187562 Mon Sep 17 00:00:00 2001 From: Hsin <2129830748@qq.com> Date: Thu, 3 Sep 2026 21:33:46 +0800 Subject: [PATCH 4/4] fix: stabilize release contract CI --- .github/workflows/cli-package-validation.yml | 2 ++ apps/desktop/electron-builder.config.mjs | 28 +++++++++++++------- package.json | 2 +- scripts/ci-workflow-policy.test.mjs | 13 +++++++++ scripts/product-release.test.mjs | 16 +++++++++++ scripts/verify-macos-dmg.mjs | 1 + scripts/verify-windows-x64.mjs | 1 + 7 files changed, 52 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cli-package-validation.yml b/.github/workflows/cli-package-validation.yml index b1482d6aff..54d2b93eef 100644 --- a/.github/workflows/cli-package-validation.yml +++ b/.github/workflows/cli-package-validation.yml @@ -44,6 +44,8 @@ on: - 'scripts/release-cli-compatibility.mjs' - 'scripts/release-cli-compatibility.test.mjs' - 'scripts/release-cli-package.mjs' + - 'scripts/release-cli-websocket-smoke.mjs' + - 'scripts/release-cli-websocket-smoke.test.mjs' - 'scripts/qualify-released-cli-state-root.mjs' - 'scripts/qualify-released-cli-state-root.test.mjs' - 'scripts/released-cli-state-root-fixture.mjs' diff --git a/apps/desktop/electron-builder.config.mjs b/apps/desktop/electron-builder.config.mjs index d630db86e3..3785b1aa92 100644 --- a/apps/desktop/electron-builder.config.mjs +++ b/apps/desktop/electron-builder.config.mjs @@ -71,12 +71,6 @@ const { runtimeHostSetupPackage, version: productVersion } = resolveProductManif desktopManifest: readManifest('./package.json'), cliManifest: readManifest('../../packages/cli/package.json'), }); -const makaReleaseIdentity = resolveMakaReleaseIdentity({ - version: productVersion, - sourceCommit: resolvePackagingSourceCommit(), - sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), -}); - const baseDesktopBuilderConfig = { appId: 'com.maka.desktop', productName: 'Maka', @@ -85,7 +79,6 @@ const baseDesktopBuilderConfig = { beforePack: stageReleaseManifests, extraMetadata: { runtimeHostSetupPackage, - makaReleaseIdentity, makaUpdateChannel: 'release', }, directories: { @@ -333,15 +326,30 @@ const baseDesktopBuilderConfig = { export function resolveDesktopBuilderConfig(environment = process.env) { const nightlyVersion = environment.MAKA_DESKTOP_NIGHTLY_VERSION?.trim(); - if (!nightlyVersion) return baseDesktopBuilderConfig; - const version = resolveDesktopBuildVersion(rootManifest.version, environment); + const version = nightlyVersion + ? resolveDesktopBuildVersion(rootManifest.version, environment) + : productVersion; + const makaReleaseIdentity = resolveMakaReleaseIdentity({ + version, + sourceCommit: resolvePackagingSourceCommit(environment), + sourcePath: join(repoRoot, 'packages/runtime-host/src/protocol/index.ts'), + }); + if (!nightlyVersion) { + return { + ...baseDesktopBuilderConfig, + extraMetadata: { + ...baseDesktopBuilderConfig.extraMetadata, + makaReleaseIdentity, + }, + }; + } return { ...baseDesktopBuilderConfig, extraMetadata: { ...baseDesktopBuilderConfig.extraMetadata, version, runtimeHostSetupPackage: resolveRuntimeHostSetupPackage(rootManifest.version, environment), - makaReleaseIdentity: { ...makaReleaseIdentity, version }, + makaReleaseIdentity, makaUpdateChannel: 'nightly', }, publish: [{ provider: 'github', owner: 'apache', repo: 'maka', channel: 'dev' }], diff --git a/package.json b/package.json index 7bdb3bac20..b45d05a378 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "check:runtime-host-peer-dependencies": "node scripts/generate-runtime-host-peer-dependencies.mjs --check", "generate:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs", "check:runtime-host-peer-notices": "node scripts/generate-runtime-host-peer-notices.mjs --check", - "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test scripts/product-nightly.test.mjs scripts/desktop-release-targets.test.mjs scripts/verify-linux-harness.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-compatibility.test.mjs scripts/release-cli-websocket-smoke.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", + "check:release": "npm run check:stale && npm run check:third-party-notices && npm run check:cli-third-party-notices && npm run check:model-metadata && npm run check:product-release-identity && npm run check:asf-npm && node --test --test-concurrency=1 scripts/product-nightly.test.mjs scripts/desktop-release-targets.test.mjs scripts/verify-linux-harness.test.mjs scripts/desktop-nightly.test.mjs scripts/desktop-nightly-stage.test.mjs scripts/desktop-nightly-release.test.mjs scripts/desktop-nightly-workflow-policy.test.mjs scripts/product-release.test.mjs scripts/product-release-authority.test.mjs scripts/release-cli-compatibility.test.mjs scripts/release-cli-websocket-smoke.test.mjs scripts/release-cli-file-policy.test.mjs scripts/release-cli-artifact-policy.test.mjs scripts/release-cli-eval-support.test.mjs scripts/release-cli-publication.test.mjs scripts/release-cli-runtime-host-diagnostics.test.mjs scripts/qualify-released-cli-state-root.test.mjs scripts/release-cli-workflow-policy.test.mjs scripts/verify-packaged-app.test.mjs scripts/third-party-closure.test.mjs scripts/generate-third-party-notices.test.mjs scripts/source-legal-inventory.test.mjs scripts/sync-model-metadata.test.mjs scripts/prepare-windows-upgrade-baseline.test.mjs scripts/windows-package-source-closure.test.mjs", "package:macos-arm64": "node scripts/package-macos.mjs arm64", "package:macos-x64": "node scripts/package-macos.mjs x64", "verify:macos": "node scripts/verify-macos-dmg.mjs", diff --git a/scripts/ci-workflow-policy.test.mjs b/scripts/ci-workflow-policy.test.mjs index 62c03b362d..b6ecad77a1 100644 --- a/scripts/ci-workflow-policy.test.mjs +++ b/scripts/ci-workflow-policy.test.mjs @@ -466,6 +466,19 @@ test('installed-package validation discards superseded pull request runs', () => ); }); +test('CLI package validation watches every release smoke helper', () => { + const filtered = new Set(readPullRequestPathFilter('cli-package-validation.yml')); + for (const path of [ + 'scripts/release-cli-compatibility.mjs', + 'scripts/release-cli-compatibility.test.mjs', + 'scripts/release-cli-websocket-smoke.mjs', + 'scripts/release-cli-websocket-smoke.test.mjs', + 'scripts/smoke-release-cli-package.mjs', + ]) { + assert.ok(filtered.has(path), `${path} is not covered by the CLI validation path filter`); + } +}); + test('the recovery lane keeps every run kind out of one shared concurrency group', () => { const workflow = readWorkflow('windows-recovery.yml'); diff --git a/scripts/product-release.test.mjs b/scripts/product-release.test.mjs index 906108fd32..c47c027a40 100644 --- a/scripts/product-release.test.mjs +++ b/scripts/product-release.test.mjs @@ -371,6 +371,17 @@ test('Desktop packaging derives the Runtime Host setup package from product mani ]); }); +test('Desktop builder binds a supplied source commit to its release identity', async () => { + const sourceCommit = 'a'.repeat(40); + const config = resolveDesktopBuilderConfig({ MAKA_RELEASE_SOURCE_COMMIT: sourceCommit }); + + assert.equal(config.extraMetadata.makaReleaseIdentity.sourceCommit, sourceCommit); + assert.equal( + config.extraMetadata.makaReleaseIdentity.version, + JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8')).version, + ); +}); + test('Desktop stages release manifests before electron-builder builds the archive', async (t) => { const stage = await mkdtemp(join(tmpdir(), 'maka-desktop-release-manifests-')); t.after(() => rm(stage, { recursive: true, force: true })); @@ -490,6 +501,11 @@ test('platform package verifiers keep Git checks out of every artifact', async ( const macosSource = await readFile(join(repoRoot, 'scripts', 'verify-macos-dmg.mjs'), 'utf8'); assert.doesNotMatch(macosSource, /requirePath\(join\(resources, ['"]git['"]/u); + assert.match(macosSource, /await verifyApp\(copiedApp, \{[\s\S]*?environment,\s*\}\);/u); + assert.match( + windowsSource, + /await verifyApp\(unpackedDirectory, \{[\s\S]*?environment,\s*\}\);/u, + ); // The channel is the release descriptor's, resolved once beside the artifact // names. Reading the nightly environment variable a second time here would diff --git a/scripts/verify-macos-dmg.mjs b/scripts/verify-macos-dmg.mjs index 6069bfe8ca..e309af6f9f 100644 --- a/scripts/verify-macos-dmg.mjs +++ b/scripts/verify-macos-dmg.mjs @@ -248,6 +248,7 @@ export async function verifyMacosDmg( workingDirectory: temporaryDirectory, expectedArch: arch, channel: target.nightly ? 'nightly' : 'release', + environment, }); // Which payloads a formal release publishes a `.sha256` beside is the // descriptor's to decide, the way `verify:linux` already reads it. diff --git a/scripts/verify-windows-x64.mjs b/scripts/verify-windows-x64.mjs index ada5747abc..eed5993afa 100644 --- a/scripts/verify-windows-x64.mjs +++ b/scripts/verify-windows-x64.mjs @@ -334,6 +334,7 @@ export async function verifyWindowsX64Release( await verifyApp(unpackedDirectory, { workingDirectory: temporaryDirectory, channel: target.nightly ? 'nightly' : 'release', + environment, }); step('checksumming the release artifacts');