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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 80 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,52 @@ on:
permissions:
contents: read

# Only superseded PR runs are cancelled; main/release evidence is never cancelled here.
concurrency:
group: ci-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
change-scope:
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.scope.outputs.mode }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
- name: Classify the entire PR conservatively
id: scope
env:
CPS_PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
CPS_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node scripts/ci-docs.mjs classify
- name: Verify classification and gate regressions without installing dependencies
run: node --test test/ci-docs.test.js

docs-check:
needs: change-scope
if: needs.change-scope.outputs.mode == 'docs'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
- name: Check ordinary documentation
env:
CPS_PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
CPS_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node scripts/ci-docs.mjs check

test:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
Comment thread
Dailin521 marked this conversation as resolved.
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -39,6 +83,8 @@ jobs:
- run: npm test

workspace-contract:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand All @@ -57,6 +103,8 @@ jobs:
run: npm run architecture:check

root-package-compat:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand All @@ -77,6 +125,8 @@ jobs:
- run: npm run package:smoke:lifecycle

web-build:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand All @@ -94,6 +144,8 @@ jobs:
- run: npm run web:build

web-browser:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -107,6 +159,8 @@ jobs:
- run: npm run web:test:e2e

electron-desktop:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -167,6 +221,8 @@ jobs:
retention-days: 7

electron-release-candidate:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -258,7 +314,8 @@ jobs:
retention-days: 30

electron-candidate-set:
needs: electron-release-candidate
needs: [change-scope, electron-release-candidate]
if: needs.change-scope.outputs.mode == 'full'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -284,6 +341,8 @@ jobs:
retention-days: 30

dependency-audit:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -298,6 +357,8 @@ jobs:
run: npm audit --audit-level=high

cross-runtime-fixtures:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: windows-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -341,6 +402,8 @@ jobs:
retention-days: 30

desktop-test:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: windows-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -359,6 +422,8 @@ jobs:
run: dotnet test desktop/CodexProviderSync.GuiE2E.Tests/CodexProviderSync.GuiE2E.Tests.csproj --configuration Release --no-build

desktop-macos:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: macos-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -373,6 +438,8 @@ jobs:
- run: dotnet build desktop/CodexProviderSync.Mac/CodexProviderSync.Mac.csproj --configuration Release

desktop-linux-lock:
needs: change-scope
if: needs.change-scope.outputs.mode == 'full'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -382,8 +449,9 @@ jobs:
- run: dotnet test desktop/CodexProviderSync.Core.Tests/CodexProviderSync.Core.Tests.csproj --filter FullyQualifiedName~LockServiceTests

