Read this reference when changing hooks, workflows, caches, runners or
publication configuration. CI explains the pull-request checks: the
single required check ci-ok, the tiers, labels, the lane table in
scripts/ci/lanes.py, lane runners, running a lane locally and adding one.
Agents load $change-maple-ci.
Commands and paths are relative to the monorepo root.
./setup-hooks.sh installs .githooks/pre-commit. It asks
scripts/ci/plan.py hook --staged which components to check: a lane's own
paths select the component in its hook field, without CI's fan-out to
consumers, and an unclassified path selects every component. Each selected
component's .githooks/pre-commit runs inside its Nix flake: the root .#ci
shell for repository checks, Research, Auth, services/updates/ and
scripts/ci/agent-updates.sh; the component flakes for apps/maple-agent/,
sdk/, proxy/ and services/opensecret?submodules=1. Without Nix the same
commands run from PATH with a warning.
The hook runs the fast part of the selected lanes: format, lint, type checks
and unit tests. The Agent runs its agent-lint and agent-test recipes;
Research Rust runs formatting and tests, without Clippy; repository changes
run actionlint on staged workflows and the scripts/ci tests. Integration
suites, cargo-deny, nix flake check and packaging stay in CI.
MAPLE_HOOK_FULL=1 runs slower variants where they exist, MAPLE_HOOK_ONLY=a,b
restricts the components, MAPLE_HOOK_NO_NIX=1 runs from PATH even with
Nix installed, and MAPLE_HOOK_SKIP=1 or git commit --no-verify skips the
hook. scripts/ci/test_plan.py keeps the hook's components and its
branches in step with the lane table.
- Pull requests restore Actions caches, and no pull-request write reaches a
cache
masterreads. The exceptions are the EIF jobs.opensecret-eif.yml'seifjob (fork PRs that edit PCR approvals, and manual runs outsidemaster) has no FlakeHub credentials, so its Nix store goes to the run's branch-scoped Actions cache, whichmasterand other pull requests cannot read. Same-repository PR EIF builds (ci.yml'sos-eifandopensecret-eif.yml'seif-trusted) push to the FlakeHub Nix cache by design, and fall back to the branch-scoped Actions cache when FlakeHub does not initialize (cache policy). - Lane caches (
Swatinem/rust-cache, prefixci-v2) are keyed by lane and toolchain pins (the workspace'sflake.lock, and OpenSecret'srust-toolchain.toml), which must match exactly, then by rust-cache's lockfile hash, which may not: a version bump restores its exact cache, and any other lockfile change the newest cache of the same lane and toolchain. The restore key also hashes the runner's rustc versions (the Windows lanes' rustup pin among them) andCARGO*/RUST*variables, so a runner image with a new preinstalled Rust starts cold once, until master saves again. rust-cache's save keeps every build of a crate still in the graph, so a master run that will save a new cache first deletes the lane's target directory: each saved cache holds one dependency graph.agent-supply-chainandagent-provider, which only resolve the Agent's dependency graph, shareagent-cargo-home: the Cargo registry and git sources (zed and cua among them) without a target directory. Master pushes and the nightly run save them, into the store of the lane's own runner vendor; a runner from the other vendor (a failover) only restores. Heavy Rust caches therefore live in Blacksmith's store. GitHub's holds what GitHub-hosted jobs need: the lane caches ofagent-full-windows(saved only by the nightly run) andagent-windows-updater, theagent-package-windowspackaging cache (below), the signed Research Windows build's Bun binary, the release, the Nix installer and that EIF check.test_workflow_policy.pylists these writers. - The signed master builds write the keys
release.ymlrestores by prefix (<os>-sccache-*for macOS and Linux,xcode-cache-*,release-onnxruntime-ios-*,<os>-bun-*,<os>-gradle-*). Research Windows builds use no compiler cache. No pull-request job writes these prefixes; the Research platform lanes only restore them. The iOS ONNX key text is identical in every workflow that uses it. A release job reads only the store of its own runner vendor, sorelease.yml'sbuild-web(GitHub-hosted) starts with a cold Bun cache; Bun installs in seconds. agent-desktop-build.yml's unsigned builders save the exact keyci-v1-agent-release-<os>-<graph hash>on master pushes, with no fallback keys.scripts/ci/cargo_graph_key.pyhashes the Agent'sflake.lockand itsCargo.lockwithout the versions of workspace and path crates, which Cargo rebuilds anyway: a release version bump keeps the key, and a change to any third-party package or dependency edge, or to the toolchain (for macOS alsoscripts/ci/apple-toolchain.json), makes the next build cold. Pushes build only Prod; the nightly Dev and dispatched builds restore the same key without saving (the profile changes only Agent workspace crates, which Cargo rebuilds). Noci.ymllane reads it. The Agent packaging lanes build thin LTO into their ownci-v1-agent-package-<os>-*caches, which only the nightly master run saves. The Agent's signing, attestation and verification jobs restore nothing.
| Workflow | Group | Cancelled |
|---|---|---|
ci.yml |
per PR, or per merge-queue head or SHA | superseded PR runs only |
frontend-tests, rust-tests, web-build, desktop-build, android-build, proxy-rust |
none | never |
codeql.yml |
per SHA | never |
agent-desktop-build.yml |
per run | never |
mobile-build.yml, ios-dev-testflight.yml |
one queue each (queue: max) |
never |
opensecret-eif.yml, pages-preview-build.yml, auth-pages-ci.yml |
per PR or SHA | PR runs only |
nightly.yml |
one group | never |
| Publishers | their own serial groups | never |
scripts/ci/test_contracts.py runs in the repo lane and imports the code on
the other side of each contract. A failure means the workflow change is wrong:
release.ymland the publishers (pages-publish,proxy-publish,updates-publish,zapstore-publish,sdk-publish-npm,sdk-publish-rust,agent-updates-publish,auth-pages-publish,opensecret-eif-release) change only with owner review. Exactly one workflow is namedRelease, and everyworkflow_runlistener names its producer and checks the head repository, conclusion and event.services/agent-updates/publisher/publisher.pytrustsagent-desktop-build.ymlby path, job names, artifact names and signer, and its run number is the Agent build number: never rename that file. The Windows packaging and verification jobs setMAPLE_PACKAGE_BUILD_NUMBERto that run number.scripts/ci/windows_signature_reference.py(the manual signature probe) trusts the same file's Windows Prod jobs and their runners.- The release preflight (
.agents/skills/release-maple/scripts/ci_evidence.py) needs the evidence jobs offrontend-tests,rust-tests,web-build,desktop-build,android-buildandmobile-buildon master pushes, and a CodeQL that is not red: the newest finished scan of the release commit or an ancestor succeeded (a scan still running does not block). These run on every relevant master push and are never cancelled. - The signed master builds write the release cache keys above (the Agent's
is
cargo_graph_key.py's hash of its lockfiles, plus the Xcode pin on macOS, with no fallback), andios-dev-testflight.yml's run number is the Maple Dev build number: never rename that file. - Every OpenSecret EIF builder that uses FlakeHub (
ci.yml'sos-eif,opensecret-eif.yml'seif-trustedand the release build) runs on the release builder's runner class, and only the listed jobs receiveid-token: writeor use FlakeHub. The unprivileged fork job stays onubuntu-24.04-arm. An EIF approval mismatch fails only where that environment's approval files are edited (or a manual run outsidemaster); on master it is a notice. - Each protected environment has a fixed set of consumer jobs;
windows-signingruns only on GitHub-hosted runners. - Docs, guides and READMEs name only workflows that exist. Docs select no
lane, so this runs with the
repolane and nightly; runjust lane repoafter editing docs.
The Pages and Auth publishers also trust their producers
(pages-preview-build.yml, pages-production-build.yml,
auth-pages-ci.yml, auth-pages-build.yml) by file, artifact name and
gates; test_pages_workflows.py and test_pages_auth_workflows.py pin them.
scripts/ci/test_workflow_policy.py holds the rules every workflow keeps: no
pull_request_target, and a fixed set of workflows that run on pull requests;
actions pinned to commits; checkouts without persisted credentials; read-only
workflow permissions, with write scopes granted to the jobs that need them;
secrets only inside their environment's jobs; a pinned, verified Nix install; a
timeout on every job; ci-ok needing every other ci.yml job; and declared
runner labels.
Master pushes start signed Research builds (desktop-build, android-build,
mobile-build, ios-dev-testflight) when app-change-detection.yml and
scripts/ci/change_detection.py find Research inputs for that platform; the
scripts only ci.yml's unsigned lanes run select none. Only mobile-build and
ios-dev-testflight can also be dispatched, which always builds; desktop and
Android release evidence comes only from master pushes. The selector routes
proxy runtime and Rust SDK build inputs to the native builds, because the
Research shell builds both from the tree. TypeScript SDK changes never select
app builds: each frontend builds the SDK its own manifest selects. Agent Prod
desktop packages build on master pushes that change Agent inputs (plan.py only agent-desktop), Agent Dev packages nightly, and both on every manual dispatch.
Signing jobs keep their protected environments; approving one trusts its
runner with that environment's secrets.
To rehearse the signed Research release scripts outside CI, run
scripts/ci/signed-release-rehearsal.sh <desktop|ios|android|all> in the root
Nix shell that builds the platform (.#apple, .#android, and
.#desktop-linux on Linux or .#ci on macOS); its usage text lists the
signing variables, and nothing is uploaded.
Routine development never authorizes releases, signing, store submission,
deployment, or live-service changes. Never push to master as a validation
step: app inputs start production-shaped signed builds and can upload iOS
artifacts to TestFlight. Creating a GitHub Release starts release builds and
downstream publication. Use $release-maple only for explicitly requested
release work, and report the tag and commit before publishing.
For Pages, read the deployment guide. Credential-bearing publication runs from trusted master; publishing flags, protected environments and Cloudflare build controls are operator configuration, not consequences of merging source.
The agent-windows-updater lane (see scripts/ci/lanes.py for what selects
it) runs the updater's native filesystem, Authenticode-rejection, transaction
and recovery tests and the bootstrap integration test on GitHub's Windows
image. Like every windows matrix lane it has no signing credential or
protected environment. A pass qualifies no official signature, signed
installed upgrade, visible GUI or paired remote upgrade.
The manual agent-windows-signature-probe.yml takes an exact historical
producer run, attempt and source and the artifact's ZIP and PE digests. It
checks that the trusted-master Windows Prod build, signing and download
verification jobs passed and that the ZIP and package identity match, then runs
the production Authenticode verifier on the PE without launching it. It has no
default reference and nothing depends on it; a pass proves historical signature
interoperability only.
ci.yml runs os-rust, os-flake-check (the component flake's checks),
os-helpers (the parent helper images), deps-opensecret (when its
manifest, lockfile or deny.toml changes), os-pcr (the root
pcr-compatibility check for PCR tooling and approval edits) and
os-sdk-e2e, which tests both SDKs against services/opensecret/ from the
same checkout. The os-eif lane builds dev and
prod EIF candidates on same-repository PRs to master that change measured
inputs (or carry the ci:eif label) without editing PCR approvals, on the
release builder's
blacksmith-16vcpu-ubuntu-2404-arm class, and reports their PCRs.
opensecret-eif.yml compares measurements on PRs that edit approved PCR JSON,
on relevant master changes and on manual runs; that approval status is not
part of ci-ok. A mismatch fails a manual run outside master, and a PR only
in an environment whose approval files it edits. On master it is pending
approval: the job passes with a notice and the built PCRs in its summary. Build
and comparison errors always fail. These checks never change approvals, sign,
release, or authorize deployment. Master and same-repository PR EIF builds receive OIDC for
FlakeHub caching; fork PRs and non-master manual runs never do. Verify cache
changes on fresh hosted runners, not a warm local Nix store. Details:
CI approval checks
and the cache policy.
Jobs install Determinate Nix 3.23.3 through an immutable
DeterminateSystems/determinate-nix-action commit with source-tag: v3.23.3
(.github/actions/setup-nix, or inline in the sacred workflows), then run
scripts/ci/verify-nix-runtime.sh. Keep the revision and tag in step when
upgrading. No GitHub token reaches the installer; FlakeHub authentication is
limited to jobs with id-token: write.
The pinned actionlint carries
scripts/ci/patches/actionlint-stream-stdin.patch, so analyzer input streams
after the child starts; remove it when upstream includes this. The macOS shell
caps Bash heredoc pipes at 512 bytes, and larger bodies use a temporary file.
CI runners assigns the ci.yml lanes. The signed and
release workflows use:
| Work | Runner |
|---|---|
Research macOS, Linux, Android and iOS builds, which compile and sign (master and release); rust-tests.yml; the Agent's unsigned master builds |
Blacksmith (blacksmith-6vcpu-macos-27, blacksmith-8vcpu-ubuntu-2404, blacksmith-8vcpu-windows-2025) |
Release build-windows |
maple-windows-2025-16core |
| Signed Research Windows master build; Agent Windows signing and verification | windows-2025 |
| Artifact verifiers, TestFlight submission, the Agent's signing and attestation, CodeQL, web and frontend evidence | Other GitHub-hosted runners |
Proxy release builds and their PR rehearsal (proxy-rust.yml) |
GitHub-hosted Linux and Windows, Blacksmith macOS |
| EIF builders that use FlakeHub | blacksmith-16vcpu-ubuntu-2404-arm |
The Agent's signing and attestation, both TestFlight submissions, the macOS
release verifiers and the Metal Toolchain export need the pinned Xcode, so they
run on GitHub's xcode-27 label, a public-preview image without an SLA. If
GitHub renames or retires it, move them to the label that carries Xcode 27.1,
such as macos-27 once that image ships it. Jobs that launch or inspect
artifacts without selecting Xcode stay on macos-26, an older macOS than the
builds link against.
The Agent's signing and attestation, windows-signing jobs and every artifact
verifier stay GitHub-hosted: the Agent publisher rejects self-hosted signers,
Blacksmith counts as self-hosted, and verifiers run fresh. The Agent's
unsigned master Windows build compiles on Blacksmith because its fat-LTO link
runs on one thread, so 16 cores are no faster. Blacksmith's Windows workspace
inherits a foreign write ACE that the updater's storage validation rejects,
so the updater tests and install smokes stay on GitHub's image. Jobs that
compile and sign install the pinned Artifact Signing module before compiling,
so a failed module download fails early; the module's own NuGet packages and
the Azure login still come after the compile. The installer retries the
PowerShell Gallery and its CDN and accepts only the pinned SHA-256. Rust,
MSVC, Perl and the linker are pinned by .github/actions/setup-agent-windows
for the Agent.
Research desktop and iOS builds select Xcode 27.1 (27A9275) from
scripts/ci/apple-toolchain.json through
scripts/ci/select-xcode.py; the root Nix shell exports the same version for
its Apple recipes. Do not rely on a runner's default Xcode. The
iOS ONNX pins
verify artifacts against that exact build, so an Xcode upgrade means rebuilding
and reviewing those hashes and validating desktop and iOS packaging. The
Agent's signed macOS build compiles gpui's Metal shaders: Blacksmith's macOS
image has no Metal Toolchain and cannot download one, so a GitHub xcode-27
job exports it as a one-day artifact that the build imports, and the build's
release cache key hashes the Xcode pin. A newer
CI image does not raise the shipped deployment targets: Research supports
macOS 13.4 and iOS 16.0.
Research Linux packaging builds AppImage and DEB with Tauri, then the RPM from
the normalized DEB payload. scripts/ci/sccache-stats.sh reports compiler-cache
statistics with the build's own cache configuration.
The RPM builder preserves explicit bundle.linux.rpm.depends from the Tauri
configuration. WebKitGTK (webkit2gtk4.1) is required on Fedora, including KDE;
package verification rejects RPMs missing that requirement. Install a downloaded
RPM through Discover or sudo dnf install ./Maple-<version>-1.x86_64.rpm so the
package manager resolves dependencies, rather than using rpm --nodeps.
For a side-by-side hosted Dev installation, use
nix run --no-update-lock-file .#research-dev-desktop, not an ordinary Research
PR package. The Linux Dev command stages its own DEB payload and normalizes it
into the separately named maple-research-dev RPM under target/research-dev.
It uses the fixed Dev native identity and callback scheme, disables updates,
and verifies the final package contains only Dev-owned executable, launcher,
icon, and runtime paths. See the
Research Dev package guide.
Follow the consumer version policy. Consumers select published SDK pins independently; local links are for development. SDK publication and upgrading a consumer are separate decisions.
Re-check prescriptive language against source, tests, and executed workflows. Correct material workflow, ownership, or validation changes alongside the implementation. Keep unrelated drift separately scoped and avoid duplicated policy. Public guides describe implementation, supported setup, tests, and CI; credential administration and private operational procedures belong with their owners outside this repository.