Skip to main content
OpenClaw CI runs on pushes to main that change a path outside **/*.md and docs/**, on every non-draft pull request, and on manual dispatch. Docs-only main pushes skip CI; mixed docs and code pushes still run it. Pull-request docs scoping is unchanged. Canonical main pushes use a two-slot pipeline keyed by run-number parity, so at most two integration runs overlap. Each slot is non-canceling and keeps one coalesced pending tip: a new merge replaces that slot’s older pending run instead of canceling work that already registered a Blacksmith matrix. Runs in the two slots can complete out of order; exact-head consumers remain bound to their requested SHA and are unaffected. Pull requests still cancel superseded heads, and manual dispatches use isolated groups. Draft no-op events use per-run isolated groups before job gating, so a delayed draft event cannot displace pending or running ready-for-review CI. converted_to_draft keeps the PR-wide group to cancel earlier CI while skipping its own jobs. Explicit workflow cancellation and manual dispatch behavior are unchanged; draft isolation adds no downstream automatic recovery. preflight classifies the diff and turns expensive lanes off when only unrelated areas changed. Ordinary manual workflow_dispatch runs intentionally bypass smart scoping and fan out the full graph for release candidates and broad validation. Exact-head release_gate fallbacks retain the pull request’s macOS, iOS smoke, and native generated-locale scope instead of forcing unrelated Apple lanes or locale parity. Native source verification still runs. Android lanes stay opt-in through include_android (or the release_gate input). Release-only plugin coverage lives in the separate Plugin Prerelease workflow and only runs from Full Release Validation or an explicit manual dispatch. Scheduled QA runs nightly at 04:41 UTC. Its live runtime job runs the gateway-restart-full-access-live scenario with openai/gpt-5.6-luna alongside the three-restart replay-safety scenario. The Full Access check must preserve shell access and delegation without repeating the interrupted side effect; failure fails the job. Both scenarios run serially and retain their reports in the job’s uploaded artifacts.

Pipeline overview

Test runtime selection

Linux test shards select Bun through scripts/lib/ci-test-runtime.mts. The ordinary and isolated unit-fast lanes partition their existing file inventories: files with known Bun failures or additional skips stay on Node, and the compatible remainder runs on Bun. Those Node files still execute; they are not excluded from CI. The complete fake-timer lane also supports Bun. UI and other families retain Node until they pass on the pinned fork within their existing CI resource budgets. Precise PR targets use the existing test-project planner to find their owners. Mixed or ambiguous selections retain Node, and no tests are removed from the selected inventory. Pull requests and their release-gate fallback run compatible selections on Bun. Ordinary manual CI, including Full Release Validation’s normal_ci child, runs the complete original selection on Node and its compatible portion on Bun within the same job and worker slot. Other selections run on Node. Main pushes retain Node. Historical targets without the runtime-selection capability keep their original Node behavior. The test-runtime setup action installs a checksum-pinned build of the Bun fork only for jobs that need it. The source commit, archive checksum, and executable checksum live together in .github/actions/setup-test-bun/action.yml. Node continues to own orchestration, builds, compiler preparation, and cleanup; Vitest and its workers use the selected runtime. Bun and Node have separate transform-cache directories and timing identities. Either runtime failing fails the job. This adds no matrix rows or runner registrations. NODE_OPTIONS continues to limit Node heaps; Bun does not use that V8 heap limit. Compare observed memory use alongside elapsed time before admitting more lanes. Compatibility evidence must use the exact fork build installed by CI; stock Bun results and different fork revisions are separate measurements.

Node execution and runtime compatibility

Most Node test, lint, and typecheck jobs currently inherit 24.x from .github/actions/setup-node-env; the Node shard also supplies that default explicitly. The composite’s ensure-node.sh reuses a matching active or cached runtime before downloading one. A wildcard therefore does not guarantee the newest patch. Compare exact versions when measuring a toolchain change, and measure setup separately from the test body. Preflight’s manifest bootstrap uses the exact NODE_VERSION pin in ci.yml (24.19.0). Unlike the repository helper, actions/setup-node can satisfy a 24.x request from an older cached patch below OpenClaw’s support floor. CI’s execution version does not define the supported user runtime matrix. package.json accepts Node 24.16+ and Node 26.1+. The full manual CI graph checks the Node 24.16.0 floor; node-runtime-compat.yml checks Node 26.1.0 weekly and on dispatch. Current packaged fresh-install and upgrade checks run on both Node 24.19.0 and Node 26.1.0, with Windows Node 24 fresh-install proof on 24.16.0. Both runtime variants use the same candidate package. These support cells stay independent of changes to the ordinary execution pin; source/build smoke alone does not prove an installed upgrade. See the package validation matrix. Select a supported Node runtime before project commands, including lightweight workflow checks and release orchestration. Hosted images can default to Node 22; an absent version pin does not prove a supported runtime. Runner images can also contain unused older toolchains, and recovery bundles intentionally retain older syntax targets so unsupported runtimes can print upgrade diagnostics. Those are separate from the supported runtime and test-job versions. GitHub JavaScript actions also have their own runtime, independent of the node on the job’s PATH.

macOS Swift phases

macos-swift (tests) builds and runs the app’s complete default- and named-profile test partitions with coverage. macos-swift (packages) independently runs the OpenClawKit Talk-trait opt-out build, OpenClawKit tests, and Swabble tests. These separate package graphs previously ran before the app build in one job; a hosted baseline spent 7m57s on them in a 21m48s job. Separating them gives app compilation and tests their own 30-minute budget without removing coverage or increasing test-process parallelism. Both phases use macos-26, with at most two concurrent jobs. Full manual validation adds the existing release phase under the same cap. This adds one hosted Mac job and its checkout/setup cost per selected run, with no additional Blacksmith registrations. Compare complete hosted timings, including queue and setup time, before treating the removed serial work as an observed speedup. Only the app phases restore the app build cache. SwiftPM dependency caches remain restore-only in packages; the existing primary phase owns shared cache writes. The aggregate gate requires every selected phase to succeed. Debug Swift CI builds omit the IDE index and use line-table debug information. Coverage instrumentation and source-line backtraces remain enabled; interactive debugger type/value inspection requires a normal local debug build. The app test cache uses a separate build profile so it cannot restore the old indexed products; Release build flags and caches remain unchanged. Ordinary Markdown and MDX pages under docs/, plus root README.md, retain their separate check-docs coverage beside precise pull-request Node tests. Page deletions and renames preserve this targeting. Explicit Node owners for Markdown inputs remain selected; workspace templates under docs/reference/templates/ and unowned source inputs retain the full fallback. When docker-seed-e2e selects the published upgrade survivor, it uploads docker-seed-upgrade-survivor-proof even after a failure. The artifact contains scheduler summaries and sanitized survivor reports. Failed reports include bounded, redacted baseline and candidate agent-turn output; private scenario state and raw logs remain outside the upload. Full canonical main pushes run the operator config and prior-release state startup corpora once through the Node runtime-config owner. Canonical pull requests also omit the duplicate Check startup corpus step when preflight certifies every corpus file in the required Node matrix on the exact same checkout revision. Partial, filtered or unknown plans retain the explicit step; release-gate dispatches retain their separate merge-tree proof. Both state repair passes, all static baseline ratchets and required Node failure aggregation remain unchanged. Eight state test files share one matrix inventory so the executor can distribute all release/config pairs across workers. Each pair still runs both Doctor and Gateway startup checks. The explicit step prepares the runtime once with pnpm build qaRuntime, then runs the config corpus and all eight state files in one Vitest process with at most four workers. A failed preparation stops the step before workers consume memory or attempt their own builds. Frozen targets from before the file split retain their config process and four state processes. The corpus uses the normal bundled-plugin resolver to select the prepared runtime from this checkout instead of forcing TypeScript plugin entrypoints. Plugins whose Doctor contracts require source loading retain that behavior; the complete config/state matrix and its assertions remain intact. Ordinary pull requests that change only independent Control UI unit-test entries keep all three UI unit rows and existing type/lint gates, without repeating dedicated UI E2E jobs. Browser and Node test entries, shared fixtures/helpers, production or build inputs, and tests imported by another owner retain E2E coverage. Main pushes, manual validation, and frozen targets keep their existing selection. The docker-seed-e2e job selects the executable owners of changed E2E helpers and the published-upgrade regression gate through one scheduler invocation. The published lane runs legacy-operator-state against an exact published predecessor on affected canonical main pushes. Canonical manual CI retains it when the target declares the Docker seed capability. Unknown changed paths retain survivor coverage; docs-only pushes remain excluded at the trigger. It uses auto-auth: every supported baseline must replace the running managed Gateway through its own updater. Schema refusal or rollback fails the gate. Main selection includes src/cli/update-cli/**, src/infra/update-*, src/infra/package-update-*, src/plugins/update.ts, src/plugins/update-*, src/commands/doctor*, src/commands/doctor/**, all src/state/**, and package.json (including its packaged schema-version metadata). It also includes scripts/e2e/upgrade-survivor*, scripts/e2e/lib/upgrade-survivor/**, the survivor policy and baseline resolver, the Docker planner/catalog, and this gate’s CI workflow, Docker selector (scripts/lib/ci-docker-seed-plan.mts), and shared test-path classifier. Node-only planner edits do not select Docker lanes. The Node planner retains its selector export for older target/harness combinations. Tests independently pin both state and agent schema-version constant owners to the published lane. The scheduler retains one 16-class Blacksmith runner on eligible main pushes and its existing serial main/manual lane admission. Pull requests and their exact-head fallback dispatches do not select this proof. Installed-driver upgrade coverage remains required when selected on main and ordinary manual release CI; the existing infrastructure timeout stays unchanged. The job is part of openclaw/ci-gate. It uses at most one runner registration per selected run; adding the survivor does not increase the existing full-inventory registration cap, job count, or matrix fanout. Standalone Periphery workflows enforce zero dead-code findings for the iOS and macOS apps. The shared OpenClawKit workflow scans both consumers in parallel and reports a declaration only when Periphery emits the same Swift USR from both builds. Its generated OpenClawProtocol/GatewayModels.swift schema contract is retained as generator-owned code rather than treated as app-local dead code. All four scans use scripts/install-periphery.sh to install the checksum-pinned Periphery 3.8.0 OSS release, including its adjacent libIndexStore.dylib, in a dedicated runner-temporary directory. The installer rejects download, checksum, and version failures without falling back to Homebrew. Installer changes select all three native workflows. Upstream archived the OSS project. The pin is a maintainer-owned bridge for the workflows’ Xcode 26.6 toolchain, not a claim of ongoing upstream support. Native CI maintainers must revalidate both app scans and both shared consumers before changing Xcode, the pinned release, or the analyzer; retain the zero-findings policy and exact-USR intersection rather than adding a baseline or a weaker fallback.

Security review checks

Security review separates product changes that maintainers can approve from the small set of security policy and enforcement files that require SecOps approval. The Security Review workflow runs both guards from trusted repository code. It publishes a commit status named openclaw/ci-gate that requires both the applicable approvals and a successful native CI gate from the latest CI run for the current PR head. The existing CI job retains its check with the same name. GitHub requires both the check and the commit status when both share a required context. Missing approval, failed CI, or evaluation errors fail the review status. Missing or running CI leaves it pending and keeps merging blocked. CI completion automatically evaluates it again. Approval comments do not rerun the test suite. The Security Review Actions job succeeds when evaluation completes, including when the required commit status blocks merging for missing approval or failed CI. This prevents an earlier evaluation from leaving a stale failed job after automatic reevaluation clears the status. Evaluation errors still fail the job and keep the required status closed. The Security Sensitive Guard publishes openclaw/security-sensitive-review. Its inventory in .github/security-review-policy.yml covers Gateway authentication, pairing and permissions; credentials, secrets and redaction; sandbox and execution policies; product security checks; and .gitignore. Ordinary documentation, tests, and test support do not trigger this inventory. Renames inspect both the old and new paths so moving a sensitive file does not remove its review requirement. The Dependency Guard publishes openclaw/dependency-review and retains its dependency classification and lockfile autoscrub behavior. Dependency removals that already qualify as informational remain informational. Edit .github/security-review-policy.yml to change path classification. Its categories group product paths with descriptions and review guidance; exclude names the product-only exclusions; and dependencies lists manifests, lockfiles, and other dependency files. Paths are quoted, repository-relative globs: * stays within a path segment, ** crosses directories, and {a,b} matches either alternative. Hidden paths are included. Matching is case-sensitive unless an exclusion explicitly sets case-insensitive: true. Product exclusions never exempt dependency changes. The first matching product category supplies the notice’s guidance; matches are not CODEOWNERS rules. The JavaScript loader handles validation and matching; it contains no path inventory. Invalid policy fails security review. Both the YAML inventory and its loader require SecOps code-owner approval. The workflow loads them from the trusted checkout and installs only the locked parser/matcher runtime through .github/actions/setup-security-review, with lifecycle scripts and dependency caches disabled. That action’s manifest and npm lock mirror the root dependency pins and pnpm-lock.yaml; update them together when those pins change. Approval decisions and dependency graph analysis remain in JavaScript. Both guards use current repository permissions. Only GitHub user accounts with maintain or admin access can grant command approval or receive the author exemption. write access and organization membership are insufficient. A maintainer pushing to an external contributor’s branch does not transfer the author exemption. Automation using a GitHub user account qualifies under the same role check; GitHub App bot identities do not qualify. For other authors, wait for the applicable guard notice to show the current PR commit, then post the command on its own line in a new PR comment:
Each command approves only its own guard. When both guards require approval, both commands are required and can appear on separate lines in one comment. Use only command lines in that comment, without prose, quotes, or code fences. Normal GitHub Approve reviews and labels do not replace these commands. The guard associates a command with the revision recorded in its trusted notice. A command posted before the notice requests approval for that revision cannot approve it. After a new commit, wait for the notice to update and post a new comment. Editing an older comment does not grant fresh approval. Deleting an approval comment or removing its command revokes that approval; current roles are checked again whenever the guard runs. GitHub commit statuses apply to a commit rather than one PR. Before publishing success, security review verifies that the head belongs to only the current open PR targeting that base branch. Duplicate heads block approval so one PR cannot reuse another PR’s author exemption or command. Close the duplicate PR or push a distinct commit; security review evaluates the affected PRs automatically. Each guard updates one PR comment with affected files, review guidance, the current revision, and the remaining action. The sensitive-change label remains after approval so reviewers can still identify the affected responsibility. Trusted issue_comment events reevaluate approval commands and edits or deletions of approval comments automatically. Edits inspect both the previous and current text so removing a command still revokes approval. Ordinary comment activity does not reevaluate the guards or change their statuses. Command mentions in prose, quotes, or code fences are not approval comments. The workflow filters ordinary comments before allocating a runner; a command mention can start the lightweight resolver, which validates the syntax before scheduling review. Both guards share one review job, and comment events do not rerun the test suite. Evaluation uses trusted repository code and GitHub metadata without executing contributor code or comment text. The hard tier lives only in .github/CODEOWNERS: security policy, ownership, CodeQL, selected scanning configuration, and the security-review enforcement closure. Keep SecOps as the sole owner on those entries. GitHub accepts any owner on a matching line, and later matching patterns replace earlier ownership. The release-manager entries retain their separate approval responsibility. This inventory does not make every general CI or scanner change SecOps-owned. The soft tier protects the review flow for fork contributors. It is not a security boundary against hostile repository writers: another workflow with a write token can publish the same status context. Binding the required context to the GitHub Actions app identifies the publisher app, not the specific workflow. This is an accepted tradeoff to avoid a separate credential-bearing publisher. The existing maintainer CI bypass also permits bypassing missing command approvals. Native CODEOWNERS review enforcement remains independent of these statuses and that CI bypass. Results apply to the PR head evaluated by the workflow. New PR heads, base-branch retargeting, command comment events, and CI completion reevaluate automatically; unrelated pushes to main do not. Sensitive-path policy and permission changes take effect on the next automatic evaluation. Guard execution does not require manual dispatches or manual reruns.

Enable enforcement after deployment

Merging workflow files does not enable GitHub merge protection. After the workflow and the reviewed CODEOWNERS inventory are on the default branch:
  1. Keep openclaw/ci-gate required and bound to the GitHub Actions app. Preserve the existing maintainer bypass and leave Require branches to be up to date disabled. The two review statuses remain visible without separate required-check entries.
  2. Verify the native CI check and security review commit status on a PR’s current head, including after command approval, a subsequent push, and CI completion. Confirm approval updates do not start another test run.
  3. In a separate ruleset with no bypass actors, enable Require a pull request, Require review from Code Owners, and Dismiss stale pull request approvals when new commits are pushed. Verify that openclaw-secops is eligible for code ownership and that its approval is required for the hard inventory. Keep the general required approval count at zero if ordinary unowned changes should retain their existing review policy; code-owner review is a separate requirement. Existing release-manager entries also become required.
  4. Verify that the CI bypass cannot skip the separate code-owner review rule. Requiring a PR also restricts direct pushes to the protected branch.
Verify the live GitHub settings and the maintainer, external contributor, automation account, and SecOps-owned-path cases before declaring enforcement active.

Fail-fast order

  1. preflight decides which lanes exist at all. The docs-scope and changed-scope logic are steps inside this job, not standalone jobs. Canonical main starts immediately in one of two parity slots; each slot admits one complete run and coalesces later pushes into its newest pending tip. Downstream jobs wait for the manifest, then eligible Blacksmith jobs restore exact dependencies from the trusted warmer or fall back to the ordinary pnpm-store cache on a miss. Pushes, pull requests, and manual runs targeting the workflow revision run preflight with native Node and skip dependency setup. Manual runs targeting a different revision install dependencies and retain that target’s tsx tooling.
  2. security-fast, check-*, check-additional-*, check-docs, and skills-python fail quickly without waiting on the heavier artifact and platform matrix jobs. The production dependency audit sends one complete graph with up to four attempts and a four-minute total request budget, including retries and response reading. Timeouts, native fetch failures, HTTP 429, and 5xx responses retry with exponential backoff; retryable HTTP responses honor Retry-After. Attempts and recovery are logged. Persistent unavailability, vulnerability findings, invalid inputs, malformed advisory data, oversized responses, and permanent HTTP failures block CI. An unavailable audit is incomplete coverage, not a clean result. Local pre-commit and release dependency audits use the same bounded request owner and fail on unavailability.
  3. build-artifacts and the locale checks overlap with the fast Linux lanes. Control UI and native app source PRs exclude generated locale snapshots/resources; their serialized refresh workflows repair and auto-merge isolated generated PRs in the background. Source CI still blocks stale source inventories and unsafe localization calls. Generated PRs, manual CI, and release prep enforce full translated/platform-generated parity. Canonical release/YYYY.M.PATCH branches may include release-prep locale repairs with the other generated release output.
  4. Heavier platform and runtime lanes fan out after that: checks-fast-core, checks-fast-contracts-plugins, checks-fast-contracts-channels, checks-node-*, checks-windows, macos-node, macos-swift, ios-build, the screenshot shards, and android.
  5. openclaw/ci-gate waits for every selected lane. Preflight and security must succeed; downstream jobs may skip only when unselected by the manifest and existing event, runner, and compatibility conditions. An unexpected selected skip or any failed or canceled downstream job fails the aggregate. The aggregate uses !cancelled() so failed prerequisites still report, while canceling the workflow skips final reporting and releases its concurrency slot without waiting for another runner.
To retranslate every Control UI or native app string, dispatch Control UI Locale Refresh or Native App Locale Refresh from main with full_refresh=true. Ordinary runs remain incremental. Both workflows read the primary and fallback models from the OPENCLAW_I18N_MODEL and OPENCLAW_I18N_FALLBACK_MODEL GitHub secrets, using the existing translation OpenAI API key. Only an explicit model_not_found provider error selects the fallback; authentication, quota, and network failures do not. Generated metadata and public diagnostics omit model identifiers. The merge coordinator may reuse an authenticated successful openclaw/ci-gate for the same pull-request head for up to 24 hours. This avoids rewriting a contributor branch after unrelated main changes. The reusable result does not replace the separate strict, App-owned test-merge check against current main. A later pending or failed rerun does not erase an earlier successful result for that unchanged head during the freshness window. Reusing a CI check does not replace the current security review commit status. The default-branch ruleset requires the GitHub Actions-owned openclaw/ci-gate context. Repository maintainers and admins retain their CI bypass, including for missing command approvals. Normal pull-request merges use the gate. The separate code-owner ruleset requires a PR and the applicable owner approval even when CI is bypassed; organization rules still block deletion and non-fast-forward updates. The separate strict App-owned test-merge check still binds the head to current main. GitHub may mark superseded pull-request jobs as cancelled when a newer head lands. Treat that as CI noise unless the newest run for the same PR is also failing. Canonical main runs are not canceled after admission; each of the two parity slots replaces only its older pending run with the newest tip. Matrix jobs use fail-fast: false, and build-artifacts reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The canonical-main CI concurrency key is versioned (CI-v8-*) so GitHub-side zombies in the old group cannot block the two-slot pipeline; runnable PR groups remain on CI-v7-*, while passive draft runs use CI-draft-v1-*. Manual full-suite runs use CI-manual-v1-* and do not cancel in-progress runs. The plugin-list startup-memory guard keeps a 400 MiB ceiling on self-hosted Blacksmith Linux and allows 425 MiB on GitHub-hosted Linux, whose RSS baseline is higher for the same built CLI. The startup-memory check finishes alone before other built-artifact checks start on every runner, so concurrent verifiers do not perturb the RSS measurement. The Testbox validation, native Periphery, OpenGrep PR Diff, Sandbox Common Smoke, and Plugin Init Scaffold Validation workflows isolate passive draft PR events (opened, reopened, and synchronize) from useful PR work at concurrency admission. A delayed draft payload therefore cannot cancel an active ready run or replace a pending one before the draft job or scan is skipped. Disabling cancel-in-progress alone would still replace pending work. Where subscribed, converted_to_draft stays in the ordinary PR group to intentionally cancel work; non-draft head supersession and each workflow’s existing manual/push grouping remain unchanged. Periphery report publication separately checks source intent and live PR state; see Scope and routing. The singleton smoke then rebuilds the runtime plugin overlay before any other verifier reads it. On Blacksmith, Gateway watch finishes its build-receipt writes and whole-tree measurement next; Doctor, SQLite lifecycle, channel, core-support-boundary, and Discord attachment checks can then overlap. Hosted 4-core runners keep their existing serial sequence and channel/core pair. TUI canaries run after all other verifiers finish. Every verifier owns a separate Vitest module cache, and each selected result remains part of the same failure aggregation. The wave step is unconditional because its startup and singleton checks always run; individual checks retain their selection gates. The artifact job consumes only its selected checkout, so base-commit fetching stays with jobs that actually compare revisions. Use pnpm ci:timings, pnpm ci:timings:recent, or node scripts/ci-run-timings.mjs <run-id> to summarize wall time, start delay, slowest jobs, and failures from GitHub Actions. Use pnpm ci:timings:trend for a 72-hour baseline and a latest-12-hours versus prior-12-hours comparison. Trend mode includes every main push outcome, cancellation/pass rates, and successful-run wall time, then loads a balanced latest/prior sample of at most 100 successful runs by default. Its detailed sample separates workflow admission, job dependency/gate delay (job.created_at minus the first job’s creation), runner queue/start latency (job.started_at minus job.created_at), and execution; it also reports critical-path ownership and the actual GitHub API request count. Reruns use attempt-specific jobs and are excluded from run-level wall/admission distributions because GitHub retains the original workflow creation time. Raise or lower the detailed-run selection cap with --detail-runs (a run with more than 100 jobs requires multiple requests), emit JSON to stdout with --json, or save the same report with --output .artifacts/ci-timings/trend.json; missing output directories are created automatically. The baseline must cover at least two comparison windows. Run the timing helper locally; there is no in-workflow timing-summary job (a permanently disabled one was removed once the local helper became the tool everyone actually used). For build timing, check the build-artifacts job’s Build dist step: pnpm build:ci-artifacts prints [build-all] phase timings: and includes ui:build; the job also uploads the startup-memory artifact. The Run Node test shard step prints Bash time -p totals: elapsed (real), user CPU (user), and system CPU (sys) seconds, including waited-for child processes. Compare CPU totals with elapsed time across equivalent runs to distinguish extra CPU work from slower execution with similar CPU work. These totals alone do not establish runner contention. Android test rows retain their existing Gradle JUnit XML for 14 days in android-test-reports-<task>-<checkout-revision>-<run-attempt> artifacts, including failed runs unless canceled. Reports identify test cases and durations; compilation, lint, setup, and queue time remain separate in the job log. Read the XML alongside that attempt’s Gradle task outcomes: reports restored by FROM-CACHE or reused by UP-TO-DATE describe an earlier execution, so their times are historical. They do not show fresh test execution or a speedup in the current run. A failure before Gradle writes XML can leave no artifact; the upload warns without replacing the original failure. The artifact contains only phone and Wear unit-test XML, not dependency caches or application build outputs. Node test shards that need a built CLI run pnpm build qaRuntime before starting Vitest. This profile builds runtime JavaScript, plugin assets, and freshness and provenance metadata. Private QA shards select their private runtime entries. The build-artifacts job owns Control UI and SDK declaration validation; release package builds still generate the full declarations. Source-only Linux Node 24 shards can restore compiled Vitest workers from the protected cache warmer. The warmer prepares one generation before SDK or runtime builds change package resolution, joins the preparation owner, and publishes only the retained cache. PR jobs restore it without publishing. Consumers enable reuse only when an archive exists; cold runners keep ordinary fresh compilation. The worker owner verifies source and dependency bytes, compiler identity, resolution topology, environment, output inventory, and the exact checkout and output-slot paths before lending a generation. Changed or incompatible inputs rebuild locally. Frozen targets, other Node versions, and runtime-building shards retain fresh preparation. The artifact job keeps its built outputs for its own smoke and boundary checks. It no longer packs or uploads the unused dist-runtime-build and bundled-plugin-assets archives. Runtime shards still start after preflight; they do not wait for SDK declarations, the Control UI build, or artifact checks. Diagnostic and proof uploads remain available. Declaration caches hash the selected writer’s transitive generator imports, package and plugin metadata, explicit schema and build metadata inputs, and the compiler’s recorded source files. Editing an unrelated CI script does not rebuild declarations. Resolution topology still participates in the cache key, and an unresolved generator import stops the build instead of trusting a cache. Local pnpm build:ci-artifacts uses the same memory admission as full and package builds. The orchestrator passes the resolved heap budget to every child process, including the SDK declaration writer, so local builds do not depend on CI’s NODE_OPTIONS setting. The existing policy accounts for host and cgroup limits and reserves native-memory headroom. If the default budget cannot fit the build, it stops before build steps or cache restoration; OPENCLAW_TSDOWN_MAX_OLD_SPACE_MB remains the explicit operator override for attempting a different budget.

Control UI size budgets

pnpm ui:build produces and verifies the bundle, then reports its compressed sizes. Budget violations do not prevent artifact generation. The separate control-ui-performance job enforces the budgets without blocking other jobs from building or testing the same source. Startup CSS has a 45 KiB advisory target and a 50 KiB hard ceiling. Growth below 1 KiB passes; an increase of 1 KiB or more in either startup CSS or the largest CSS file fails the comparison. The existing largest-file, JavaScript, request-count, and isolated-renderer ceilings still apply independently. Reports include exact bytes, base deltas, and remaining headroom, with an early warning when the largest CSS file has less than 1 KiB of headroom. JavaScript accounting separates ordinary chunks, deferred special-purpose chunks, startup assets, and the full bundle total. The 215 KiB ordinary-chunk ceiling excludes the isolated Mermaid renderer and configured locale catalogs. A locale catalog must match assets/<locale>-<nonempty-suffix>.js; each configured locale may produce one deferred chunk, with at most 20 locale chunks overall and a 300 KiB ceiling per chunk. Locale chunks remain forbidden in the startup asset set. Total JavaScript accounting includes ordinary, deferred, and startup assets for reporting while enforcement stays on the category-specific limits. CI builds the selected checkout and the exact preflight base with the same installed Node, Vite, and dependencies. The temporary base’s CSS sidecars are normalized through the candidate’s pinned compressor before comparison. The report identifies both revisions and the toolchain. There is no manually updated CSS baseline, and the cumulative ceilings still bound a series of small changes. Run the same comparison locally after installing dependencies:
To enforce absolute budgets on an existing build, run pnpm ui:check-performance. Use --base-dist <directory> to compare with an already-built base, or --report-only to report violations without failing. Missing or malformed build artifacts remain errors in report-only mode.