| name | build-image |
| description | Use when turning a Git repository into a container image with Zeabur CI/CD — a one-off build or a reusable branch source. Use when the user says "build my repo into an image", "deploy my own code" (build first, then deploy), or "rebuild that branch". Works with public repos and private ones (HTTPS token or SSH deploy key), on any Git host — not just GitHub. Talks to the GraphQL API directly; no CLI needed. |
Zeabur Build Image
Zeabur CI/CD clones a Git repo, builds it into a container image (Dockerfile or auto-detected), and pushes it to Zeabur's registry. You get back an imageReference you can pull from anywhere — a VPS running k3s or docker, or any other machine. No Zeabur project is created.
Set up the zapi helper from the auth skill first.
One difference from the other toolkit skills: the CI/CD API derives the owner from the token — there is no ownerID argument, so builds always belong to the token's own account (a team workspace cannot be selected).
Source URL and authentication must match
| Repo | authentication.type | URL shape |
|---|
| Public | PUBLIC | https://<host>/<owner>/<repo> (.git optional) |
| Private over HTTPS | HTTPS + {username, password} (password = token/PAT) | https://<host>/<owner>/<repo> |
| Private over SSH | SSH + {privateKey} (deploy key) | git@<host>:<owner>/<repo>.git or ssh://git@<host>/... |
Any public Git host works (GitHub, GitLab, Bitbucket, self-hosted…). Rejected regardless of type: credentials embedded in the URL, plain http://, IP addresses, localhost. Credentials are encrypted server-side, used once for the build, and can never be read back through the API — but they must still transit the mutation, so read them from an env var or file instead of pasting them into the conversation.
One-off build
region is the build-cluster location, not your server's — hkg1 is the safe default (an unsupported value returns INVALID_ARGUMENT: Unsupported build region). Always pass an idempotencyKey: retrying with the same key returns the original build instead of starting (and paying for) a second one.
OP_ID=$(uuidgen)
zapi "$(jq -n --arg op "$OP_ID" '{query: "mutation($in: CreateCICDBuildInput!){ createCICDBuild(input: $in) { id status region createdAt } }", variables: {in: {region: "hkg1", url: "https://github.com/<owner>/<repo>", ref: "main", authentication: {type: "PUBLIC"}, idempotencyKey: $op}}')"
ref accepts a branch, tag, or commit SHA.
dockerfile — omit it and the builder auto-detects/generates one (the result shows up as resolvedDockerfile on the build); set it only for a non-default path.
rootDirectory — relative subdirectory for monorepos (no absolute paths, no ..).
buildVariables: [{key, value}] — build-time variables. Keys must match [A-Za-z_][A-Za-z0-9_]* and be unique. Never put secrets here: unlike Git credentials, build variables are stored in plaintext and are readable back through the API by anyone who can query the build.
For a private repo, replace the authentication object:
--arg tok "$GIT_TOKEN" … authentication: {type: "HTTPS", https: {username: "<user>", password: $tok}}
--arg key "$(cat ./deploy_key)" … authentication: {type: "SSH", ssh: {privateKey: $key}}
Poll until it finishes
A successful mutation is not a successful start — dispatch failures surface asynchronously as FAILED. Poll every ~10 seconds; builds are hard-capped at 30 minutes:
zapi '{"query":"query($id: ID!){ cicdBuild(id: $id) { status imageReference resolvedDockerfile failureSummary startedAt finishedAt } }","variables":{"id":"<cicd_…>"}}' | jq '.data.cicdBuild'
status runs QUEUED → RUNNING → SUCCEEDED | FAILED | CANCELLED. There is no build-log API yet — on FAILED, failureSummary is all the diagnostics you get. To abort a build: mutation($id: ID!){ cancelCICDBuild(id: $id) }.
If you buffer the response in a shell variable inside a polling loop, print it with printf '%s\n' "$RESP", not echo — resolvedDockerfile and failureSummary are multi-line, and echo under sh/zsh expands their \n escapes into raw newlines, breaking jq (see the auth skill). Piping zapi … | jq directly avoids the problem entirely.
Save the build id — there is no API to list past builds.
Use the image
imageReference looks like <registry>/cicd/o-<owner>@sha256:… — digest-pinned, and pullable without any registry credentials. On a k3s server just reference it (no imagePullSecret), and docker pull works the same on a compose/docker machine:
kubectl create deployment myapp --image='<imageReference>' --port=<port>
The flip side of anonymous pull: treat the image as public — anyone who learns the reference can pull it, so don't bake secrets into the image. Image retention is not guaranteed forever; deploy soon after building. To wire up the service, ingress, and domain, continue with the deploy skill (steps after the workload) or kubernetes-workloads / subdomain.
Reusable source — rebuild a branch on demand
When the user will rebuild the same branch repeatedly, register it once (ref must be a branch here — tags/commits are one-off-build only):
zapi "$(jq -n '{query: "mutation($in: CreateCICDSourceInput!){ createCICDSource(input: $in) { id url ref region generation } }", variables: {in: {region: "hkg1", url: "https://github.com/<owner>/<repo>", ref: "main", authentication: {type: "PUBLIC"}}}}')"
Then each rebuild is one call (returns a full build to poll; buildVariables are not supported on this path):
zapi '{"query":"mutation($in: TriggerCICDSourceInput!){ triggerCICDSource(input: $in) { id status } }","variables":{"in":{"sourceID":"<cicdsrc_…>"}}}'
- List:
cicdSources(first, after) (forward-only pagination); latest build per source: cicdSourceLatestBuild(sourceID, status) — null just means "never built".
- Update:
updateCICDSource requires the current expectedGeneration (stale value → ABORTED, re-read and retry). Omitted fields keep their value; empty-string rootDirectory/dockerfile resets to the default; omitted authentication keeps the stored credential, explicit PUBLIC clears it. The url is immutable — new repo means a new source.
- There are no automatic Git triggers yet (no webhooks/polling) — rebuilds happen when you call
triggerCICDSource.
When the API pushes back
| Error | Meaning / next move |
|---|
PERMISSION_DENIED | CI/CD is not enabled for this account — direct the user to Zeabur support/dashboard. |
RESOURCE_EXHAUSTED | Concurrent-build limit hit (often 1) — wait for the active build to finish, then retry. |
FAILED_PRECONDITION: The Git source could not be resolved | The ref doesn't exist, the repo is private but auth was PUBLIC, or the credentials are wrong. Verify with git ls-remote locally. |
ALREADY_EXISTS | Same idempotencyKey reused with different inputs — generate a fresh key. |