c10-evidence-bundle:
if: ${{ always() }}
if: ${{ always() && needs.change-scope.result == 'success' && needs.change-scope.outputs.mode == 'full' }}
needs:
- change-scope
- test
- workspace-contract
- root-package-compat
Expand Down Expand Up @@ -424,7 +492,7 @@ jobs:
CPS_REQUIRED_JOB_RESULTS_JSON: ${{ toJSON(needs) }}
CPS_SOURCE_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
CPS_EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: npm run evidence:c10
run: node scripts/ci-docs.mjs c10
- name: Upload the non-release C10 evidence bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand All @@ -438,6 +506,8 @@ jobs:
name: ci-gate
if: ${{ always() }}
needs:
- change-scope
- docs-check
- test
- workspace-contract
- root-package-compat
Expand All @@ -454,37 +524,11 @@ jobs:
- c10-evidence-bundle
runs-on: ubuntu-latest
steps:
- name: Verify required jobs
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
- name: Verify required jobs without accepting unexpected skips
env:
NODE_TEST_RESULT: ${{ needs.test.result }}
WORKSPACE_CONTRACT_RESULT: ${{ needs.workspace-contract.result }}
ROOT_PACKAGE_COMPAT_RESULT: ${{ needs.root-package-compat.result }}
WEB_BUILD_RESULT: ${{ needs.web-build.result }}
WEB_BROWSER_RESULT: ${{ needs.web-browser.result }}
ELECTRON_DESKTOP_RESULT: ${{ needs.electron-desktop.result }}
ELECTRON_RELEASE_CANDIDATE_RESULT: ${{ needs.electron-release-candidate.result }}
ELECTRON_CANDIDATE_SET_RESULT: ${{ needs.electron-candidate-set.result }}
DEPENDENCY_AUDIT_RESULT: ${{ needs.dependency-audit.result }}
CROSS_RUNTIME_FIXTURES_RESULT: ${{ needs.cross-runtime-fixtures.result }}
DESKTOP_TEST_RESULT: ${{ needs.desktop-test.result }}
MACOS_TEST_RESULT: ${{ needs.desktop-macos.result }}
LINUX_LOCK_RESULT: ${{ needs.desktop-linux-lock.result }}
C10_EVIDENCE_RESULT: ${{ needs.c10-evidence-bundle.result }}
run: |
if [ "$NODE_TEST_RESULT" != "success" ] ||
[ "$WORKSPACE_CONTRACT_RESULT" != "success" ] ||
[ "$ROOT_PACKAGE_COMPAT_RESULT" != "success" ] ||
[ "$WEB_BUILD_RESULT" != "success" ] ||
[ "$WEB_BROWSER_RESULT" != "success" ] ||
[ "$ELECTRON_DESKTOP_RESULT" != "success" ] ||
[ "$ELECTRON_RELEASE_CANDIDATE_RESULT" != "success" ] ||
[ "$ELECTRON_CANDIDATE_SET_RESULT" != "success" ] ||
[ "$DEPENDENCY_AUDIT_RESULT" != "success" ] ||
[ "$CROSS_RUNTIME_FIXTURES_RESULT" != "success" ] ||
[ "$DESKTOP_TEST_RESULT" != "success" ] ||
[ "$MACOS_TEST_RESULT" != "success" ] ||
[ "$LINUX_LOCK_RESULT" != "success" ] ||
[ "$C10_EVIDENCE_RESULT" != "success" ]; then
echo "One or more required CI jobs did not succeed."
exit 1
fi
CPS_CI_NEEDS: ${{ toJSON(needs) }}
run: node scripts/ci-docs.mjs gate
2 changes: 1 addition & 1 deletion apps/desktop/electron-builder.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
appId: com.dailin521.codex-provider-sync
productName: Codex Provider Sync
compression: maximum
compression: normal
electronLanguages:
- en-US
- zh-CN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

namespace CodexProviderSync.Core.Tests;

[CollectionDefinition("Cross-runtime process locks", DisableParallelization = true)]
public sealed class CrossRuntimeProcessLockCollection;

