diff --git a/.github/workflows/ci-cache-warmup-platform-jobs.yml b/.github/workflows/ci-cache-warmup-platform-jobs.yml index 38ca56e57500..ea4f3e7df6d1 100644 --- a/.github/workflows/ci-cache-warmup-platform-jobs.yml +++ b/.github/workflows/ci-cache-warmup-platform-jobs.yml @@ -15,7 +15,9 @@ jobs: strategy: fail-fast: false matrix: - runner: [ubuntu-24.04-arm, blacksmith-4vcpu-ubuntu-2404] + runner: + - ubuntu-24.04-arm + - ${{ github.repository == 'stablyai/orca' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }} runs-on: ${{ matrix.runner }} timeout-minutes: 10 env: diff --git a/.github/workflows/fork-integration-build.yml b/.github/workflows/fork-integration-build.yml index 609486dc110b..f80beb5f2b7b 100644 --- a/.github/workflows/fork-integration-build.yml +++ b/.github/workflows/fork-integration-build.yml @@ -211,6 +211,52 @@ jobs: compression-level: 0 retention-days: 14 + linux: + needs: identity + runs-on: ubuntu-22.04 + timeout-minutes: 90 + env: + ORCA_LOCAL_BUILD_VERSION: ${{ needs.identity.outputs.version }} + ORCA_INTEGRATION_TAG: ${{ needs.identity.outputs.tag }} + ORCA_BUILD_COMMIT: ${{ needs.identity.outputs.sha }} + NODE_OPTIONS: --max-old-space-size=4096 + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ needs.identity.outputs.sha }} + persist-credentials: false + - uses: pnpm/setup@v2 + with: + install: false + - uses: actions/setup-node@v6 + with: + node-version-file: package.json + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: ./.github/actions/install-mobile-dependencies + - uses: ./.github/actions/prepare-wayland-clipboard + - name: Test integration Linux build and update contracts + run: pnpm exec vitest run --config config/vitest.config.ts config/scripts/integration-builds/integration-builds.test.mjs src/shared/integration-builds/release-catalog.test.ts src/main/integration-builds/integration-update-feed.test.ts + - name: Install Linux accessibility verification dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends python3-gi gir1.2-gtk-3.0 gir1.2-atspi-2.0 + - name: Build desktop and native helpers + run: pnpm run build:release + - name: Package Linux x64 AppImage + run: | + pnpm run ensure:electron-runtime + pnpm exec electron-builder --config config/scripts/integration-builds/electron-builder.cjs --linux AppImage --x64 --publish never + - name: Verify packaged Linux CLI launch + run: node config/scripts/run-linux-cli-launch-contract-docker.mjs --appimage dist/orca-linux.AppImage + - uses: actions/upload-artifact@v7 + with: + name: integration-linux-x64-${{ needs.identity.outputs.sha }} + path: dist/orca-linux.AppImage + if-no-files-found: error + compression-level: 0 + retention-days: 14 + android: needs: identity env: @@ -318,7 +364,7 @@ jobs: retention-days: 14 publish: - needs: [identity, macos, android, release-summary] + needs: [identity, macos, linux, android, release-summary] if: github.repository == 'Fartown/orca' && github.ref == 'refs/heads/fork/integration' && github.event_name != 'pull_request' runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/config/architecture-policies.jsonc b/config/architecture-policies.jsonc index 55a2b2da6faa..a4b74ffded79 100644 --- a/config/architecture-policies.jsonc +++ b/config/architecture-policies.jsonc @@ -76,6 +76,10 @@ ".github/workflows/ci-closed-pr-caches.yml", "config/scripts/ci-dependency-download-cache.test.mjs", ".github/workflows/fork-integration-build.yml", + ".github/workflows/pr.yml", + "config/scripts/pr-preflight-gates.test.mjs", + "config/scripts/ci-dependency-download-cache.test.mjs", + ".github/workflows/ci-cache-warmup-platform-jobs.yml", "config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs", "config/scripts/integration-builds/**", "src/shared/integration-builds/**", @@ -582,6 +586,8 @@ "mobile/src/mobile-web-shell/bridge-host-init.test.ts", ".github/workflows/fork-integration-build.yml", "config/scripts/mobile-web-bundle-packaging-workflow-contract.test.mjs", + "config/scripts/ci-dependency-download-cache.test.mjs", + ".github/workflows/ci-cache-warmup-platform-jobs.yml", "config/scripts/integration-builds/**", "src/shared/integration-builds/**", "src/main/integration-builds/**", diff --git a/config/fork-features.jsonc b/config/fork-features.jsonc index b26af0745d46..a1a0914aaaff 100644 --- a/config/fork-features.jsonc +++ b/config/fork-features.jsonc @@ -24,12 +24,14 @@ "features": [ { "id": "integration-builds", - "title": "集成分支自动出包:同一提交的 macOS 与 Android 内测包", + "title": "集成分支自动出包:同一提交的 macOS、Linux 与 Android 内测包", "goal": "同源发布 fork 内测包,并提供桌面更新清单和 Android 应用内更新", "journal": "docs/issue/集成分支自动出包/journal.md", "dependsOn": [ "config/electron-builder.config.cjs", "config/scripts/build-mac-local.mjs", + "config/scripts/build-wayland-clipboard.mjs", + "config/scripts/run-linux-cli-launch-contract-docker.mjs", "mobile/scripts/prepare-android-release.mjs", "mobile/src/theme/mobile-theme.ts", "src/main/startup/configure-process.ts", @@ -63,6 +65,11 @@ "config/scripts/integration-builds/ai-release-summary.mjs" ], "seams": [ + { + "file": ".github/workflows/ci-cache-warmup-platform-jobs.yml", + "mustContain": "github.repository == 'stablyai/orca'", + "why": "Warm the Linux dependency cache on the same available runners used by fork PR checks." + }, { "file": "config/scripts/ci-draft-pr-admission.test.mjs", "mustContain": "repository = 'stablyai/orca'", diff --git a/config/scripts/ci-dependency-download-cache.test.mjs b/config/scripts/ci-dependency-download-cache.test.mjs index 67ac43c95028..fcf503be78bf 100644 --- a/config/scripts/ci-dependency-download-cache.test.mjs +++ b/config/scripts/ci-dependency-download-cache.test.mjs @@ -382,14 +382,16 @@ describe('CI dependency download caches', () => { const warmInstall = warm.steps.find((step) => step.uses === installer) const unitInstall = units.steps.find((step) => step.uses === installer) expect(warm['runs-on']).toBe('${{ matrix.runner }}') - expect(warm.strategy.matrix.runner).toEqual([ - 'ubuntu-24.04-arm', - runInNewContext(pr.jobs.test.with.runner.slice(3, -2), { - github: { repository: 'stablyai/orca' } - }) - ]) for (const repository of ['stablyai/orca', 'Fartown/orca']) { const context = { github: { repository } } + expect( + warm.strategy.matrix.runner.map((runner) => + runner.startsWith('${{') ? runInNewContext(runner.slice(3, -2), context) : runner + ) + ).toEqual([ + 'ubuntu-24.04-arm', + runInNewContext(pr.jobs.test.with.runner.slice(3, -2), context) + ]) expect(runInNewContext(pr.jobs.preflight['runs-on'].slice(3, -2), context)).toBe( repository === 'stablyai/orca' ? 'blacksmith-8vcpu-ubuntu-2404' : 'ubuntu-24.04' ) diff --git a/config/scripts/integration-builds/electron-builder.cjs b/config/scripts/integration-builds/electron-builder.cjs index ae6ce96e2070..48731121d456 100644 --- a/config/scripts/integration-builds/electron-builder.cjs +++ b/config/scripts/integration-builds/electron-builder.cjs @@ -34,5 +34,9 @@ module.exports = { dmg: { ...upstream.dmg, artifactName: 'orca-integration-macos-${arch}.${ext}' + }, + linux: { + ...upstream.linux, + target: ['AppImage'] } } diff --git a/config/scripts/integration-builds/integration-builds.test.mjs b/config/scripts/integration-builds/integration-builds.test.mjs index 456ff860aeae..3cfb6ffe650b 100644 --- a/config/scripts/integration-builds/integration-builds.test.mjs +++ b/config/scripts/integration-builds/integration-builds.test.mjs @@ -208,6 +208,9 @@ describe('integration package identity and signing', () => { expect(config.mac.target).toEqual(['dmg', 'zip']) expect(config.extraMetadata.orcaUpdateChannel).toBe('integration') expect(config.extraMetadata.version).toBe(env.ORCA_LOCAL_BUILD_VERSION) + expect(config.linux.extraResources).toBe(upstream.linux.extraResources) + expect(config.linux.target).toEqual(['AppImage']) + expect(config.appImage).toBe(upstream.appImage) }) it('refuses mixing formal release credentials and integration packaging', () => { @@ -238,9 +241,20 @@ describe('complete, immutable fork prereleases', () => { const manifest = JSON.parse(readFileSync(join(directory, 'build-info.json'), 'utf8')) expect(manifest.sha).toBe(sha) expect(manifest.assets.map((asset) => asset.name)).toEqual([ - ...PACKAGE_NAMES, + ...PACKAGE_NAMES.filter((name) => name !== 'orca-linux.AppImage'), LEGACY_INTEL_PLACEHOLDER ]) + expect(manifest.linuxAssets.map((asset) => asset.name)).toEqual(['orca-linux.AppImage']) + const linuxUpdate = parse(readFileSync(join(directory, 'latest-linux.yml'), 'utf8')) + expect(linuxUpdate.version).toBe(env.ORCA_LOCAL_BUILD_VERSION) + expect(linuxUpdate.files).toEqual( + manifest.linuxAssets.map((asset) => ({ + url: asset.name, + sha512: asset.sha512, + size: asset.bytes + })) + ) + expect(gh.mock.calls[3][0]).toContain(join(directory, 'latest-linux.yml')) expect(manifest.androidVersionCode).toBe(androidConfig.androidVersionCode(timestamp)) expect(manifest.desktopVersion).toBe('1.2.3-preview.2') expect(manifest.macCertificateSha256).toBe(signingCertificate().sha256) @@ -284,15 +298,15 @@ describe('complete, immutable fork prereleases', () => { ) }) - it('does not create a release when one platform is missing or empty', async () => { + it.each(PACKAGE_NAMES)('does not publish when %s is missing or empty', async (name) => { const directory = packages() const gh = github() - writeFileSync(join(directory, PACKAGE_NAMES[2]), '') + writeFileSync(join(directory, name), '') await expect(publishRelease({ env, directory, mobile, gh, git: repository() })).rejects.toThrow( /empty/ ) expect(gh).not.toHaveBeenCalled() - rmSync(join(directory, PACKAGE_NAMES[2])) + rmSync(join(directory, name)) await expect(hashPackages(directory)).rejects.toThrow() }) @@ -488,7 +502,7 @@ describe('workflow wiring', () => { "${{ github.event_name == 'pull_request' }}" ) expect(workflow.jobs.identity.if).toContain("github.repository == 'Fartown/orca'") - for (const name of ['macos', 'android']) { + for (const name of ['macos', 'linux', 'android']) { const job = workflow.jobs[name] expect(job.needs).toBe('identity') expect(job.steps[0].with.ref).toBe('${{ needs.identity.outputs.sha }}') @@ -504,7 +518,13 @@ describe('workflow wiring', () => { (step) => step.name === 'Package internal-test DMG and update ZIP' ).run ).toContain('--publish never') - expect(workflow.jobs.publish.needs).toEqual(['identity', 'macos', 'android', 'release-summary']) + expect(workflow.jobs.publish.needs).toEqual([ + 'identity', + 'macos', + 'linux', + 'android', + 'release-summary' + ]) expect(workflow.jobs.macos.env.CSC_FOR_PULL_REQUEST).toBe('true') expect(workflow.jobs.publish.if).toContain("github.ref == 'refs/heads/fork/integration'") expect(workflow.jobs.publish.if).not.toContain('always()') diff --git a/config/scripts/integration-builds/publish-release.mjs b/config/scripts/integration-builds/publish-release.mjs index e750e64de1be..2cd0ef128963 100644 --- a/config/scripts/integration-builds/publish-release.mjs +++ b/config/scripts/integration-builds/publish-release.mjs @@ -21,7 +21,8 @@ import { export const PACKAGE_NAMES = [ 'orca-integration-macos-arm64.dmg', 'orca-integration-macos-arm64.zip', - 'orca-integration-android.apk' + 'orca-integration-android.apk', + 'orca-linux.AppImage' ] const MAC_UPDATE_ZIP = 'orca-integration-macos-arm64.zip' @@ -69,6 +70,7 @@ const INSTALL_NOTES = [ '提供 Apple Silicon ZIP 与 latest-mac.yml,支持同一固定签名身份之间的原生自动更新。现有 ad-hoc 旧版需先手动安装一次 DMG,之后可应用内下载并确认重启更新。', `Intel 版已停止提供。${LEGACY_INTEL_PLACEHOLDER} 不是安装包,只是一份说明,让 2026-09-17 之前的集成包仍把本次发布识别为完整并继续自动更新。`, 'Android 集成包自动检查 fork 更新,可在应用内下载 APK 后通过系统确认安装。旧版需先手动安装一次;不同签名安装不能覆盖。APK 使用 Expo debug 内测签名,不用于商店发布。', + 'Linux x64 提供 AppImage 与 latest-linux.yml,复用上游原生依赖和 glibc 2.31 门槛。已解包运行的安装需要手动替换;安装前保留原应用和配置。', 'build-info.json 和 SHA256SUMS.txt 记录来源及下载校验和。' ] @@ -103,7 +105,7 @@ function buildNotes( foldedSection( '构建信息', bulletList([ - `版本:macOS ${version};Android ${android.version}(versionCode ${android.versionCode})`, + `版本:macOS / Linux ${version};Android ${android.version}(versionCode ${android.versionCode})`, `提交:[\`${sha.slice(0, 12)}\`](https://github.com/${repo}/commit/${sha})`, `构建:${runUrl}`, since @@ -232,11 +234,14 @@ export async function publishRelease({ androidCertificateSha256: ANDROID_CERT_SHA256, macSigning: 'fixed self-signed publisher, not notarized', macCertificateSha256, - assets: published, + // Older clients reject unknown names in assets, but ignore optional manifest fields. + assets: published.filter((asset) => asset.name !== 'orca-linux.AppImage'), + linuxAssets: published.filter((asset) => asset.name === 'orca-linux.AppImage'), changes: manifestChanges(context.buildChanges, syncSummaries) } writeFileSync(join(directory, 'build-info.json'), `${JSON.stringify(manifest, null, 2)}\n`) writeFileSync(join(directory, 'latest-mac.yml'), macUpdateManifest(version, assets)) + writeFileSync(join(directory, 'latest-linux.yml'), linuxUpdateManifest(version, assets)) writeFileSync( join(directory, 'SHA256SUMS.txt'), published.map((a) => `${a.sha256} ${a.name}\n`).join('') @@ -304,6 +309,7 @@ export async function publishRelease({ ...PACKAGE_NAMES, LEGACY_INTEL_PLACEHOLDER, 'latest-mac.yml', + 'latest-linux.yml', 'build-info.json', 'SHA256SUMS.txt' ].map((name) => join(directory, name)) @@ -337,9 +343,17 @@ export async function publishRelease({ } export function macUpdateManifest(version, assets) { - const files = assets.filter((asset) => asset.name === MAC_UPDATE_ZIP) + return updateManifest(version, assets, MAC_UPDATE_ZIP) +} + +export function linuxUpdateManifest(version, assets) { + return updateManifest(version, assets, 'orca-linux.AppImage') +} + +function updateManifest(version, assets, name) { + const files = assets.filter((asset) => asset.name === name) if (files.length !== 1) { - throw new Error('The Apple Silicon update ZIP is required.') + throw new Error(`The update package is required: ${name}`) } return [ `version: ${JSON.stringify(version)}`, diff --git a/docs/issue/README.md b/docs/issue/README.md index ef575d51c287..15d55820cc98 100644 --- a/docs/issue/README.md +++ b/docs/issue/README.md @@ -4,7 +4,7 @@ | 需求 | 目标 | 状态 | 需求文档 | 方案 | 测试用例 | 更新 | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -| [集成分支自动出包](集成分支自动出包/journal.md) | 同源发布 fork 内测包,并提供桌面更新清单和 Android 应用内更新 | done | [集成分支自动出包](集成分支自动出包/requirements/集成分支自动出包.md) | [2026-10-21-orcad上游同步](集成分支自动出包/solutions/2026-10-21-orcad上游同步.md)、[macOS自动更新签名修复方案](集成分支自动出包/solutions/macOS自动更新签名修复方案.md)、[发布页分层与可读发布说明方案](集成分支自动出包/solutions/发布页分层与可读发布说明方案.md) | [自动出包](集成分支自动出包/tests/cases/自动出包.md) | 2026-10-22 | +| [集成分支自动出包](集成分支自动出包/journal.md) | 同源发布 fork 内测包,并提供桌面更新清单和 Android 应用内更新 | testing | [集成分支自动出包](集成分支自动出包/requirements/集成分支自动出包.md) | [2026-10-21-orcad上游同步](集成分支自动出包/solutions/2026-10-21-orcad上游同步.md)、[macOS自动更新签名修复方案](集成分支自动出包/solutions/macOS自动更新签名修复方案.md)、[发布页分层与可读发布说明方案](集成分支自动出包/solutions/发布页分层与可读发布说明方案.md) | [自动出包](集成分支自动出包/tests/cases/自动出包.md) | 2026-10-22 | | [文档预览大小](文档预览大小/journal.md) | 扩大文档文本预览上限,不扩大普通文件读取和 paired RPC 预算 | testing | [文档预览大小](文档预览大小/requirements/文档预览大小.md) | - | [预览大小边界](文档预览大小/tests/cases/预览大小边界.md) | 2026-10-21 | | [Goal 目标模式](Goal目标模式/journal.md) | 持久目标、既有会话续跑、独立验收与目标管理 UI | testing | [Goal目标模式](Goal目标模式/requirements/Goal目标模式.md) | [Goal守卫监工与唤醒兜底修订方案](Goal目标模式/solutions/Goal守卫监工与唤醒兜底修订方案.md)、[Goal守卫监工与唤醒兜底修订方案.review](Goal目标模式/solutions/Goal守卫监工与唤醒兜底修订方案.review.md)、[Goal目标模式技术说明](Goal目标模式/solutions/Goal目标模式技术说明.md)、[Goal目标管理与交互闭环方案](Goal目标模式/solutions/Goal目标管理与交互闭环方案.md) | [Goal功能测试](Goal目标模式/tests/cases/Goal功能测试.md) | 2026-10-21 | | [Issues 看板与会话](Issues看板与会话/journal.md) | 组织跨工作区会话,复用公共会话名称与原生恢复 | implementing | [Issues看板与会话](Issues看板与会话/requirements/Issues看板与会话.md) | [Issues看板与会话技术说明](Issues看板与会话/solutions/Issues看板与会话技术说明.md) | [Issues功能测试](Issues看板与会话/tests/cases/Issues功能测试.md)、[Issues摘要容量回归](Issues看板与会话/tests/cases/Issues摘要容量回归.md) | 2026-10-21 | diff --git "a/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/journal.md" "b/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/journal.md" index 85fcd1a208e4..bccfa6c04ea4 100644 --- "a/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/journal.md" +++ "b/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/journal.md" @@ -1,7 +1,7 @@ --- title: 集成分支自动出包 slug: 集成分支自动出包 -status: done +status: testing created: 2026-09-12 updated: 2026-10-22 external_ids: [] @@ -9,6 +9,15 @@ external_ids: [] # 集成分支自动出包 开发 Journal +## 2026-10-22:补齐正式发布前的 PR 运行器入口(implementing) + +- 本轮目标:按用户要求合入并发布 Linux 包,以发布包覆盖原安装,后续不创建安装备份。 +- 完成内容:已确认 PR #61 的 Linux、macOS arm64、Android 实际构建成功;完整 PR 检查在 Blacksmith runner 排队,尚不能合并。 +- 代码或文档变更:登记 PR workflow 的 runner 选择接缝及对应合同测试;仅 Fartown/orca 使用 GitHub-hosted runner,上游仍用既有 Blacksmith 规格。 +- 验证证据:Actions 39766206400 的首个 job 119782178485 为 queued,标签 blacksmith-2vcpu-ubuntu-2404;Actions 39766206408 的三平台构建通过。 +- 未解决问题:runner 修复后完整 PR 已实际执行,Actions 39774725680 在全仓格式检查发现 97 个文件,aggregate 未通过,尚不能合并发布。已修正本次 Linux 新增测试的三处格式;其余仓库检查仍待处理。实际网页当前会话可通过快捷键切换并读取对话,恢复的历史标签缺少 provider session 关联而显示空白,终端右键入口仍缺失;这些功能问题未计为通过。 +- 下一步:运行器/缓存/发布相关 217 项测试通过(1 项平台跳过),架构与 fork 功能门禁通过;推送修复后等待完整 PR,再合并发布、覆盖安装并复验。 + ## 1. 关键文档链接 | 类型 | 文档 | 状态 | 说明 | @@ -75,6 +84,24 @@ external_ids: [] ## 3. 开发记录 +### 2026-10-22:Linux PR 吸收最新集成修复(testing) + +- 本轮目标:完成 PR #61 合入,保留 Linux 同源打包与原位安装交付。 +- 完成内容:合并集成提交 3f0e3c1c;解决 runner、缓存合同、功能登记与文档冲突,复用新的通用 fork runner 策略。 +- 代码或文档变更:保留 Linux AppImage、兼容清单和更新源改动;缓存预热与 PR runner 在上游/fork 两种身份下保持一致。 +- 验证证据:370 项相关测试通过、1 项平台跳过;完整类型检查、全仓格式、17 组架构、功能/文档登记通过;按 CI 的 quality/type-aware/react-doctor 范围检查完整 fork 差异通过。 +- 未解决问题:最新远端 CI、最终合入和发布待完成;历史会话 Chat View 关联问题仍未修复。 +- 下一步:推送合并结果并完成 PR 合入,核对集成发布产物。 + +### 2026-10-22 Linux x64 集成包与本机安装(testing) + +- 本轮目标:REQ-413;补齐fork Linux x64安装包并将本机安装从上游切换到fork。 +- 完成内容:已核对上游AppImage摘要与当前运行路径;fork最新发布没有Linux;在独立feat/integration-linux-builds工作树扩展既有功能。 +- 代码或文档变更:先补需求与TC-414、登记标题;实现将复用Linux打包、glibc门槛与fork发布流程,Linux校验资料使用可选字段兼容旧客户端。 +- 验证证据:[Linux合同验证](tests/runs/2026-10-22-linux-contracts.md):桌面148通过/1跳过、移动21通过、完整类型、三个本地化、16组架构、登记/文档和定向lint通过。产物与实际安装待验;原服务尚未退出。 +- 未解决问题:首轮Linux CI在原生验证中缺Atspi typelib,已补runner的GTK/AT-SPI依赖,不跳过验证;本地原生构建仍在恢复容器网络。CI产物与实际安装尚未完成。 +- 下一步:实现、测试、生成PR,取得Linux产物后隔离验收并安装。 + ### 2026-10-22:Fork PR runner 与就绪状态契约修正(testing) - 本轮目标:修复阻塞日志链路补齐合入的已查实问题。 diff --git "a/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/requirements/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205.md" "b/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/requirements/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205.md" index f8d3a78a79c2..b455abcc5fd3 100644 --- "a/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/requirements/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205.md" +++ "b/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/requirements/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205.md" @@ -3,7 +3,7 @@ title: 集成分支自动出包 document_type: requirement status: ready created: 2026-09-12 -updated: 2026-10-06 +updated: 2026-10-22 --- # 集成分支自动出包 @@ -16,8 +16,8 @@ updated: 2026-10-06 ## 范围 -- In Scope:集成分支自动构建、手动重跑、macOS Apple Silicon DMG/ZIP/更新清单、Android APK、fork 更新检查及 Android 应用内下载和安装确认。Intel 版自 2026-09-17 起不再出包(D-005)。 -- Out of Scope:Apple 正式签名/公证、应用商店、稳定更新频道、Windows/Linux 出包、本机 App 替换。 +- In Scope:集成分支自动构建、手动重跑、macOS Apple Silicon DMG/ZIP/更新清单、Linux x64 AppImage/更新清单、Android APK、fork 更新检查及 Android 应用内下载和安装确认。Intel 版自 2026-09-17 起不再出包(D-005)。 +- Out of Scope:Apple 正式签名/公证、应用商店、稳定更新频道、Windows 与 Linux arm64 出包、Linux 已解包安装的自动替换。 ## 需求与验收 @@ -119,3 +119,13 @@ updated: 2026-10-06 | 2026-09-17 | 用户要求发布内容随合入的 PR 变化、版本号改为常规格式;分两步上线 | REQ-406、REQ-407 | D-004 | | 2026-09-17 | 用户要求去掉 Intel 出包;保留占位说明文件兼容旧集成包 | REQ-401、REQ-402、REQ-404 | D-005 | | 2026-10-06 | 用户要求发布页像官方一样清晰:构建发布分层与清理、按用户可见变化分组、同步上游摘要、整理版 AI 短版本、改动经 PR;预览版编号随之改为现有最大编号加一 | REQ-407、REQ-408~REQ-412 | D-006 | + +### REQ-413 Linux x64 同源安装包与兼容更新清单 + +- 当前状态:实现中。 +- 来源:用户2026-10-22要求“改成我的仓库里的安装包吧,如果没有linux版本改一下;加上linux然后安装”。当前本机安装为上游1.4.214,fork发布仅有macOS/Android。 +- 交付补充:用户要求通过完整 PR 门禁合入集成分支、生成正式发布包,再覆盖原安装位置;后续安装不创建备份。Fartown 的 PR 检查使用可用的 GitHub-hosted runner,上游保留 Blacksmith;检查项目、依赖与 aggregate 判定保持不变。 +- 业务规则:Linux x64与macOS/Android锁定同一SHA、预览版号和发布tag;复用既有Linux打包钩子、原生依赖和glibc2.31门槛;全部必需包成功才发布。提供AppImage、latest-linux.yml与校验和,安装包内标记integration并指向Fartown更新源。 +- 兼容性:旧schemaVersion2客户端严格限制assets文件名,Linux校验资料使用可选linuxAssets,原assets保持旧版可读。Linux客户端只选择含完整Linux资产的fork发布;Mac和Android继续读取原字段。Windows保持既有行为。 +- 执行归属:CI runner负责构建与发布;本机安装目录、配置和6768监听由本机服务拥有,不读取SSH远端文件。Linux版同样用于本地工作区和SSH工作区,更新只替换运行客户端或服务的执行主机,不把本地路径用于远端。保留混合版本协议和原SSH功能。 +- 验收标准:TC-414覆盖缺失Linux包拒绝发布、新旧清单兼容与Linux更新选择;真实Linux产物可启动CLI和隔离headless服务,来源SHA/版本/集成渠道可核验;在原安装位置覆盖应用文件,保留用户配置与现有终端 daemon,不创建安装备份;复验正在运行的服务和网页。 diff --git "a/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/tests/cases/\350\207\252\345\212\250\345\207\272\345\214\205.md" "b/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/tests/cases/\350\207\252\345\212\250\345\207\272\345\214\205.md" index 992f622272ba..c53da8466516 100644 --- "a/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/tests/cases/\350\207\252\345\212\250\345\207\272\345\214\205.md" +++ "b/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/tests/cases/\350\207\252\345\212\250\345\207\272\345\214\205.md" @@ -164,3 +164,11 @@ updated: 2026-10-06 - 关联需求:REQ-412;模块:文档、发布说明;优先级:P2;类型:文档核对、单元。 - 步骤:检查 AGENTS.md 与 `docs/reference/fork-maintenance.md`;运行 `release-notes.test.mjs` 的分类用例。 - 预期:两份文档写明除同步上游及其修复外经 PR 合入;发布说明里 PR 带「(#编号)」,直接提交不带。 + +### TC-414 Linux x64 同源发布、安装与旧版兼容 + +- 关联需求:REQ-413。 +- 场景:同一SHA的Linux AppImage与macOS/Android齐备可发布;Linux缺失或空文件时不得公开发布;SHA256与latest-linux.yml的SHA512来自实际AppImage。 +- 旧版:build-info.json原assets仍只含旧版支持的macOS/Android文件,额外linuxAssets不影响旧客户端;无Linux字段的旧发布仍可由Mac/Android读取。 +- Linux:新客户端筛选完整Linux发布,固定fork更新源;不会因最新发布只有Mac而误选上游包;未知Linux名称、空/坏摘要拒绝。 +- 真实安装:检查架构、glibc门槛、CLI版本/帮助、集成渠道;使用隔离本地配置与端口启动headless服务,并按既有SSH验证契约检查远端入口。原安装与用户配置保留备份;没有授权退出时不终止用户运行实例。 diff --git "a/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/tests/runs/2026-10-22-linux-contracts.md" "b/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/tests/runs/2026-10-22-linux-contracts.md" new file mode 100644 index 000000000000..e57cb9d11dbf --- /dev/null +++ "b/docs/issue/\351\233\206\346\210\220\345\210\206\346\224\257\350\207\252\345\212\250\345\207\272\345\214\205/tests/runs/2026-10-22-linux-contracts.md" @@ -0,0 +1,25 @@ +--- +title: Linux x64 出包与旧版更新兼容合同 +status: completed +created: 2026-10-22 +updated: 2026-10-22 +--- + +关联需求:REQ-413。关联用例:TC-414。 + +本记录只确认合同、类型与静态门禁;真实安装和CI产物验收另记,不把本地通过视为安装完成。 + +- 桌面发布/清单/更新相关:12个文件,148通过、1个平台探针跳过;Linux包缺失/空文件拒绝发布,SHA512清单、旧assets兼容、新linuxAssets校验与Linux完整发布选择通过。 +- 移动端集成功能:3个文件21项通过。 +- 完整pnpm tc:4个项目通过;Electron net.fetch不接受URL对象,已使用类型安全的URL字符串适配,不增加断言。 +- pnpm check:fork-features、check:fork-docs通过;完整check:architecture-policies共16组通过。 +- 三个本地化门禁通过,定向oxlint通过。 +- 本机Node24与仓库指定pnpm放在忽略的验证目录,没有修改全局Node/pnpm。 +- Docker基础镜像直连超时;通过用户指定代理取回官方Ubuntu20.04 amd64清单/层,校验digest后导入Docker。容器bridge无法解析代理,改以host网络准备同一Dockerfile的构建缓存,未修改系统Docker配置或降低glibc要求。 +- 真实Linux构建、CLI/headless/SSH与安装验证仍待完成。原工作树未提交的其他需求文档未动。 + +首轮真实CI已运行Linux原生helper且glibc门槛通过;之后verify-computer-native的Python导入报Namespace Atspi not available。工作流补装python3-gi、gir1.2-gtk-3.0、gir1.2-atspi-2.0,保持原验证执行。JavaScript实际发布构建已在本机通过,包括CLI、主进程/renderer、Web投影、技能闭包与mobile-web校验;Linux实际打包/安装仍待验。 + +实际本地AppImage打包确认electron-builder将Linux架构宏展开为x86_64,已将CI收集、发布清单和客户端资产名统一为orca-integration-linux-x86_64.AppImage,避免仅模拟配置测试通过而实际文件漏收。19个随包原生二进制的glibc门槛、打包Node PTY与daemon入口校验已通过。 + +实际打包随后验证到上游static-appimage-package-contract只接受orca-linux.AppImage/orca-linux-arm64.AppImage。最终保留上游标准orca-linux.AppImage名称,fork身份由下载仓库、版本、渠道和SHA确定;同步统一CI、发布、清单和客户端资产名,不放宽或跳过静态AppImage契约。 diff --git a/src/main/integration-builds/integration-update-feed.test.ts b/src/main/integration-builds/integration-update-feed.test.ts index 7a82c5d135b7..e5e86a47bf8e 100644 --- a/src/main/integration-builds/integration-update-feed.test.ts +++ b/src/main/integration-builds/integration-update-feed.test.ts @@ -15,8 +15,10 @@ import { } from './integration-update-feed' const platform = Object.getOwnPropertyDescriptor(process, 'platform')! +const architecture = Object.getOwnPropertyDescriptor(process, 'arch')! afterEach(() => { Object.defineProperty(process, 'platform', platform) + Object.defineProperty(process, 'arch', architecture) mock.app.isPackaged = true vi.resetAllMocks() }) @@ -39,7 +41,7 @@ describe('integration-only default updater feed', () => { mock.readFileSync.mockReturnValue('{}') expect(await pinIntegrationReleaseFeed({} as AppUpdater)).toBe(false) fork() - Object.defineProperty(process, 'platform', { value: 'linux' }) + Object.defineProperty(process, 'platform', { value: 'win32' }) expect(isIntegrationBuild()).toBe(false) expect(mock.fetch).not.toHaveBeenCalled() }) @@ -65,7 +67,13 @@ describe('integration-only default updater feed', () => { tag_name: tag, draft: false, prerelease: true, - assets: [...assets, { name: 'build-info.json' }, { name: 'latest-mac.yml' }] + assets: [ + ...assets, + { name: 'build-info.json' }, + { name: 'latest-mac.yml' }, + { name: 'latest-linux.yml' }, + { name: 'orca-linux.AppImage' } + ] } ]) ) @@ -86,6 +94,32 @@ describe('integration-only default updater feed', () => { ) } + it('pins packaged Linux builds to the fork Linux feed', async () => { + fork() + Object.defineProperty(process, 'platform', { value: 'linux' }) + Object.defineProperty(process, 'arch', { value: 'x64' }) + publish({ + linuxAssets: [{ name: 'orca-linux.AppImage', bytes: 123, sha256: 'b'.repeat(64) }] + }) + const updater = feedUpdater() + expect(isIntegrationBuild()).toBe(true) + expect(await pinIntegrationReleaseFeed(updater)).toBe(true) + expect(updater.setFeedURL).toHaveBeenCalledWith({ + provider: 'generic', + url: `https://github.com/Fartown/orca/releases/download/${tag}` + }) + expect(integrationChangelog(localVersion)?.release.description).toContain('Linux x64') + expect(updater.allowDowngrade).toBe(false) + }) + + it('does not offer an x64 package to an unsupported Linux architecture', () => { + fork() + Object.defineProperty(process, 'platform', { value: 'linux' }) + Object.defineProperty(process, 'arch', { value: 'arm64' }) + expect(isIntegrationBuild()).toBe(false) + expect(mock.fetch).not.toHaveBeenCalled() + }) + it('pins the concrete fork feed and matching notes without allowing downgrades', async () => { fork() const version = localVersion diff --git a/src/main/integration-builds/integration-update-feed.ts b/src/main/integration-builds/integration-update-feed.ts index 86f1ca18a443..a47af0ace683 100644 --- a/src/main/integration-builds/integration-update-feed.ts +++ b/src/main/integration-builds/integration-update-feed.ts @@ -41,7 +41,9 @@ export function integrationChangelog(version: string) { : `Orca Integration ${release.sha.slice(0, 12)}`, description: changeSummary(release) ?? - 'Fork integration build, signed by a fixed self-signed publisher and not notarized. Native updates work between builds with this identity. Older ad-hoc builds require one manual DMG installation first.', + (process.platform === 'linux' + ? 'Fork integration build for Linux x64. AppImage updates come from Fartown/orca. Extracted installations require manual replacement.' + : 'Fork integration build, signed by a fixed self-signed publisher and not notarized. Native updates work between builds with this identity. Older ad-hoc builds require one manual DMG installation first.'), releaseNotesUrl: `${INTEGRATION_RELEASES_URL}/tag/${release.tag}` } } @@ -49,7 +51,10 @@ export function integrationChangelog(version: string) { export function isIntegrationBuild(): boolean { try { - if (!app.isPackaged || process.platform !== 'darwin') { + if (!app.isPackaged || !['darwin', 'linux'].includes(process.platform)) { + return false + } + if (process.platform === 'linux' && process.arch !== 'x64') { return false } return ( @@ -70,7 +75,10 @@ export async function pinIntegrationReleaseFeed( if (!isIntegrationBuild()) { return false } - const release = await fetchIntegrationRelease('mac', net.fetch.bind(net) as typeof fetch) + const release = await fetchIntegrationRelease( + process.platform === 'linux' ? 'linux' : 'mac', + (input, init) => net.fetch(input instanceof URL ? input.toString() : input, init) + ) resolvedRelease = release updater.allowPrerelease = true updater.allowDowngrade = false diff --git a/src/shared/integration-builds/release-catalog.test.ts b/src/shared/integration-builds/release-catalog.test.ts index 6c160cae09ac..6308e9efd444 100644 --- a/src/shared/integration-builds/release-catalog.test.ts +++ b/src/shared/integration-builds/release-catalog.test.ts @@ -35,6 +35,44 @@ const atom = (...tags: string[]) => ) describe('fork integration release catalog', () => { + const linuxAsset = { + name: 'orca-linux.AppImage', + bytes: 1234, + sha256: 'c'.repeat(64) + } + + it('keeps the legacy asset list readable while Linux uses its optional field', () => { + const value = { ...manifest, linuxAssets: [linuxAsset] } + expect(parseIntegrationRelease(value, tag).assets).toEqual(manifest.assets) + expect(parseIntegrationRelease(value, tag).linuxAssets).toEqual([linuxAsset]) + expect(parseIntegrationRelease(manifest, tag).linuxAssets).toBeUndefined() + }) + + it('selects a complete Linux build even when a newer release is Mac-only', async () => { + const newer = { ...release, tag_name: 'integration-124-aaaaaaaaaaaa' } + const linuxRelease = { + ...release, + assets: [...release.assets, { name: 'latest-linux.yml' }, { name: linuxAsset.name }] + } + const fetcher = vi + .fn() + .mockResolvedValueOnce(json([newer, linuxRelease])) + .mockResolvedValueOnce(json({ ...manifest, linuxAssets: [linuxAsset] })) + expect((await fetchIntegrationRelease('linux', fetcher)).tag).toBe(tag) + expect(fetcher.mock.calls[1][0]).toBe(integrationDownloadUrl(tag, 'build-info.json')) + }) + + it.each([ + { linuxAssets: [] }, + { + linuxAssets: [{ name: 'orca-linux.AppImage', bytes: 0, sha256: 'c'.repeat(64) }] + }, + { linuxAssets: [{ name: 'orca-linux.AppImage', bytes: 123, sha256: 'bad' }] }, + { linuxAssets: [{ name: 'other.AppImage', bytes: 123, sha256: 'c'.repeat(64) }] } + ])('rejects malformed optional Linux assets: %j', ({ linuxAssets }) => { + expect(() => parseIntegrationRelease({ ...manifest, linuxAssets }, tag)).toThrow() + }) + it.each([403, 429])('recovers from API HTTP %i using the public fork feed', async (status) => { const fetcher = vi .fn() diff --git a/src/shared/integration-builds/release-catalog.ts b/src/shared/integration-builds/release-catalog.ts index 3a8304625571..487e4a44bc30 100644 --- a/src/shared/integration-builds/release-catalog.ts +++ b/src/shared/integration-builds/release-catalog.ts @@ -25,6 +25,7 @@ export type IntegrationRelease = { androidVersion: string androidVersionCode: number assets: IntegrationAsset[] + linuxAssets?: IntegrationAsset[] /** Unvalidated manifest list; read it through `integrationChanges`. */ changes?: unknown } @@ -109,6 +110,26 @@ export function parseIntegrationRelease(value: unknown, tag: string): Integratio } names.add(asset.name) } + if (item.linuxAssets !== undefined) { + if (!Array.isArray(item.linuxAssets) || !item.linuxAssets.length) { + throw new Error('Invalid integration Linux assets.') + } + for (const asset of item.linuxAssets) { + if ( + !asset || + asset.name !== 'orca-linux.AppImage' || + names.has(asset.name) || + !Number.isSafeInteger(asset.bytes) || + asset.bytes <= 0 || + asset.bytes > 2_000_000_000 || + typeof asset.sha256 !== 'string' || + !/^[a-f0-9]{64}$/.test(asset.sha256) + ) { + throw new Error('Invalid integration Linux asset.') + } + names.add(asset.name) + } + } return item as IntegrationRelease } @@ -152,9 +173,10 @@ async function readManifest(tag: string, fetcher: typeof fetch): Promise name.startsWith('orca-')) - .every((name) => release.assets.some((asset) => asset.name === name)) + .every((name) => assets.some((asset) => asset.name === name)) } async function fetchPublicRelease(required: string[], fetcher: typeof fetch) { @@ -194,14 +216,16 @@ async function fetchPublicRelease(required: string[], fetcher: typeof fetch) { } export async function fetchIntegrationRelease( - platform: 'android' | 'mac', + platform: 'android' | 'mac' | 'linux', fetcher: typeof fetch = fetch ): Promise { const required = [ 'build-info.json', ...(platform === 'android' ? ['orca-integration-android.apk'] - : ['latest-mac.yml', 'orca-integration-macos-arm64.zip']) + : platform === 'linux' + ? ['latest-linux.yml', 'orca-linux.AppImage'] + : ['latest-mac.yml', 'orca-integration-macos-arm64.zip']) ] let releases: unknown try {