DocsGit & code

CI/CD pipelines

The job DAG, scheduling and dispatch, execution isolation, secrets, environments, and built-in DORA metrics.

The job graph#

A pipeline is a directed acyclic graph of jobs, each with:

FieldMeaning
needsNames of jobs that must reach a terminal state before this job is eligible to run — this is what makes it a DAG rather than a flat list.
runWhenALWAYS, ON_FAILURE, or the default (on success) — controls whether the job runs given its dependencies' outcomes; a job whose condition isn't met is marked SKIPPED, not silently omitted.
requiredTags (tags:)A runner only claims this job if it advertises every tag listed — e.g. tags: [gpu] restricts a job to runners started with RUNNER_TAGS=gpu. An untagged job runs on any runner.
cacheKeyJobs sharing a cache key share a persistent workspace subdirectory across runs (e.g. for node_modules), avoiding a full reinstall every pipeline.
approvalStatus (approvers)When set, the job is held at PENDING regardless of its dependencies until a permitted approver acts — used to gate production deploy jobs behind a manual sign-off.
downstream triggersOn success, a job can enqueue a pipeline run in a different repository — the mechanism behind multi-repository release trains.

Build this graph visually in Pipelines → Editor, or author .khub/ci.yml directly; the editor and the YAML parser both compile down to the same internal job-graph representation, so either path produces identical runtime behavior.

Scheduling and dispatch#

Pipeline creation inserts every job row as PENDING. services/runner computes, after each job reaches a terminal state, which sibling jobs just became eligible (all of needs terminal, condition satisfied, not still gated on approval) and enqueues them onto a Redis-backed BullMQ queue named ci-jobs.

A runner that dequeues a job whose requiredTags it doesn't satisfy declines it by throwing rather than executing — BullMQ's retry/backoff (up to 20 attempts, exponential backoff starting at 5s) then gives every other runner in the fleet a turn to claim it before the job is ever reported as unschedulable. This "decline and retry" pattern is what lets a heterogeneous runner fleet (some tagged gpu, some not) share one queue without a separate scheduler process making placement decisions.

Execution isolation#

Each job runs in its own Docker container, created fresh per job (never reused across jobs or pipelines):

  • CapDrop: ["ALL"] and SecurityOpt: ["no-new-privileges"] — the job process gets no Linux capabilities beyond an unprivileged user's defaults, and cannot regain privileges even via a setuid binary in its image.
  • Memory and memory+swap are both capped at the runner's configured jobMemoryMb; CPU is capped via NanoCpus; process count is capped at 512 (PidsLimit) to bound fork-bombs.
  • Networking is a private bridge network per container — jobs can reach the public internet (to npm install, pull images, etc.) but not the host's other containers or the Docker socket.
  • The job's workspace and cache directory are mounted in via a volume subpath (VolumeOptions.Subpath) rather than a bind mount from the host filesystem, so a job cannot traverse outside the directory scoped to it even with an escaped container.
  • Job stdout/stderr is streamed to a per-job log file and, in the same pass, run through a secret masker that redacts any resolved secret value it can match — a job that does echo $DEPLOY_TOKEN gets a masked log line, not a leaked credential, without needing the job author to remember to suppress it.

Secrets#

Secrets are scoped to ORGANIZATION, PROJECT, or REPOSITORY and cascade — a job's environment is the union of all three scopes that apply to it, with more specific scopes able to shadow a same-named broader one. Values are stored encrypted at rest and decrypted only inside the runner process immediately before injecting them into the job's container environment. A secret can additionally be marked protected, in which case it's withheld entirely from jobs running on a non-protected branch — so a production database credential can be made unavailable to a pipeline run on an arbitrary feature branch, even one opened by a maintainer.

Environments & deployments#

An Environment (e.g. staging, production) tracks the currently-deployed pipeline/commit and its deployment history. Redeploy re-runs the exact same deploy job against the same artifact; rollback targets a specific prior successful deployment — both are ordinary pipeline job executions, not a separate deployment engine, which is why they show up in the same job logs and status model as everything else.

Pipeline status#

A pipeline's overall status is derived, not stored independently: computed from the set of its jobs' statuses every time a job reaches a terminal state (PASSED, FAILED, CANCELED, SKIPPED). Once every job is terminal, the runner notifies the API over an internal endpoint so it can fire the PIPELINE_COMPLETED webhook event and update DORA metrics.

Engineering insights (DORA metrics)#

An organization's Insights page computes the four DORA metrics directly from pipeline and deployment history — no separate instrumentation or opt-in is required:

  • Deployment frequency — count of successful deployments to production-tagged environments over time.
  • Lead time for changes — elapsed time from a commit landing on the default branch to its deployment.
  • Change failure rate — proportion of deployments followed by a rollback or a failed pipeline against the same environment within a short window.
  • Mean time to recovery — elapsed time between a failed deployment and the next successful one to the same environment.