// Keep the real Node/.NET contenders inside each test concurrent, while avoiding
// unrelated process-heavy classes competing for the hosted runner's startup budget.
[Collection("Cross-runtime process locks")]
public sealed class CrossRuntimeStateDbLockTests
{
private static readonly TimeSpan NodeReadinessTimeout = TimeSpan.FromSeconds(30);
Expand Down Expand Up @@ -216,7 +222,11 @@ public async Task RealNodeHomeOwner_MakesDotNetStatusReturnItsLastCompleteSnapsh

string script = $$"""
import { acquireLock } from {{JsonSerializer.Serialize(LockingModuleUrl())}};
const release = await acquireLock({{JsonSerializer.Serialize(fixture.CodexHome)}}, "node-home-status-writer");
process.stderr.write("lock-parity: acquiring-home-lock\n");
const release = await acquireLock({{JsonSerializer.Serialize(fixture.CodexHome)}}, "node-home-status-writer", {
onCandidateReady: () => process.stderr.write("lock-parity: candidate-ready\n")
});
process.stderr.write("lock-parity: home-lock-acquired\n");
console.log(JSON.stringify({ ready: true }));
await new Promise((resolve) => process.stdin.once("data", resolve));
await release();
Expand Down
25 changes: 25 additions & 0 deletions docs/CI_PERFORMANCE_ZH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# CI 与打包提速记录

## 本次范围

- 普通文档 PR 使用白名单轻量检查,规则见 [Windows 发布流程](WINDOWS_ELECTRON_RELEASE_ZH.md)。按整个 PR 判断,代码 PR 后续只补文档仍完整检查。
- 同一 PR 新提交取消旧运行;main 和独立发布运行保留。
- electron-builder 使用 `compression: normal`,保留原包大小限制、产物审计和容器验收。
- main 最终源码的跨平台检查与发布门禁不变。RC 与正式更新渠道的产物复用、不稳定测试诊断留待后续处理。

## Windows ZIP 压缩实测

2026-09-11,在同一台 Windows 开发机上,使用锁定的 electron-builder 26.15.7 内部 `archive('zip', ..., { compression, withoutDir: true, preserveSymlinks: false })`,对同一份本地 1.0.2 程序内容分别压缩一次。未设置压缩级别环境变量。该测量用于选择压缩配置,不是新源码的正式发布证据;测试期间机器也执行了其他验证,不能据此承诺 CI 的固定耗时。

| 配置 | 耗时 | ZIP 字节数 |
| --- | ---: | ---: |
| maximum | 310.14 秒 | 128,959,840 |
| normal | 42.48 秒 | 129,224,083 |

本次测量耗时下降约 86%,大小增加 264,243 字节(约 258 KiB,0.20%)。普通压缩 ZIP 低于现有 130 MiB 上限。该版本打包器的 maximum ZIP 使用 `-mx=9 -mfb=258 -mpass=15`,normal 使用 `-mx=7`。

两份 ZIP 解压后的 18 个文件逐个 SHA-256 相同。共同的 `resources/app.asar` SHA-256:`914b88f24d954c1ad9a2f5e4db82f1c52b9db67bfa9020e7e36efacf82e7ee9e`。

normal ZIP 解压后的生产包 4 项测试通过,包含真实公开更新检查、SQLite、Provider 切换、隔离数据 Sync → Restore。普通压缩 NSIS 通过 prepackaged 构建,大小 104,601,532 字节,低于 105 MiB 上限;未在本机执行实际安装/卸载。macOS/Linux 新压缩配置和真实安装验收仍需后续 CI 验证。

复测时使用固定输入目录及新的输出路径,记录打包器版本、源文件哈希、环境变量、耗时、大小及解压后的逐文件哈希。不要复用已存在 ZIP,以免打包器按时间戳跳过压缩;不要放宽包大小限制来使测量通过。
3 changes: 2 additions & 1 deletion docs/MAIN_BRANCH_PROTECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ PR #63 已在 `.github/workflows/ci.yml` 增加名称固定的汇总 Job:
- Windows Desktop 测试必须成功
- macOS 构建和 Core 测试必须成功
- Linux 文件锁测试必须成功
- 任一依赖失败、取消或跳过,`ci-gate` 都失败
- 完整 CI 模式下任一业务依赖失败、取消或跳过,`ci-gate` 都失败
- 普通文档 PR 例外:整个 PR 非空差异均在根 README、CHANGELOG、版本中文发布说明白名单中时,分类和文档校验必须成功,重型任务及 C10 必须全部明确为 skipped,才允许 `ci-gate` 通过。main push 始终完整;轻量结果不作为发布证据。具体范围见 [ADR-0039 增补](adr/0039-windows-first-electron-release.md) 和 [发布流程](WINDOWS_ELECTRON_RELEASE_ZH.md)。上方旧四任务 YAML 仅说明稳定汇总门禁原理;当前精确任务清单与模式判定以 `.github/workflows/ci.yml`、`scripts/ci-docs.mjs` 为准。
- Ruleset 只绑定名称稳定的 `ci-gate`

未来可以修改矩阵版本或增加内部检查,而不必同步修改 `main` 的 Required status
Expand Down
10 changes: 9 additions & 1 deletion docs/WINDOWS_ELECTRON_RELEASE_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@
## 1. 提交与合并

1. 保留本地已有变更,核对源码、合同、配套生成物与测试清单。不要提交 `output/`、Playwright traces、真实数据或本地安装目录。
2. 首次 V1 迁移使用 V1 分支;后续补丁从最新 `origin/main` 创建独立修复分支。以可审计提交推送并创建指向 main 的 PR,等待当前 head 对应的完整 `ci-gate` 成功;旧 head 的绿灯不可复用。
2. 首次 V1 迁移使用 V1 分支;后续补丁从最新 `origin/main` 创建独立修复分支。以可审计提交推送并创建指向 main 的 PR,等待当前 head 对应的 `ci-gate` 成功;产品改动必须完整检查,普通文档例外见下文,旧 head 的绿灯不可复用。
3. 处理所有未解决审查,采用 merge commit 合并,禁止 squash、rebase、force-push 或绕过失败门禁。
4. main 合入后的实际 SHA 必须再次通过 `ci-gate`。所有产物与测试结论记录实际 SHA,不以 PR 分支名代替。

部分重跑:GitHub 的“仅重跑失败 job”会保留本次 workflow 中已成功 job 的产物。C10 可复用同一仓库、同一 `runId`、同一 tested commit 且来自当前或更早正整数 `runAttempt` 的历史 Release 验证证据;拒绝跨运行、跨提交、未来或无效次数。全部 required jobs 仍必须成功,Release 资产/二进制/备份哈希校验不变。新 bundle 的 `workflow.runAttempt` 记录当前次数,`historicalFormalRelease.sourceRunAttempt` 如实记录证据来源次数,不改写源证据;v1 schema 将该新增字段设为可选,仅为兼容历史 bundle。重跑成功不抹去之前失败的记录,也不授权发布。

### 普通文档 PR 的轻量检查

仅 `pull_request` 的整个 base…head 差异非空、且所有路径都属于根 `README.md`、`CHANGELOG.md` 或 `docs/release-notes/v*-zh.md` 时,允许轻量检查。不按最后一次提交判断;重命名检查新旧两个路径。合同、ADR、行为夹具、发布流程文档、工作流、配置、依赖和代码均触发完整 CI;未知路径或空差异也走完整 CI,Git 比较失败直接使门禁失败。

轻量检查无需安装依赖,验证改动文档的本地链接目标、全项目版本一致性和改动的中文发布说明。删除白名单文档会使检查失败,避免遗漏未修改文档中的入链;本地目标支持平衡或转义括号。远程链接与 Markdown 锚点不做可达性验证。重型任务和 C10 明确跳过,但 `ci-gate` 始终执行,只有分类与文档检查成功且跳过状态符合预期才通过。轻量 PR 不生成候选包或 C10 发布证据。

main push 始终完整执行全部原有任务和 C10,最终 SHA 的发布门禁不变。同一 PR 新提交可取消过期检查;main 检查和独立发布工作流不被该规则取消。首次提交前一次性核对代码、版本、文档和测试要求,避免补漏引发重复 CI。

## 2. 候选包与独立流程

`.github/workflows/publish-electron-windows.yml` 是独立的手动候选准备流程。现有 `publish.yml` 仍构建 Legacy .NET;`publish-npm.yml` 仍发布 npm。本轮不得运行后两者。
Expand Down
8 changes: 8 additions & 0 deletions docs/adr/0039-windows-first-electron-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

## 决策

### 2026-09-12:普通文档 PR 的合并检查例外

维护者确认 CI 提速后,普通文档 PR 可采用轻量检查,取代本文对这类 PR 一律完整 CI 的要求。范围仅为整个 PR 非空差异全部属于根 `README.md`、`CHANGELOG.md` 或版本中文发布说明;不按最后提交判断,不包含本 ADR、行为契约、工作流、依赖或代码。删除文档不能通过,重命名检查两侧,分类失败不放行。文档链接、版本与公告检查通过后,稳定名称 `ci-gate` 仅接受精确清单中预期的跳过状态。

轻量检查不产生 C10 或候选产物。产品 PR、main push 和正式发布继续遵守下述完整跨平台及最终 SHA 门禁;本增补不允许用轻量 PR 绿灯发布。同一 PR 的旧检查可由新提交取消,main 和独立发布运行不受该取消规则影响。规则及夹具分别见 [发布流程](../WINDOWS_ELECTRON_RELEASE_ZH.md)、[行为夹具](../migration/BEHAVIOR_FIXTURES_ZH.md)。

### 原始发布决策与后续版本约束

2026-09-11 补丁发布约束:输入与 Draft 创建前均从已核验 tag 对象读取根/桌面 `package.json`,两个源码版本必须等于正式版本或 RC 的基础版本;不能用 dispatch 分支清单替代,不允许将 1.0.1 源码注入成 1.0.2。Release 存在性检查同样支持严格 1.0.x,仍拒绝覆盖已有 Draft/公开版本。

维护者确认先交付已进行人工功能测试的 Windows Electron。整理并推送 V1,在最终 PR 的完整 CI 通过后,使用 merge commit 保留既有 checkpoint 历史。最新 main 必须已包含于候选;main 合入后的实际 commit 需要重新取得成功的 `ci-gate` 与绑定该 SHA 的产物证据。
Expand Down
Loading