From 5602ee552e1e4796764c49ec44d66d7893dfe4fc Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 18:28:59 +0800 Subject: [PATCH 1/6] ci(merge-queue): validate queued main candidates --- .github/workflows/ci.yml | 2 ++ tests/ci/policy.test.mjs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6d1ae937..e3c65f2b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,8 @@ on: branches: [main] pull_request: branches: [main] + merge_group: + types: [checks_requested] workflow_dispatch: permissions: diff --git a/tests/ci/policy.test.mjs b/tests/ci/policy.test.mjs index 3d341aca6..64cfb85d7 100644 --- a/tests/ci/policy.test.mjs +++ b/tests/ci/policy.test.mjs @@ -174,6 +174,37 @@ function results(plan) { }; } +test('merge queue candidates run full checks even when their changes are documentation-only', t => { + const repo = repository(t); + repo.write('docs/site/package.json', JSON.stringify({ scripts: { check: 'node check.mjs' } })); + repo.write('docs/site/package-lock.json', '{}'); + const base = repo.commit(); + repo.write('docs/guide.md', '# Queued documentation change\n'); + const head = repo.commit(); + const event = resolve(repo.root, 'event.json'); + writeFileSync(event, JSON.stringify({ action: 'checks_requested', merge_group: { base_sha: base, head_sha: head } })); + const script = fileURLToPath(new URL('../../.github/scripts/ci-policy.mjs', import.meta.url)); + const env = { ...process.env, GITHUB_EVENT_NAME: 'merge_group', GITHUB_EVENT_PATH: event, + GITHUB_OUTPUT: resolve(repo.root, 'outputs'), GITHUB_STEP_SUMMARY: resolve(repo.root, 'summary') }; + delete env.GITHUB_TOKEN; + delete env.GH_TOKEN; + const plan = JSON.parse(execFileSync(process.execPath, [script, 'classify'], { cwd: repo.root, env, encoding: 'utf8' })); + assert.equal(plan.full_cpp, true); + assert.equal(plan.docs_check, true); + assert.equal(plan.files_known, false); + assert.equal(plan.reason, 'full-event'); + const valid = results(plan); + execFileSync(process.execPath, [script, 'gate'], { cwd: repo.root, + env: { ...env, CI_NEEDS_JSON: JSON.stringify(valid) }, stdio: ['ignore', 'pipe', 'pipe'] }); + for (const job of ['network', 'applications', 'database', 'documentation']) { + for (const result of ['skipped', 'failure', 'cancelled']) { + const invalid = structuredClone(valid); + invalid[job].result = result; + assert.throws(() => validateGate(invalid), `${job}/${result}`); + } + } +}); + test('the required gate accepts only the exact full or explicitly lightweight job plan', () => { for (const plan of [decision(['README.md']), decision(['docs/guide.md']), decision(['src/main.cpp']), decision(['src/main.cpp'], { site: { present: false, available: false } }), From 3a7fb06bec182c7a7359c5be9aaea0fc2d8a4a98 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 18:36:23 +0800 Subject: [PATCH 2/6] docs(contributing): align delivery conventions with merge queue --- AGENTS.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..bb82e233e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,92 @@ +# Contributor and agent instructions + +These rules apply to this repository. Preserve unrelated work and use an isolated +branch or worktree when the existing checkout contains uncommitted changes. + +## Commits and pull requests + +- Write all new commit subjects and PR titles in English using Conventional + Commits: `type(scope): description`. Use a short, lowercase scope and an + imperative description of the resulting change. +- Types: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `build`, `ci`, `style`, + `chore` and `revert`. Choose the type that describes the actual change; use + `style` only for formatting changes without behavior changes. +- Examples: `feat(config): support TOML application manifests`, + `fix(logging): preserve the configured log directory`, + `ci(cache): restore vcpkg package archives`. +- Name new topic branches `type/short-kebab-case`, matching the change, for example + `feat/database-coroutines`, `fix/windows-includes` or `docs/readme-maintainers`. + Do not use the `codex/` prefix or generic `xxx-branch` names. Existing published + branches do not need renaming solely to adopt this rule. Use lowercase names + for new GitHub repositories. +- Mark incompatible public changes with `!` and explain them in a + `BREAKING CHANGE:` footer. Describe migration steps in the PR and release notes. +- Keep each PR focused on one functional module or related change. Publish + independently reviewable work instead of accumulating unrelated modifications. +- All changes to `main` go through a PR. Do not push directly, force-push or delete + `main`, bypass branch protection, or rewrite already merged history. +- Use squash only after every required CI check passes and review conversations + are resolved. Outside the merge queue, synchronize the PR head with `main` + before acceptance. With the merge queue enabled, validate the queue candidate + containing the latest `main` and preceding queued changes; a green PR head alone + does not validate that combination. Add eligible PRs to the queue promptly. + Another maintainer's approval is not currently mandatory. Merge eligible PRs + without asking for confirmation again. +- Explicitly set the squash commit subject to the Conventional Commit PR title. + Replace GitHub's generated commit-list body with a concise change description; + do not copy intermediate commit subjects into release notes. + +## Verification and delivery + +- Run the checks relevant to the change before publishing. Report the exact + tested revision, commands, results and material limitations. +- Investigate failed CI logs before changing code or retrying. Do not repeatedly + rerun an unexplained failure, weaken tests or protection, or extend timeouts to + hide a defect. +- Pending, skipped, cancelled and successful checks are distinct states. A + cancelled run is not validation. If a run is cancelled for scheduling, record + the reason and resume its acceptance checks; do not leave it indefinitely red. +- Use the repository's change-scope policy: documentation changes run the relevant + documentation/link checks; code, build, dependencies and CI changes run the + full configured matrix. Never fabricate a passing required result. +- Include what changed, why, validation and remaining limits in PR descriptions. + Verify the merge result before saying that a PR has merged. + +## C++ and repository style + +- Use C++20 and consistent `snake_case` names, including types, functions and + members (`ark::plugin_manager`, `find_module()`, `module_name_`). Do not manually + edit generated protobuf output; change the schema or generation source. +- Follow `.clang-format` and the repository formatting scripts with clang-format + 18. Keep function opening braces on the declaration line, expand short function + bodies, and use braces for control statements. +- Group includes as system, third-party, then project headers. Keep required + platform-header dependencies in a valid order. Separate include blocks, + namespaces and adjacent definitions with the prescribed blank lines. +- Run `CLANG_FORMAT=/path/to/clang-format bash scripts/format.sh --check`. +- CI policy tests: `node --test tests/ci/*.test.mjs`. Nimbus documentation: + `npm ci --prefix docs/site` followed by `npm run check --prefix docs/site`. +- Keep official plugins in the ark repository and application business plugins + in application repositories. The approved layout has one public header root, + `include/ark/`, with implementation under `src/core/` and + `src/plugins//`; retain independent plugin targets and optional builds. + Verify migration status against accepted source before describing new paths + as available. Do not expand the paused hot-update design without discussing + its lifecycle and compatibility boundaries. + +## Documentation and releases + +- Keep English and Chinese documentation aligned. Keep the README concise and + place detailed usage, architecture and validation material under `docs/`. +- Write release notes for users: summarize core capabilities and group relevant + changes under Features, Improvements, Fixes and Upgrade notes. Omit empty + sections and combine implementation details into their user-visible effect. +- Do not publish a commit-by-commit diary, file list, formatting list or test + assertion inventory. Link detailed validation and performance reports instead. + Retain compatibility changes, security requirements and known limitations. +- Only describe changes contained in the release tag. Verify the release commit, + assets and checks before publishing; never move an existing release tag to + make unreleased work appear included. +- Keep raw measurements and build logs outside the source repository. Published + performance claims must identify the machine, workload, units and tested + revision; local tests do not establish production PCU capacity. From 95286f3e773e5eebbc3698be718b861571f9062e Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 19:13:34 +0800 Subject: [PATCH 3/6] ci(cache): save dependency archives after configure --- .github/workflows/ci.yml | 32 ++++++++--------- tests/ci/README.md | 4 ++- tests/ci/README_CN.md | 3 +- tests/ci/cache-workflow.test.mjs | 59 ++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 18 deletions(-) create mode 100644 tests/ci/cache-workflow.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3c65f2b6..f24cebe42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,13 +130,6 @@ jobs: -DARK_BUILD_NETWORK_TESTS=ON -DASIO_STANDALONE=OFF ${{ matrix.sanitizer == 'address' && '"-DCMAKE_CXX_FLAGS=-fsanitize=address,undefined -fno-omit-frame-pointer" "-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address,undefined"' || matrix.sanitizer == 'thread' && '"-DCMAKE_CXX_FLAGS=-fsanitize=thread -fno-omit-frame-pointer" "-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=thread"' || '' }} - - run: cmake --build ci-build --parallel 2 - - name: Run network regressions - env: - ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 - UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 - TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1 - run: cmake --build ci-build --target check-network - name: Save vcpkg package archives if: ${{ success() && steps.vcpkg-cache.outputs.enabled == 'true' && steps.vcpkg-restore.outputs.cache-hit != 'true' }} continue-on-error: true @@ -144,6 +137,13 @@ jobs: with: path: ${{ steps.vcpkg-cache.outputs.directory }} key: ${{ steps.vcpkg-cache.outputs.key }} + - run: cmake --build ci-build --parallel 2 + - name: Run network regressions + env: + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 + TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1 + run: cmake --build ci-build --target check-network applications: name: Applications - ${{ matrix.os }} - C++20 needs: scope @@ -197,9 +197,6 @@ jobs: -DARK_BUILD_CORE_TESTS=ON -DARK_BUILD_EXAMPLES=ON -DASIO_STANDALONE=OFF - - run: cmake --build ci-build --parallel 2 - - name: Run core and Protobuf regressions - run: cmake --build ci-build --target check-core - name: Save vcpkg package archives if: ${{ success() && steps.vcpkg-cache.outputs.enabled == 'true' && steps.vcpkg-restore.outputs.cache-hit != 'true' }} continue-on-error: true @@ -207,6 +204,9 @@ jobs: with: path: ${{ steps.vcpkg-cache.outputs.directory }} key: ${{ steps.vcpkg-cache.outputs.key }} + - run: cmake --build ci-build --parallel 2 + - name: Run core and Protobuf regressions + run: cmake --build ci-build --target check-core database: name: Database - ${{ matrix.os }} - ${{ matrix.asio }} needs: scope @@ -275,12 +275,6 @@ jobs: "-DARKNET_SOURCE_DIR=$env:GITHUB_WORKSPACE/ci-arknet" -DARKNET_ENABLE_SSL=ON -DASIO_STANDALONE=${{ matrix.standalone }} - - name: Compile native services and coroutine tests - run: cmake --build ci-database --parallel 2 - - name: Run local database lifetime regressions - run: cmake --build ci-database --target check-database - - name: Run local database benchmark self-tests - run: cmake --build ci-database --target check-database-benchmark - name: Save vcpkg package archives if: ${{ success() && steps.vcpkg-cache.outputs.enabled == 'true' && steps.vcpkg-restore.outputs.cache-hit != 'true' }} continue-on-error: true @@ -288,6 +282,12 @@ jobs: with: path: ${{ steps.vcpkg-cache.outputs.directory }} key: ${{ steps.vcpkg-cache.outputs.key }} + - name: Compile native services and coroutine tests + run: cmake --build ci-database --parallel 2 + - name: Run local database lifetime regressions + run: cmake --build ci-database --target check-database + - name: Run local database benchmark self-tests + run: cmake --build ci-database --target check-database-benchmark required: name: CI - required if: always() diff --git a/tests/ci/README.md b/tests/ci/README.md index 62f303e10..c8bb47b16 100644 --- a/tests/ci/README.md +++ b/tests/ci/README.md @@ -21,7 +21,9 @@ The vcpkg cache tests cover Windows/Unix archive paths, dependency and runner-im key changes, provider exports, unavailable storage and explicit cold-build fallbacks. The C++ CI jobs restore only local package archives through Actions cache; vcpkg still validates each package ABI. A cache miss/service failure runs the ordinary -dependency build and tests. Archives are saved only after the job's tests succeed. +dependency build and tests. Archives are saved after successful CMake configuration, +before compiling and testing ark; later build or test failures still fail the job. +Workflow checks cover this order and retain fatal configure/build/test steps. Missing runner-image identity or unsupported archive paths disable persistence and export `VCPKG_BINARY_SOURCES=clear`, which prevents run-vcpkg's removed `x-gha` default from being selected. Missing GitHub file-command channels fail explicitly. diff --git a/tests/ci/README_CN.md b/tests/ci/README_CN.md index 261bc56e3..6411201c4 100644 --- a/tests/ci/README_CN.md +++ b/tests/ci/README_CN.md @@ -17,6 +17,7 @@ Nimbus 检查单独验证生成文档站内的链接与锚点,不包含根 REA vcpkg 缓存测试覆盖 Windows/Unix 归档路径、依赖和执行机镜像变化引起的 key 变化、 缓存配置导出、目录不可用,以及明确的无缓存构建路径。C++ CI 通过 Actions 缓存本地依赖包归档, 每个包的 ABI 仍由 vcpkg 校验。未命中缓存或缓存服务不可用时,依赖构建和测试照常执行; -只有任务中的测试全部通过后才保存归档。缺少执行机镜像信息或归档路径不受支持时, +归档在 CMake 配置成功后、ark 编译和测试之前保存;后续编译或测试失败仍会使任务失败。 +工作流检查覆盖该顺序,并确保配置、编译和测试失败时不会忽略错误。缺少执行机镜像信息或归档路径不受支持时, 关闭持久缓存并导出 `VCPKG_BINARY_SOURCES=clear`,避免 run-vcpkg 再选择已移除的 `x-gha` 默认值。 缺少 GitHub 文件命令通道时明确失败。不缓存项目的 C++ 编译产物、依赖安装目录、凭据或编译器缓存。 diff --git a/tests/ci/cache-workflow.test.mjs b/tests/ci/cache-workflow.test.mjs new file mode 100644 index 000000000..ae5de2829 --- /dev/null +++ b/tests/ci/cache-workflow.test.mjs @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const workflow = readFileSync(new URL('../../.github/workflows/ci.yml', import.meta.url), 'utf8').replaceAll('\r\n', '\n'); +const profiles = [ + ['network', 'Configure network regressions', 'ci-build', ['check-network']], + ['applications', 'Configure applications', 'ci-build', ['check-core']], + ['database', 'Configure native database development tests', 'ci-database', + ['check-database', 'check-database-benchmark']], +]; + +// Read this workflow's job/step indentation; this is not a general YAML parser. +function jobSteps(name) { + const jobs = workflow.split(/(?=^ [a-z][a-z0-9_-]*:$)/m); + const job = jobs.find(block => block.startsWith(` ${name}:\n`)); + assert.ok(job, `missing job ${name}`); + const [settings, ...steps] = job.split(/(?=^ - )/m); + assert.doesNotMatch(settings, /^ continue-on-error:/m, `${name} must remain fatal`); + assert.ok(steps.length > 0, `${name} must contain steps`); + return steps; +} + +function onlyStep(steps, predicate, description) { + const found = steps.map((step, index) => ({ step, index })).filter(({ step }) => predicate(step)); + assert.equal(found.length, 1, `expected exactly one ${description}`); + return found[0]; +} + +for (const [name, configureName, buildDirectory, checks] of profiles) { + test(`${name} saves dependency archives immediately after successful configure`, () => { + const steps = jobSteps(name); + const configure = onlyStep(steps, step => step.startsWith(` - name: ${configureName}\n`), 'configure step'); + const save = onlyStep(steps, step => step.includes('uses: actions/cache/save@v4\n'), 'archive save'); + const build = onlyStep(steps, step => step.includes(`run: cmake --build ${buildDirectory} --parallel `), 'source build'); + assert.equal(save.index, configure.index + 1, 'save only after configure has completed'); + assert.ok(save.index < build.index, 'save dependencies before compiling project sources'); + assert.match(save.step, /^ if: \$\{\{ success\(\) && steps\.vcpkg-cache\.outputs\.enabled == 'true' && steps\.vcpkg-restore\.outputs\.cache-hit != 'true' \}\}$/m); + assert.match(save.step, /^ continue-on-error: true$/m); + assert.match(save.step, /^ path: \$\{\{ steps\.vcpkg-cache\.outputs\.directory \}\}$/m); + assert.match(save.step, /^ key: \$\{\{ steps\.vcpkg-cache\.outputs\.key \}\}$/m); + }); + + test(`${name} still requires configure, compilation and every regression command`, () => { + const steps = jobSteps(name); + const configure = onlyStep(steps, step => step.startsWith(` - name: ${configureName}\n`), 'configure step'); + const build = onlyStep(steps, step => step.includes(`run: cmake --build ${buildDirectory} --parallel `), 'source build'); + const required = [configure, build, ...checks.map(target => onlyStep(steps, + step => step.includes(`run: cmake --build ${buildDirectory} --target ${target}\n`), target))]; + assert.match(configure.step, /^ cmake -S \. -B /m); + for (const { step } of required) { + assert.doesNotMatch(step, /^ continue-on-error:/m, 'configure/build/test failure must fail the job'); + assert.doesNotMatch(step, /^ if:/m, 'configure/build/tests must retain their default success condition'); + } + for (const check of required.slice(2)) { + assert.ok(build.index < check.index, 'compile before running regressions'); + } + }); +} From d00746699e028e53d4b160e45550ca36d2f4a087 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 19:43:56 +0800 Subject: [PATCH 4/6] ci(workflow): let active runs finish before queued updates --- .github/workflows/ci.yml | 2 +- tests/ci/README.md | 8 ++++++++ tests/ci/README_CN.md | 6 ++++++ tests/ci/workflow-concurrency.test.mjs | 28 ++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/ci/workflow-concurrency.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f24cebe42..a1628e1e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ permissions: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: scope: diff --git a/tests/ci/README.md b/tests/ci/README.md index c8bb47b16..2a0a0ea64 100644 --- a/tests/ci/README.md +++ b/tests/ci/README.md @@ -11,6 +11,14 @@ C++ rebuild or database is required. They verify complete rename-aware path deci safe fallback to the full C++ matrix, documentation availability and strict job results. Main pushes and manual runs always retain the full C++ matrix. +Each existing PR/ref concurrency group permits one active run and only the most +recently queued pending run. A new run waits for the active run to finish instead +of automatically cancelling it. Individual running jobs retain their configured +timeouts; those limits do not include pending time. Merge-group candidates use +their own refs. The required gate still rejects failed, cancelled or missing +results. Manual cancellation can still prevent the `always()` summary from +completing. See [GitHub concurrency rules](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency). + Root README link checks run only when a root README changes. They validate local file targets, including images, HTML `srcset` and reference links. It handles the forms used by these READMEs; it is not a complete Markdown parser. Remote URLs and heading diff --git a/tests/ci/README_CN.md b/tests/ci/README_CN.md index 6411201c4..adbece476 100644 --- a/tests/ci/README_CN.md +++ b/tests/ci/README_CN.md @@ -10,6 +10,12 @@ node --test tests/ci/*.test.mjs 覆盖包含重命名旧路径的完整文件集合、无法确定范围时执行完整 C++ 矩阵、文档站可用性, 以及各任务结果的严格校验。推送 main 和手动运行始终执行完整 C++ 矩阵。 +每个既有的 PR/引用并发分组最多执行一轮,只保留最新进入该分组的一轮等待任务。 +新运行等待旧轮结束,不再自动取消正在执行的旧轮。每个已运行的 job 保留原有超时限制, +等待时间不计入这些限制;Merge Queue 候选使用各自的引用分组。required gate 仍拒绝失败、 +取消或缺失的结果。手动取消仍可能使 `always()` 汇总无法完成。 +详见 [GitHub 并发规则](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency)。 + 只有根 README 变更时才检查其本地文件链接,包含图片、HTML `srcset` 和引用式链接。 检查器覆盖这些 README 使用的表达形式,不是完整的 Markdown 解析器;不验证远程 URL 或标题锚点。 Nimbus 检查单独验证生成文档站内的链接与锚点,不包含根 README 的标题锚点。 diff --git a/tests/ci/workflow-concurrency.test.mjs b/tests/ci/workflow-concurrency.test.mjs new file mode 100644 index 000000000..6c0f26952 --- /dev/null +++ b/tests/ci/workflow-concurrency.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const workflow = readFileSync(new URL('../../.github/workflows/ci.yml', import.meta.url), 'utf8').replaceAll('\r\n', '\n'); + +test('PR, main and merge-group runs retain the bounded per-PR/ref workflow group', () => { + const concurrency = workflow.match(/^concurrency:\n((?: [^\n]+\n)+)/m)?.[1]; + assert.equal(concurrency, + ' group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n' + + ' cancel-in-progress: false\n'); + assert.equal([...workflow.matchAll(/^ *concurrency:/gm)].length, 1, 'keep one workflow-level group'); + assert.match(workflow, /^ push:\n branches: \[main\]$/m); + assert.match(workflow, /^ pull_request:\n branches: \[main\]$/m); + assert.match(workflow, /^ merge_group:\n types: \[checks_requested\]$/m); +}); + +test('the required gate still runs after failed needs and invokes the fatal result validator', () => { + const required = workflow.split(/(?=^ [a-z][a-z0-9_-]*:$)/m) + .find(block => block.startsWith(' required:\n')); + assert.ok(required, 'retain the required gate'); + assert.match(required, /^ name: CI - required$/m); + assert.match(required, /^ if: always\(\)$/m); + assert.match(required, /^ needs: \[scope, network, applications, database, documentation, readme\]$/m); + assert.match(required, /^ CI_NEEDS_JSON: \$\{\{ toJSON\(needs\) \}\}$/m); + assert.match(required, /^ run: node \.github\/scripts\/ci-policy\.mjs gate$/m); + assert.doesNotMatch(required, /^ *continue-on-error:/m, 'gate errors must fail the job'); +}); From 79cae77b71a9e782050cd8e1584610d7a34ae43f Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 20:01:56 +0800 Subject: [PATCH 5/6] ci(workflow): avoid repeating accepted main builds --- .github/workflows/ci.yml | 2 -- README.md | 2 +- README_CN.md | 2 +- tests/ci/README.md | 6 +++++- tests/ci/README_CN.md | 4 +++- tests/ci/policy.test.mjs | 2 +- tests/ci/workflow-concurrency.test.mjs | 15 +++++++++++++-- 7 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1628e1e4..89fa9892c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,6 @@ name: CI run-name: CI - ${{ github.event_name }} - ${{ github.ref_name }} on: - push: - branches: [main] pull_request: branches: [main] merge_group: diff --git a/README.md b/README.md index fa5f2e8fc..4e53fc459 100755 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

Compose services. Model entities. Connect processes.

- CI + Merge queue CI C++20 Apache-2.0

diff --git a/README_CN.md b/README_CN.md index 664932a03..8bd12819f 100755 --- a/README_CN.md +++ b/README_CN.md @@ -12,7 +12,7 @@

组织业务服务,描述实体数据,连接应用进程。

- CI + 合并队列 CI C++20 Apache-2.0

diff --git a/tests/ci/README.md b/tests/ci/README.md index 2a0a0ea64..fe1d324c6 100644 --- a/tests/ci/README.md +++ b/tests/ci/README.md @@ -9,7 +9,11 @@ node --test tests/ci/*.test.mjs The tests use temporary local Git repositories. No GitHub API token, network service, C++ rebuild or database is required. They verify complete rename-aware path decisions, safe fallback to the full C++ matrix, documentation availability and strict job results. -Main pushes and manual runs always retain the full C++ matrix. +PRs run the checks required by their changed paths. Merge-group candidates and +manual runs retain the full C++ matrix. Protected `main` accepts only candidates +that passed their required checks; its resulting push does not repeat CI for the +same accepted tree. The separate documentation workflow still publishes on main +pushes. README badges display the merge-group workflow status. Each existing PR/ref concurrency group permits one active run and only the most recently queued pending run. A new run waits for the active run to finish instead diff --git a/tests/ci/README_CN.md b/tests/ci/README_CN.md index adbece476..0f0731042 100644 --- a/tests/ci/README_CN.md +++ b/tests/ci/README_CN.md @@ -8,7 +8,9 @@ node --test tests/ci/*.test.mjs 测试使用临时的本地 Git 仓库,不需要 GitHub API token、网络服务、C++ 重编译或数据库。 覆盖包含重命名旧路径的完整文件集合、无法确定范围时执行完整 C++ 矩阵、文档站可用性, -以及各任务结果的严格校验。推送 main 和手动运行始终执行完整 C++ 矩阵。 +以及各任务结果的严格校验。PR 按变更范围执行检查,Merge Queue 候选和手动运行保留完整 C++ 矩阵。 +受保护的 `main` 只接收已通过必需检查的合并候选;合入后的推送不再对同一代码树重复执行 CI。 +独立的文档工作流仍在 main 推送后发布文档。README 徽章展示合并候选的工作流状态。 每个既有的 PR/引用并发分组最多执行一轮,只保留最新进入该分组的一轮等待任务。 新运行等待旧轮结束,不再自动取消正在执行的旧轮。每个已运行的 job 保留原有超时限制, diff --git a/tests/ci/policy.test.mjs b/tests/ci/policy.test.mjs index 64cfb85d7..90ffa582d 100644 --- a/tests/ci/policy.test.mjs +++ b/tests/ci/policy.test.mjs @@ -68,7 +68,7 @@ test('empty, missing, truncated and invalid UTF-8 path lists never grant a light assert.equal(decision(['docs/a\nname.md']).full_cpp, true); }); -test('manual, main pushes and unsupported events retain full C++ coverage', () => { +test('events outside pull requests cannot grant a lightweight classifier result', () => { for (const eventName of ['push', 'workflow_dispatch', 'merge_group', 'unknown']) { const plan = decision(['docs/guide.md'], { eventName }); assert.equal(plan.full_cpp, true); diff --git a/tests/ci/workflow-concurrency.test.mjs b/tests/ci/workflow-concurrency.test.mjs index 6c0f26952..d0e3d9e72 100644 --- a/tests/ci/workflow-concurrency.test.mjs +++ b/tests/ci/workflow-concurrency.test.mjs @@ -4,15 +4,26 @@ import test from 'node:test'; const workflow = readFileSync(new URL('../../.github/workflows/ci.yml', import.meta.url), 'utf8').replaceAll('\r\n', '\n'); -test('PR, main and merge-group runs retain the bounded per-PR/ref workflow group', () => { +test('PR, merge-group and manual runs retain the bounded per-PR/ref workflow group', () => { const concurrency = workflow.match(/^concurrency:\n((?: [^\n]+\n)+)/m)?.[1]; assert.equal(concurrency, ' group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}\n' + ' cancel-in-progress: false\n'); assert.equal([...workflow.matchAll(/^ *concurrency:/gm)].length, 1, 'keep one workflow-level group'); - assert.match(workflow, /^ push:\n branches: \[main\]$/m); assert.match(workflow, /^ pull_request:\n branches: \[main\]$/m); assert.match(workflow, /^ merge_group:\n types: \[checks_requested\]$/m); + assert.match(workflow, /^ workflow_dispatch:$/m); +}); + +test('protected admission is validated before merge without repeating CI on a main push', () => { + assert.doesNotMatch(workflow, /^ push:/m); + const publishing = readFileSync(new URL('../../.github/workflows/docs.yml', import.meta.url), 'utf8').replaceAll('\r\n', '\n'); + assert.match(publishing, /^ push:\n branches: \[main\]$/m, 'keep documentation publication after merge'); + for (const name of ['README.md', 'README_CN.md']) { + const readme = readFileSync(new URL('../../' + name, import.meta.url), 'utf8'); + assert.match(readme, /ci\.yml\/badge\.svg\?event=merge_group/); + assert.doesNotMatch(readme, /ci\.yml\/badge\.svg\?branch=main/); + } }); test('the required gate still runs after failed needs and invokes the fatal result validator', () => { From 51f5847b8a2031ffcfa7bb87772553a48c171a5d Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 20:27:04 +0800 Subject: [PATCH 6/6] docs(ci): prioritize older pull requests for shared runners --- AGENTS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index bb82e233e..4ee276433 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,11 @@ branch or worktree when the existing checkout contains uncommitted changes. ## Verification and delivery +- When runner capacity is shared by several PRs, prioritize the oldest unblocked + PRs and their required queue candidates. Prepare newer work locally and defer + its hosted runs until older ready PRs can finish; do not repeatedly push newer + branches ahead of the backlog. Preserve active acceptance runs. A cancelled + scheduling run still needs fresh successful checks before acceptance. - Run the checks relevant to the change before publishing. Report the exact tested revision, commands, results and material limitations. - Investigate failed CI logs before changing code or retrying. Do not repeatedly