Every build on this platform runs on one CI pool. The pool is ephemeral, scales from zero, and is non-privileged by construction. This page covers what runs-on: isolated gives you, how the platform builds images without a Docker daemon, and the one bootstrap step a fresh platform needs.
One pool: runs-on: isolated
Declare runs-on: isolated in a workflow. You get an ephemeral runner with a full toolchain, a rootless image builder, and a smoke Postgres — and no path to the node or other tenants.
Note: isolated is the only pool. No privileged pool exists, so a runs-on: docker job is never claimed; it waits forever.
Each runner pod is shaped for isolation:
automountServiceAccountToken: false— tenant code gets no kube API token.- The main container is the
open-platform/ci-runnertoolchain image (bun, node, git, curl baked in). It runsforgejo-runner one-job: one job, then gone. The pool registers ephemeral runners from a global instance-scoped secret, so every repo can targetisolated. - The execution label is
isolated:host— steps run directly in the toolchain container. There is no job container and no implicit checkout. Each job clones its exact commit itself with the ephemeral, repo-scopedgithub.token. - Sidecars: a rootless BuildKit builder (
moby/buildkitrootless, uid 1000, not privileged, reached over a UNIX socket), apostgres:16smoke database at127.0.0.1:5432(credsci/ci, dbci), and a CA-bundle init container so pushes to the platform's HTTPS registry endpoint verify. - An egress-only NetworkPolicy allows exactly: DNS, the Forgejo namespace (clone + registry pull), the edge (HTTPS push), and the MCP provisioning endpoints (
/ci/preview,/ci/sync-env). The endpoints authorize each call by a check of the caller's Actions token for push rights on that repo, and they fail closed. Public internet egress is open except RFC1918 ranges and the cloud-metadata address169.254.169.254.
Scale-from-zero
The pool is a KEDA ScaledJob (0 → 6 replicas, 15s polling, a 40-minute job deadline). The trigger is a postgresql query against Forgejo's own database: SELECT COUNT(*) FROM action_run_job WHERE status = 5 AND runs_on::text LIKE '%isolated%'. Waiting isolated jobs spawn runner pods. No waiting jobs, no pods, no idle cost.
The build pipeline
App workflows (see the five that ship in app-template: check, guard-ppt, preview, release, sync-env) build and ship in three steps:
- Build.
buildctl --addr "$BUILDKIT_HOST"drives the rootless BuildKit sidecar. Images push to the platform's public HTTPS registry endpoint.buildctlrefuses a plain-HTTP push — that is what the CA bundle is for. - Sign.
cosign signsigns by digest with the platform key (--tlog-upload=false, self-hosted). When no key is configured, the step skips gracefully — the KyvernoverifyImagespolicy is Audit-first, so a keyless fresh platform still deploys. - Chart.
helm package charts/app --version "$V" --app-version "$V"(the chart is renamed to the app), thenhelm pushtooci://<registry>/<org>/charts.
Previews use the tag 0.0.0-pr.<N>.<sha7> — sha-suffixed, so every push makes a distinct chart+image version that Flux rolls on. Releases are v* git-tag pushes; the bare semver becomes the image tag, the chart version, and APP_VERSION. From there Flux takes over — see Preview Environments and Platform Internals.
The credentials split by design. github.token (ephemeral, scoped to this repo) handles clone, API reads, and provisioning calls. REGISTRY_TOKEN is registry auth only — a per-org write:package-only credential, never an admin token.
Writing workflows: there is no Docker daemon
No node on the platform runs a Docker daemon for CI. For a workflow author that means:
- No
docker run, nodocker build, no service containers. Your tools arebun(install, test, build),buildctl(images),helm(charts),cosign(signing), plus git and curl. - Need a database for tests? Use the Postgres sidecar at
127.0.0.1:5432. The template'scheckworkflow uses it to lint, typecheck, and build, then boots the real server and smoke-asserts its endpoints — all before any image ships. - Need to reach the platform API? The in-cluster registry is
forgejo-http.forgejo.svc.cluster.local:3000; the provisioning API ishttp://mcp.mcp.svc.cluster.local.
guard-ppt
guard-ppt.yml runs on every push and PR. It fails when any workflow file uses the pull_request_target trigger — the one Actions trigger that hands base-repo secrets to untrusted PR code. It is a required status check on main for open-platform/mitosis and open-platform/app-template, so a reintroduction cannot merge there. Making it required on every app repo is a known open follow-up. The rest of the workflow surface and its controls are covered in Security Model.
The bootstrap chicken-and-egg
The runner pool's own container image is open-platform/ci-runner. On a fresh platform, nothing can build it, because every build needs a runner. Germination — the run that creates a new platform — breaks the cycle off-cluster.
Warning: Do not skip this step. Without it the platform stands up, but no CI ever runs.
On a k3d host, germinate runs docker build for the seeded ci-runner repo as git.<domain>/open-platform/ci-runner:<tag>, then imports it with k3d image import. Germinate reads the tag from the gitops pin, so the tag cannot skew. On a bare VPS with no Docker on the box, run a one-time docker build + docker save | ssh ... ctr images import from any Docker machine. The tag must match the gitops pin; the exact block is in Grow Your Own Platform.
Once the pool is alive, the ci-runner repo's own release workflow rebuilds the image through normal CI, and the loop is closed.
For everyone
Experience
- Build Your First App
- Building with Issues
- Dependency Trees
- Routines
- Orgs and Planning
- Preview Environments
- Connect Your Agent
Operate
Under the hood
Grow
This wiki ships inside open-platform/mitosis (wiki/) — edit it there, not here. Grown by the platform it describes.