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
4 changes: 2 additions & 2 deletions .github/scripts/ci-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function isDocumentationPath(file) {
if (cppPath.test(file) || /^CMake(?:Lists\.txt|(?:User)?Presets\.json|Cache\.txt)$/i.test(name) || /\.cmake$/i.test(file) ||
dependencyName.test(name) || file.split('/').includes('.github') || /^action\.ya?ml$/i.test(name) ||
/^\.clang-(?:format|tidy)$/.test(name)) return false;
if (file === 'README.md' || file === 'README_CN.md' || pluginIndexPaths.has(file) || testGuidePaths.has(file)) return true;
if (file === 'AGENTS.md' || file === 'README.md' || file === 'README_CN.md' || pluginIndexPaths.has(file) || testGuidePaths.has(file)) return true;
return file.startsWith('docs/') && (docPath.test(file) || name === 'LICENSE' || name === '.gitignore');
}

Expand Down Expand Up @@ -98,7 +98,7 @@ export function selectScope({ eventName, files, site, enumerationFailed = false
const scopedEvent = eventName === 'pull_request' || eventName === 'merge_group';
const docsOnly = scopedEvent && nonempty && site.available && paths.every(isDocumentationPath);
const readme = paths.some(file => file === 'README.md' || file === 'README_CN.md');
const docsTouched = readme || paths.some(file => file.startsWith('docs/') || pluginIndexPaths.has(file) || testGuidePaths.has(file));
const docsTouched = readme || paths.some(file => file === 'AGENTS.md' || file.startsWith('docs/') || pluginIndexPaths.has(file) || testGuidePaths.has(file));
const ciTouched = paths.some(file => file.startsWith('.github/') || file.startsWith('tests/ci/'));
const siteReferenced = site.present || paths.some(file => file.startsWith('docs/site/'));
const docs = siteReferenced && (eventName !== 'pull_request' || !known || docsTouched || ciTouched);
Expand Down
13 changes: 13 additions & 0 deletions docs/project/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ npm run check --prefix docs/site
The site provides language switching, search, code highlighting and plain Markdown endpoints for AI readers. Pull requests run the documentation checks without publishing. After a change is merged into `main`, the documentation workflow builds and publishes GitHub Pages; maintainers can also trigger it manually. Node.js 22.12+ is needed for the documentation build; it is not needed to compile ark or its C++ tests.


## CI documentation scope

The exact root `AGENTS.md` file is documentation. When the changed range is known,
nonempty and confined to recognized documentation, and the Nimbus site is available,
both source PRs and merge-group candidates require the documentation checks and
`CI - required`, while native jobs are explicitly skipped. Nimbus checks the site;
root README link checks run only when a root README changes.

Nested unknown `AGENTS.md` paths, similarly named code, mixed source changes,
unavailable ranges or an unavailable site retain native checks. Manual runs keep
the full native matrix. Changes to the CI policy itself still require native
acceptance; this documentation exception does not change that requirement.

## CI dependency archive cache

CI restores vcpkg package archives from the runner temporary directory with
Expand Down
10 changes: 10 additions & 0 deletions docs/project/contributing_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ npm run check --prefix docs/site
站点提供语言切换、搜索、代码高亮,以及供 AI 阅读的纯 Markdown 接口。PR 只运行文档检查,不发布站点。变更合并到 `main` 后,文档工作流构建并发布 GitHub Pages;维护者也可手动触发。文档构建需要 Node.js 22.12+;编译 ark 与 C++ 测试不需要 Node.js。


## CI 文档范围

根目录的精确文件 `AGENTS.md` 归为文档。改动范围已知且非空、仅包含已识别的文档,
并且 Nimbus 文档站可用时,源分支 PR 与合并候选均要求文档检查和 `CI - required`,
原生任务明确跳过。Nimbus 检查文档站;只有根 README 变化时才检查其链接。

嵌套的未知 `AGENTS.md` 路径、名称相近的代码、混合源码变更、范围不可用或文档站不可用时,
仍保留原生检查。手动运行保留完整原生矩阵。CI 策略本身的变更仍须完成原生验收,
这项文档例外不会改变该要求。

## CI 依赖包归档缓存

CI 使用 `actions/cache` 恢复执行机临时目录中的 vcpkg 包归档。
Expand Down
8 changes: 8 additions & 0 deletions tests/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ their own refs. Independent groups do not enforce PR creation order. The require
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).

The exact root `AGENTS.md` path also qualifies as documentation. A known,
nonempty range confined to that file and other recognized documentation selects
Nimbus and the final required gate on both PRs and merge-group candidates, with
native jobs explicitly skipped when the site is available. This does not extend
the root README link check or exempt nested `AGENTS.md` files, similarly named
code, mixed source changes or uncertain ranges. Manual runs retain the full
native matrix. This CI policy change itself still requires native acceptance.

Within `src/`, only the exact plugin index paths `src/plugins/README.md` and
`src/plugins/README_CN.md` additionally qualify as documentation. Changes confined
to these indexes select the Nimbus check on PRs and documentation-only queue
Expand Down
6 changes: 6 additions & 0 deletions tests/ci/README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ README 徽章展示合并候选的工作流状态。
required gate 仍拒绝失败、取消或缺失的结果。手动取消仍可能使 `always()` 汇总无法完成。
详见 [GitHub 并发规则](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency)。

根目录的精确路径 `AGENTS.md` 也归为文档。范围已知且非空、文档站可用时,
仅修改该文件和其他已识别文档的 PR 与合并候选均要求 Nimbus 检查和最终必需门禁,
并明确跳过原生任务。这不会扩展根 README 链接检查,也不会豁免嵌套的 `AGENTS.md`、
名称相近的代码、混合源码变更或不确定范围。手动运行仍保留完整原生矩阵。
本次 CI 策略变更自身仍须完成原生验收。

`src/` 下仅将两个精确的插件索引路径 `src/plugins/README.md` 与 `src/plugins/README_CN.md` 归为文档。
文档站可用时,只修改这两个索引的 PR 与纯文档合并候选会选择 Nimbus 检查。
这不会将根 README 链接检查扩展到插件索引;Nimbus 验证生成的文档站,不直接检查索引文件。
Expand Down
5 changes: 3 additions & 2 deletions tests/ci/module-validation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ test('actual workflow output bindings provide complete plans to the required gat
const outputs = [...jobBlock('scope').matchAll(/^ (\w+): \$\{\{ steps\.scope\.outputs\.(\w+) \}\}$/gm)];
assert.ok(outputs.length > 0);
for (const scope of [plan(['tests/network/rudp.cpp']), plan(['include/ark/core/module.hpp']),
plan(['README.md']), plan(['tests/database/redis_service.cpp'], 'merge_group'),
plan(['README.md']), plan(['AGENTS.md']), plan(['AGENTS.md'], 'merge_group'),
plan(['tests/database/redis_service.cpp'], 'merge_group'),
plan(['README.md'], 'merge_group'), plan([], 'workflow_dispatch'), plan([], 'pull_request')]) {
const needs = results(scope);
needs.scope.outputs = Object.fromEntries(outputs.map(([, name, source]) => [name, String(scope[source])]));
Expand Down Expand Up @@ -113,7 +114,7 @@ test('public core headers, build/dependency/CI inputs and unknown paths retain e
'3rdparty/spdlog', 'ports/asio/portfile.cmake', '.github/workflows/ci.yml',
'config/timers.app.toml', 'resources/excel/item.xlsx', 'resources/photon/proto/protocol.proto',
'tools/gen-config.sh', 'deployment/scripts/run.sh',
'.github/scripts/ci-policy.mjs', 'tests/ci/module-validation.test.mjs', 'AGENTS.md', '.clang-format',
'.github/scripts/ci-policy.mjs', 'tests/ci/module-validation.test.mjs', 'src/AGENTS.md', '.clang-format',
'application/unknown.bin', 'application/build.cmake', 'applications/main.cpp', 'src/app/main.cpp',
'src/plugins/new_plugin/new_module.cpp', 'src/plugins/net/unknown.bin', 'scripts/new-check.sh',
'docs/example.cpp', 'docs/site/package-lock.json', 'docs/unknown.bin', 'LICENSE']) {
Expand Down
93 changes: 93 additions & 0 deletions tests/ci/policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,99 @@ test('only recognized documentation paths qualify for the lightweight plan', ()
assert.equal(decision(['README.md', 'docs/guide.md']).docs_check, true);
});

test('root AGENTS.md requires documentation and the final gate without native work at both scoped stages', () => {
assert.equal(isDocumentationPath('AGENTS.md'), true);
for (const eventName of ['pull_request', 'merge_group']) {
for (const files of [['AGENTS.md'], ['AGENTS.md', 'docs/guide.md']]) {
const scope = decision(files, { eventName });
assert.equal(scope.validation_stage, 'documentation');
assert.equal(scope.reason, 'documentation-only');
assert.equal(scope.docs_check, true);
assert.equal(scope.readme_check, false);
for (const key of ['native_check', 'format_check', 'network_check', 'applications_check', 'database_check']) {
assert.equal(scope[key], false, `${eventName}/${key}`);
}
validateGate(results(scope));
for (const outcome of ['skipped', 'failure', 'cancelled', undefined]) {
const invalid = results(scope);
invalid.validation.outputs.documentation_outcome = outcome;
assert.throws(() => validateGate(invalid), `${eventName}/${outcome}`);
}
for (const result of ['skipped', 'failure', 'cancelled', undefined]) {
const invalid = results(scope);
invalid.validation.result = result;
assert.throws(() => validateGate(invalid), `${eventName}/validation/${result}`);
}
}
assert.equal(decision(['AGENTS.md', 'README.md'], { eventName }).readme_check, true);
}
});

test('an exact root AGENTS.md Git change selects documentation through the real PR and merge-group classifier', t => {
const repo = repository(t);
const base = addSite(repo);
repo.write('AGENTS.md', '# Contributor and agent instructions\n');
const head = repo.commit();
assert.deepEqual(changedPaths({ root: repo.root, base, head }), ['AGENTS.md']);
for (const eventName of ['pull_request', 'merge_group']) {
const payload = eventName === 'pull_request' ? { pull_request: { base: { sha: base }, head: { sha: head } } } :
{ merge_group: { base_sha: base, head_sha: head } };
const { plan: scope, output } = classifyEvent(repo, eventName, payload);
assert.equal(scope.validation_stage, 'documentation');
assert.equal(scope.docs_check, true);
assert.equal(scope.readme_check, false);
for (const key of ['native_check', 'format_check', 'network_check', 'applications_check', 'database_check']) {
assert.equal(scope[key], false, `${eventName}/${key}`);
assert.match(output, new RegExp(`^${key}=false$`, 'm'));
}
validateGate(results(scope));
}
});

test('AGENTS neighbors and nested unknown paths cannot inherit the root documentation exception', () => {
const paths = ['agents.md', 'AGENTS.MD', 'AGENTS_CN.md', 'AGENTS.md.bak', 'AGENTS.md.js',
'AGENTS.cpp', 'AGENTS.md.hpp', 'nested/AGENTS.md', 'src/AGENTS.md', 'tests/AGENTS.md',
'src/plugins/net/AGENTS.md', '.github/AGENTS.md', 'docs/AGENTS.cpp', './AGENTS.md',
'nested/../AGENTS.md', 'nested//AGENTS.md', 'nested\\AGENTS.md'];
for (const file of paths) {
assert.equal(isDocumentationPath(file), false, file);
for (const eventName of ['pull_request', 'merge_group']) {
const scope = decision(['AGENTS.md', file], { eventName });
assert.equal(scope.native_check, true, `${eventName}/${file}`);
assert.equal(scope.format_check, true);
assert.equal(scope.docs_check, true);
for (const job of ['network', 'applications', 'database']) assert.equal(scope[`${job}_check`], true);
validateGate(results(scope));
}
}
});

test('AGENTS mixed with code keeps source module selection and every merge-candidate native module', () => {
for (const eventName of ['pull_request', 'merge_group']) {
const scope = decision(['AGENTS.md', 'tests/core/containers.cpp'], { eventName });
assert.equal(scope.native_check, true);
assert.equal(scope.docs_check, true);
assert.equal(scope.format_check, true);
assert.equal(scope.validation_stage, eventName === 'pull_request' ? 'modules' : 'full');
for (const job of ['network', 'applications', 'database']) {
assert.equal(scope[`${job}_check`], eventName !== 'pull_request' || job === 'applications');
}
validateGate(results(scope));
}
});

test('AGENTS never exempts manual events, unavailable sites or uncertain changed paths from native checks', () => {
for (const extra of [{ eventName: 'workflow_dispatch' }, { enumerationFailed: true },
{ site: { present: true, available: false } }, { site: { present: false, available: false } }]) {
const scope = decision(['AGENTS.md'], extra);
assert.equal(scope.native_check, true);
assert.equal(scope.validation_stage, 'full');
for (const job of ['network', 'applications', 'database']) assert.equal(scope[`${job}_check`], true);
if (extra.site?.present) assert.equal(scope.docs_check, true);
validateGate(results(scope));
}
});

test('only the two exact plugin indexes select documentation and keep root README checks separate', () => {
const indexes = ['src/plugins/README.md', 'src/plugins/README_CN.md'];
for (const eventName of ['pull_request', 'merge_group']) {
Expand Down
Loading