CI Builds
Run one-shot CI builds in an isolated build machine and gate deploys on the result with ccp ci.
CI Builds
ccp ci runs a one-shot CI build in an isolated build machine, streams its logs live, and exits with the build's own exit code. That last part is the whole point: because a failing build exits non-zero, ccp ci composes directly into a shell gate.
ccp ci && ccp deploy --prodThe build runs in a Firecracker microVM cloned from the platform's build-machine template — an 8 GB memory ceiling with warm Go and Cargo toolchains. Each build clones your repo fresh and starts with no project dependencies; declare a cache block to reuse downloaded dependencies across builds. The VM is always torn down when the build finishes, whether it passed, failed, timed out, or was cancelled.
CI is the produce-and-verify half of the platform; compute is the run-the-artifact half. Use ccp ci to prove a commit builds and its tests pass, then ship it with ccp deploy or ccp compute deploy.
Quickstart
Add a .cluster-ci.yaml to your repo describing the build steps:
version: 1
timeout_sec: 1800
steps:
- run: go build ./...
- run: go test ./...Commit and push it, then run the build from the repo root:
ccp ciccp ci streams each step's output to your terminal as it runs and, on completion, exits with the build's exit code — 0 when every step passed, non-zero when one failed.
What Gets Built
ccp ci builds the pushed git remote, not your local working tree. The build machine clones the repository fresh, so only commits you've pushed are part of the build.
From the target directory (default .) the CLI derives the job from git:
| Field | Source |
|---|---|
repo_url | git remote get-url origin, normalized from SSH to HTTPS so the build machine can clone it with a server-injected token |
commit_sha | git rev-parse HEAD |
git_ref | the current branch — override with --git-ref |
If your local HEAD differs from origin/<ref>, ccp ci prints a ⚠ warning (the build clones the remote, so commit and push first) but still submits — handy when you intentionally want to build a ref other than your working state. On a detached HEAD it errors cleanly instead of guessing a ref.
Build Logs
ccp ci streams each step's output to your terminal live, and that output is secret-masked before it reaches you. The build machine clones private repos with a server-injected GitHub token; if a build step ever echoes that token, it is redacted to *** in the streamed logs rather than shown in the clear. Masking is chunk-split-safe, so a token split across two output chunks is still caught.
The public credential values the build machine uses — github.com and your git username — are not masked, so ordinary clone output reads normally. Masking covers only the token the platform injects on your behalf; you should still never print your own secrets from a build step.
The .cluster-ci.yaml Spec
Build steps live in .cluster-ci.yaml in the target directory, or in a file you point at with -f:
version: 1
timeout_sec: 1800
steps:
- run: cargo build --release
- run: cargo test
- run: cargo clippy -- -D warningsSteps run in order. The first non-zero step stops the run — later steps are skipped and the build is reported as failed.
The CLI converts the YAML to JSON and ships it verbatim as the job spec; it does no schema validation of its own. The server is the sole validator — a malformed spec is rejected with a 400, so a typo in .cluster-ci.yaml surfaces as a clear submit-time error rather than a confusing mid-build failure.
Dependency Caching
By default every build starts from the build-machine template with warm toolchains but no project dependencies, so it re-downloads its entire dependency set — usually the dominant cost of a cold build. Declare a cache block to persist those downloads and reuse them across builds:
version: 1
cache:
- ecosystem: go
- ecosystem: cargo
steps:
- run: go build ./...
- run: go test ./...Each entry names an ecosystem. The cache is keyed on your lockfile (plus the toolchain version and CPU architecture), so a build reuses a previous build's downloads whenever the lockfile is unchanged:
| Ecosystem | Cached | Keyed on |
|---|---|---|
go | yes | go.sum |
cargo | yes | Cargo.lock |
node, bun, python | accepted, not yet cached | — |
node, bun, and python are valid ecosystem values — they pass validation — but are a no-op for now: declaring them does nothing until caching lands for them. Any other value is rejected at submit time with a 400.
Before the steps run, the platform restores the cache for each ecosystem whose lockfile is present in the repo; after a build passes on a cache miss, it saves the freshly-downloaded dependencies for the next build. Caching is best-effort — a miss, or a cache that's temporarily unavailable, never fails an otherwise-passing build; it just falls back to a cold download. The build log reports the outcome per ecosystem:
✓ dependency cache hit: go
• dependency cache miss: cargo (cold build, will populate)An entry whose lockfile is absent from the repo is skipped silently — there's nothing to key the cache on.
Command and Flags
ccp ci [DIRECTORY] [-f FILE] [--org-id ORG] [--git-ref REF] [--no-wait] [--json]| Flag | Description |
|---|---|
DIRECTORY | Directory holding .cluster-ci.yaml and the git repo. Default .. |
-f / --file | Spec file override. Default <DIRECTORY>/.cluster-ci.yaml. |
--org-id | Owning organization. Skips the org picker; required in headless mode if you belong to more than one org (or set CCP_ORG_ID). |
--git-ref | Ref to build. Default: the current branch. |
--no-wait | Submit the build and print its job id, then return immediately — don't stream logs or block. |
--json | Suppress the streamed logs and print a single machine-parseable result line on completion. The process still exits with the build's exit code. This is the agent-facing mode. |
Exit Codes
ccp ci forwards the build's outcome as its own exit code, which is what makes it usable as a gate:
| Build status | Exit code |
|---|---|
passed | the steps' exit code (0) |
failed | the failing step's exit code (default 1) |
timed_out | 124 |
cancelled | 130 |
So ccp ci && ccp deploy --prod deploys only when the build passes, and a CI runner that checks $? sees the real failure code.
Failure Reasons
When a build doesn't pass, the final line names why. In human mode the reason is appended in parentheses, and in --json it's the error field:
ccp ci
# ...
# build failed — 43def05c (github_not_linked)The reason is a stable, branchable code:
| Reason | Meaning |
|---|---|
github_not_linked | The clone ran unauthenticated and the repo rejected it — the owning user has no linked GitHub account (or its token expired). Relink GitHub. This is the usual cause for a private repo that fails fast. |
clone_failed | A GitHub token was present but the clone still failed: a bad --git-ref, a missing repo, or the account lacks access to that specific repo. |
no_capacity | No build node had room (the build-machine needs 8 GB). Retry. |
setup_failed | An infra-side setup failure (build VM / envd not reachable), not a problem with your repo or GitHub. Retry; if it persists it's an outage. |
step N exited M | Build step N failed (the common case); the exit code is M. |
spec_invalid | The stored .cluster-ci.yaml spec was unparseable. |
A reason that merely mirrors the status (passed, timed_out, cancelled) is suppressed, so only the informative cases get the parenthetical.
Examples
ccp ci # build + stream the current repo, gate on the exit code
ccp ci --json # agent mode: one JSON result line, real exit code
ccp ci --no-wait # fire-and-forget — prints the job id and returns
ccp ci ../other-repo --git-ref main # build a different repo + refHeadless / Agent Use
ccp ci is headless-safe. Set CCP_HEADLESS=1 and provide a session token via CCP_SESSION_TOKEN:
export CCP_HEADLESS=1
export CCP_SESSION_TOKEN=$(... fetched from the operator's machine ...)
# Multi-org accounts must name the org; single-org auto-resolves:
ccp ci --org-id "$ORG_ID"It needs a git repo with an origin remote and a .cluster-ci.yaml (or -f FILE). For AI agents and scripts, add --json to suppress the streamed logs and get one parseable result line:
ccp ci --json
# {"id":"...","status":"passed","exit_code":0,"error":null}--no-wait --json instead returns as soon as the build is submitted, with just the id and status:
ccp ci --no-wait --json
# {"id":"...","status":"..."}Either way the streaming run's process exits with the build's exit code, so an agent can branch on $? without parsing the JSON. A build can also be cancelled from another shell using its job id; the build VM is always torn down on completion regardless of how the build ends.
See Headless mode for the full non-interactive pattern.
Next Steps
- Compute Services — run the artifact a passing build produced
- Deploying — ship a function once
ccp ciis green - Headless mode — wire
ccp ciinto CI pipelines and agent loops