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
62 changes: 54 additions & 8 deletions .github/scripts/ci-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,35 @@ export function selectScope({ eventName, files, site, enumerationFailed = false
};
}

export function validateGate(needs, eventName) {
export function admissionState(eventName, event) {
if (!['pull_request', 'merge_group', 'workflow_dispatch'].includes(eventName)) throw new Error('unsupported admission event');
if (eventName !== 'pull_request') return 'ready';
if (typeof event?.pull_request?.draft !== 'boolean') throw new Error('missing or invalid pull request draft metadata');
return event.pull_request.draft ? 'deferred' : 'ready';
}

export class AdmissionDeferredError extends Error {
constructor() {
super('Draft admission deferred: native acceptance was not executed in this run; this PR is not admitted. The root scheduler must release it for fresh ready_for_review validation.');
this.name = 'AdmissionDeferredError';
}
}

export function validateGate(needs, eventName, admission = 'ready') {
if (!['ready', 'deferred'].includes(admission)) throw new Error('invalid admission state');
if (admission === 'deferred') {
if (eventName !== 'pull_request') throw new Error('only draft pull requests may defer admission');
throw new AdmissionDeferredError();
}
validateResults(needs, eventName, false);
}

export function validatePreview(needs, eventName, admission) {
if (eventName !== 'pull_request' || admission !== 'deferred') throw new Error('preview requires a genuine draft pull request');
validateResults(needs, eventName, true);
}

function validateResults(needs, eventName, preview) {
if (!needs || needs.scope?.result !== 'success') throw new Error('scope job did not succeed');
if (!['pull_request', 'merge_group', 'workflow_dispatch'].includes(eventName) ||
needs.scope.outputs?.event_name !== eventName) throw new Error('scope event does not match this run');
Expand Down Expand Up @@ -170,7 +198,7 @@ export function validateGate(needs, eventName) {
network: flags.network_check, applications: flags.applications_check, database: flags.database_check,
};
for (const [job, mustRun] of Object.entries(required)) {
const expected = mustRun ? 'success' : 'skipped';
const expected = !preview && mustRun ? 'success' : 'skipped';
if (needs[job]?.result !== expected) throw new Error(`${job}: expected ${expected}, got ${needs[job]?.result ?? 'missing'}`);
}
if (needs.validation?.result !== 'success') throw new Error('validation job did not succeed');
Expand All @@ -187,10 +215,12 @@ function classify() {
const root = process.cwd();
const eventName = process.env.GITHUB_EVENT_NAME;
let files;
let admission = eventName === 'pull_request' ? 'unknown' : 'ready';
let enumerationFailed = false;
try {
const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
if (eventName === 'pull_request') {
admission = admissionState(eventName, event);
files = changedPaths({ root, base: event.pull_request?.base?.sha, head: event.pull_request?.head?.sha });
} else if (eventName === 'merge_group') {
files = changedPaths({ root, base: event.merge_group?.base_sha, head: event.merge_group?.head_sha, pullRequest: false });
Expand All @@ -206,23 +236,39 @@ function classify() {
const output = Object.entries(decision).map(([key, value]) => `${key}=${value}\n`).join('');
if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, output);
const summary = `CI scope: ${decision.reason}\n\n${output}\n` +
(admission === 'deferred' ? 'Admission deferred: native checks will not execute. Light checks do not admit this draft or establish native acceptance.\n' :
admission === 'unknown' ? 'Invalid draft metadata: full scope fallback; the required gate must reject admission.\n' : '') +
(!decision.site_available ? (decision.docs_check ? 'The required Nimbus site is missing or invalid; its job must fail.\n' :
'Nimbus is not introduced in this checkout; site validation is explicitly not required and all native modules remain required.\n') : '');
if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
console.log(JSON.stringify({ ...decision, file_count: files?.length ?? null }, null, 2));
console.log(JSON.stringify({ ...decision, admission, file_count: files?.length ?? null }, null, 2));
}

if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
try {
if (process.argv[2] === 'classify') classify();
else if (process.argv[2] === 'gate') {
validateGate(JSON.parse(process.env.CI_NEEDS_JSON), process.env.GITHUB_EVENT_NAME);
console.log('Every required job and validation step succeeded; only explicitly unnecessary checks were skipped.');
else if (['gate', 'preview'].includes(process.argv[2])) {
const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
const admission = admissionState(process.env.GITHUB_EVENT_NAME, event);
const needs = JSON.parse(process.env.CI_NEEDS_JSON);
if (process.argv[2] === 'preview') {
validatePreview(needs, process.env.GITHUB_EVENT_NAME, admission);
const summary = 'Draft preview only: scope and selected light checks succeeded. Native acceptance was not executed in this run; this PR is not admitted. This preview does not report CI - required success. Root scheduling must release it for fresh ready_for_review checks before admission.\n';
if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
console.log(summary.trim());
} else {
validateGate(needs, process.env.GITHUB_EVENT_NAME, admission);
console.log('Every required job and validation step succeeded; only explicitly unnecessary checks were skipped.');
}
} else if (process.argv[2] === 'require-site') {
if (!inspectSite(process.cwd()).available) throw new Error('Required Nimbus package, lockfile or check script is unavailable');
} else throw new Error('Expected classify, gate or require-site');
} else throw new Error('Expected classify, gate, preview or require-site');
} catch (error) {
console.error(error.message);
if (error instanceof AdmissionDeferredError && process.env.GITHUB_STEP_SUMMARY) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY,
'Admission deferred (not a passing check). Native acceptance was not executed in this run; this draft is not admitted. Light-check results remain separate. Root scheduling must release it for fresh ready_for_review checks.\n');
}
console.error(`${error instanceof AdmissionDeferredError ? '[DRAFT_ADMISSION_DEFERRED] ' : ''}${error.message}`);
process.exitCode = 1;
}
}
13 changes: 7 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ run-name: CI - ${{ github.event_name }} - ${{ github.ref_name }}
on:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
merge_group:
types: [checks_requested]
workflow_dispatch:
Expand Down Expand Up @@ -91,7 +92,7 @@ jobs:
network:
name: Network - ${{ matrix.os }} - Boost - ${{ matrix.sanitizer }}
needs: scope
if: ${{ !cancelled() && (needs.scope.result != 'success' || needs.scope.outputs.network_check != 'false') }}
if: ${{ !cancelled() && !(github.event_name == 'pull_request' && toJSON(github.event.pull_request.draft) == 'true') && (needs.scope.result != 'success' || needs.scope.outputs.network_check != 'false') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
Expand Down Expand Up @@ -156,7 +157,7 @@ jobs:
applications:
name: Applications - ${{ matrix.os }} - C++20
needs: scope
if: ${{ !cancelled() && (needs.scope.result != 'success' || needs.scope.outputs.applications_check != 'false') }}
if: ${{ !cancelled() && !(github.event_name == 'pull_request' && toJSON(github.event.pull_request.draft) == 'true') && (needs.scope.result != 'success' || needs.scope.outputs.applications_check != 'false') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
Expand Down Expand Up @@ -220,7 +221,7 @@ jobs:
database:
name: Database - ${{ matrix.os }} - ${{ matrix.asio }}
needs: scope
if: ${{ !cancelled() && (needs.scope.result != 'success' || needs.scope.outputs.database_check != 'false') }}
if: ${{ !cancelled() && !(github.event_name == 'pull_request' && toJSON(github.event.pull_request.draft) == 'true') && (needs.scope.result != 'success' || needs.scope.outputs.database_check != 'false') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
Expand Down Expand Up @@ -292,7 +293,7 @@ jobs:
- name: Run local database benchmark self-tests
run: cmake --build ci-database --target check-database-benchmark
required:
name: CI - required
name: ${{ github.event_name == 'pull_request' && toJSON(github.event.pull_request.draft) == 'true' && 'CI - draft preview' || 'CI - required' }}
if: always()
needs: [scope, network, applications, database, validation]
runs-on: ubuntu-latest
Expand All @@ -304,7 +305,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 26
- name: Require successful builds and regressions
- name: Validate acceptance or draft preview
env:
CI_NEEDS_JSON: ${{ toJSON(needs) }}
run: node .github/scripts/ci-policy.mjs gate
run: node .github/scripts/ci-policy.mjs ${{ github.event_name == 'pull_request' && toJSON(github.event.pull_request.draft) == 'true' && 'preview' || 'gate' }}
116 changes: 104 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ branch or worktree when the existing checkout contains uncommitted changes.
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.
does not validate that combination. Admit eligible PRs in the creation order
defined below.
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.
Expand All @@ -38,11 +39,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.
- Follow the root scheduler and PR creation order defined below when runner
capacity is shared. Prepare newer work locally and defer hosted admission;
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 Expand Up @@ -81,12 +82,16 @@ branch or worktree when the existing checkout contains uncommitted changes.
- 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/<name>/`; 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.
in application repositories. The approved next plugin layout co-locates public
headers, private declarations and implementation files inside each plugin;
do not reintroduce a mandatory per-plugin include/src split. Keep public header
mappings explicit and independent from physical paths. Concrete implementation
types may live in cpp files when registration and tests no longer need their
definitions. Public-header staging/installation and creation-macro replacements
require the agreed design before implementation; do not silently change ABI,
lifecycle or ownership. Retain independent plugin targets and optional builds.
Verify accepted source before claiming the new layout or an installed SDK is
available. Hot-update and Registry design discussions remain paused.

## Documentation and releases

Expand All @@ -104,3 +109,90 @@ branch or worktree when the existing checkout contains uncommitted changes.
- 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.

## Task execution and CI admission

- Use `gh` CLI for GitHub operations; do not replace it with direct REST/GraphQL
requests or `gh api`. Normal Git fetch/push commands remain appropriate.
- The root agent alone observes CI and controls PR readiness and queue admission.
Order admission by PR creation time: finish the oldest unfinished PR before
admitting newer ones. A newer PR becoming green first is not permission to
overtake an older PR. Account for shared runner capacity across ark and arknet.
- Continue independent implementation and local validation in parallel. Publish
focused modules without starting newer native matrices ahead of the backlog.
Use drafts only when the deployed workflow actually defers draft native jobs;
otherwise retain a reviewable local branch until admission is available.
- Do not enable automatic merge for held PRs. Release them in creation order;
after required checks pass, squash through the protected merge queue promptly.
Do not repeatedly ask for already-authorized merges or bypass any check.
- When a draft-preview workflow is deployed, require its fresh `ready_for_review`
run to succeed at the intended head before enabling auto-merge or queue admission.
Preview success, an absent required context, or older green checks on the same
SHA do not establish fresh admission. Verify the hosted behavior before claiming it works.
- Give jobs unique, descriptive names. Consolidate compatible jobs/steps to reduce
setup time without dropping required platforms, protocols or verification.
- Preserve active acceptance runs. If correcting scheduling requires cancellation,
record its reason and arrange fresh acceptance; cancellation never counts as
a successful test. GitHub Actions concurrency groups do not by themselves
enforce PR-creation order.
- Keep one observer per PR batch. Batch status reads, back off unchanged polling,
and let scripts collect logs and exit codes. Read summaries and relevant failure
excerpts, not entire repeated logs. Communicate completion, failures, interface
changes and decisions; do not repeatedly ask agents for unchanged progress.
- Delegate only concrete independent deliveries with a scope and acceptance
criteria. Do not wake finished agents just to keep them busy. Record a short
handoff at module completion and preserve unrelated work in dirty checkouts.
- Current-session monitoring does not authorize recurring/background automation.
Stop owned observers before ending the session unless separately authorized.

## Keep documentation synchronized

- Resolve legacy page aliases to their canonical source before editing. Nimbus
may skip retired Markdown files even when they remain in Git. Check the emitted
HTML and Markdown for the changed content; a successful build alone does not
prove that an edited source is published.
- Treat documentation as part of each functional change. Review both languages,
README, feature/support tables, API and client/server examples, configuration,
installation, lifecycle, navigation and affected performance/validation reports.
Update the relevant material in the same PR; do not routinely defer it.
- Describe the final supported behavior. Remove stale TODO/experimental/private
development-layer claims when accepted code supersedes them, and avoid
redundant "implemented", "retest" or "latest test" labels. Keep actual future
capabilities clearly marked TODO; do not imply they already work.
- Keep ark and arknet boundaries clear. Cross-link the other project's accepted
public APIs rather than copying them or claiming pending integration is merged.
Never attribute later main changes or unpublished results to an older release.
- Run the relevant Nimbus/link checks. After publication, verify the Pages run
succeeded for the intended source revision and inspect affected published pages.
A local preview, merged PR and deployed site are distinct evidence states.
- Use the full names AddressSanitizer, UndefinedBehaviorSanitizer and
ThreadSanitizer in explanatory prose. Preserve command flags, paths, workflow
identifiers and machine-readable fields. Call Asio/Beast dependencies rather
than backends; capitalize Standalone Asio in prose.
- Prefer SVG or Mermaid for editable diagrams and charts. Show labeled units and
readable legends, retain useful error bars, and avoid repeating chart contents
in long prose. Keep the README concise, with properly formatted dependency lists;
detailed guidance belongs in the bilingual documentation site.

## Evidence and performance boundaries

- Keep raw measurement JSON, packet captures, logs and credentials local, outside
version control. Do not upload them as release assets, CI artifacts or public
downloads without separate explicit authorization for those exact files.
- Extend existing doctest correctness tests rather than adding a Boost test
dependency. Prefer standard C++20 facilities over obsolete wrappers/macros when
behavior and ownership can be preserved; keep necessary platform/ABI boundaries.
- Maintain compile-and-run C++ tests and benchmarks in their project directories;
do not require Python or Ruby as the user's test entry point. Shell scripts may
orchestrate internal batches. Do not replace permanent tests with temporary ones.
- Measure performance locally before writing reports. Identify machine, OS,
compiler, dependency versions, source/binary identity, duration and units.
Compare payload sizes, in-flight batches, repeated runs, coroutine/callback
interfaces and IO/thread models, including one context/one thread and one
context/multiple threads. Report throughput, latency and CPU utilization;
100% CPU denotes one logical core, with the measurement scope stated.
- Retain the final valid results and material limitations, not a retesting diary.
Functional checks, sanitizer checks, performance measurements and deployment
capacity are separate claims. Loopback tests do not prove production PCU or
cross-host capacity; successful controls do not establish an unresolved root
cause. Do not extend timeouts or rerun to green to conceal failures.
Loading
Loading