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
36 changes: 17 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -15,7 +13,7 @@ permissions:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
cancel-in-progress: false

jobs:
scope:
Expand Down Expand Up @@ -130,20 +128,20 @@ 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
uses: actions/cache/save@v4
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
Expand Down Expand Up @@ -197,16 +195,16 @@ 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
uses: actions/cache/save@v4
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
Expand Down Expand Up @@ -275,19 +273,19 @@ 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
uses: actions/cache/save@v4
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()
Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<p align="center">Compose services. Model entities. Connect processes.</p>

<p align="center">
<a href="https://github.com/OpenArkStudio/ark/actions/workflows/ci.yml"><img src="https://github.com/OpenArkStudio/ark/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
<a href="https://github.com/OpenArkStudio/ark/actions/workflows/ci.yml"><img src="https://github.com/OpenArkStudio/ark/actions/workflows/ci.yml/badge.svg?event=merge_group" alt="Merge queue CI"></a>
<a href="CMakeLists.txt"><img src="https://img.shields.io/badge/C%2B%2B-20-5267df" alt="C++20"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-5267df" alt="Apache-2.0"></a>
</p>
Expand Down
2 changes: 1 addition & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<p align="center">组织业务服务,描述实体数据,连接应用进程。</p>

<p align="center">
<a href="https://github.com/OpenArkStudio/ark/actions/workflows/ci.yml"><img src="https://github.com/OpenArkStudio/ark/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
<a href="https://github.com/OpenArkStudio/ark/actions/workflows/ci.yml"><img src="https://github.com/OpenArkStudio/ark/actions/workflows/ci.yml/badge.svg?event=merge_group" alt="合并队列 CI"></a>
<a href="CMakeLists.txt"><img src="https://img.shields.io/badge/C%2B%2B-20-5267df" alt="C++20"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-5267df" alt="Apache-2.0"></a>
</p>
Expand Down
18 changes: 16 additions & 2 deletions tests/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@ 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
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
Expand All @@ -21,7 +33,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.
Expand Down
13 changes: 11 additions & 2 deletions tests/ci/README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ node --test tests/ci/*.test.mjs

测试使用临时的本地 Git 仓库,不需要 GitHub API token、网络服务、C++ 重编译或数据库。
覆盖包含重命名旧路径的完整文件集合、无法确定范围时执行完整 C++ 矩阵、文档站可用性,
以及各任务结果的严格校验。推送 main 和手动运行始终执行完整 C++ 矩阵。
以及各任务结果的严格校验。PR 按变更范围执行检查,Merge Queue 候选和手动运行保留完整 C++ 矩阵。
受保护的 `main` 只接收已通过必需检查的合并候选;合入后的推送不再对同一代码树重复执行 CI。
独立的文档工作流仍在 main 推送后发布文档。README 徽章展示合并候选的工作流状态。

每个既有的 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 或标题锚点。
Expand All @@ -17,6 +25,7 @@ Nimbus 检查单独验证生成文档站内的链接与锚点,不包含根 REA
vcpkg 缓存测试覆盖 Windows/Unix 归档路径、依赖和执行机镜像变化引起的 key 变化、
缓存配置导出、目录不可用,以及明确的无缓存构建路径。C++ CI 通过 Actions 缓存本地依赖包归档,
每个包的 ABI 仍由 vcpkg 校验。未命中缓存或缓存服务不可用时,依赖构建和测试照常执行;
只有任务中的测试全部通过后才保存归档。缺少执行机镜像信息或归档路径不受支持时,
归档在 CMake 配置成功后、ark 编译和测试之前保存;后续编译或测试失败仍会使任务失败。
工作流检查覆盖该顺序,并确保配置、编译和测试失败时不会忽略错误。缺少执行机镜像信息或归档路径不受支持时,
关闭持久缓存并导出 `VCPKG_BINARY_SOURCES=clear`,避免 run-vcpkg 再选择已移除的 `x-gha` 默认值。
缺少 GitHub 文件命令通道时明确失败。不缓存项目的 C++ 编译产物、依赖安装目录、凭据或编译器缓存。
59 changes: 59 additions & 0 deletions tests/ci/cache-workflow.test.mjs
Original file line number Diff line number Diff line change
@@ -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');
}
});
}
2 changes: 1 addition & 1 deletion tests/ci/policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions tests/ci/workflow-concurrency.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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, 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, /^ 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', () => {
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');
});
Loading