Docs

Documentation / Pipelines

Pipeline definition

The ejo.yaml schema; step kinds, depends_on, matrix, if, paths, cache, artifacts, outputs and manual run inputs.

Where a pipeline is defined#

You can define a pipeline in three places; all three use the same schema:

  • In the console, under Delivery > Pipelines.
  • In a file in the repository, usually ejo.yaml. When the binding's Pipeline source is From repo — ejo.yaml is source of truth, EJO reads the file from the bound branch and updates the pipeline when the file changes.
  • In the pipeline: block of the .ejo file. See The .ejo file.

The schema is strict: an unknown key (a typo such as usees:) is an error.

A first pipeline#

name: web
description: Build, scan and deploy to Kubernetes
steps:
  - name: Clone
    kind: GIT_CLONE
  - name: Build
    kind: KANIKO_BUILD
    config:
      dockerfile_path: Dockerfile
      registry_id: "{{.REGISTRY_ID}}"
    depends_on: [0]
  - name: Tests
    kind: SHELL
    image: node:20-alpine
    config:
      command: npm ci && npm test
    depends_on: [0]
  - name: Deploy
    kind: K8S_DEPLOY
    config:
      image: "{{.ImageTag}}"
      container_port: 3000
      wait_ready: true
      wait_seconds: 120
    depends_on: [1, 2]

Top-level fields#

Field Required Description
name Yes The pipeline name.
description No A description.
steps Yes At least one step.
inputs No Values asked for on a manual run. See "Manual run inputs" below.

Step fields#

Field Description
name Required. The step name.
kind The step kind, for example SHELL, DOCKER_BUILD, K8S_DEPLOY. Cannot be used together with uses.
config Settings for the step kind. Values may contain {{.VARIABLE}} templates.
uses A step pack step: <pack>/<step>@<version>. See Step packs.
with Inputs of a uses step.
image SHELL steps only: runs the command in this container image.
depends_on Positions (starting at 0) of the steps this step waits for.
continue_on_failure With true the pipeline continues even if this step fails.
matrix Repeats the step for every combination of the axes.
if A condition; the step runs when it evaluates to true.
paths A filter on changed files (list of globs).
runs_on Only agents with these labels may run the step.
cache Directories kept between runs.
artifacts Files the step uploads.
needs_artifacts Artifacts downloaded before the step starts.
id_tokens SHELL steps only: EJO OIDC tokens. See Secrets and variables.
test_reports JUnit reports the step writes (list of globs).
parallelism Splits the step into 2 to 16 shards.
split With parallelism, spreads test files by their past durations: split.files.

Order and dependencies#

  • Without depends_on, a step waits for the step before it (a sequential flow).
  • depends_on: [] means the step can start right away.
  • depends_on: [1, 2] makes the step wait for both steps 1 and 2; independent steps run in parallel.
  • Cycles and self-dependencies are rejected.

Matrix#

- name: Test
  kind: SHELL
  image: python:3.12-slim
  matrix:
    suite: [unit, integration]
    db: [postgres, sqlite]
  config:
    command: pytest tests/{{.MatrixSuite}} --db={{.MatrixDb}}

Every combination runs as its own step. Axis values are available in the step as {{.MatrixSuite}} and {{.matrix_suite}}. A step can expand to at most 32 combinations. A step that depends on a matrix step waits for every combination.

Conditions: if and paths#

- name: Deploy to production
  kind: K8S_DEPLOY
  if: '{{eq .Branch "main"}}'
  paths: ["src/**", "Dockerfile"]
  • if is a template; the step runs when the result is true, 1 or yes, and is skipped when it is false, 0, no or empty. Any other result is an error.
  • paths are GitHub Actions style globs: * and ? do not cross a directory boundary, ** does. The step runs when any changed file matches. In runs where the changed files are unknown (manual, schedule, tag) the step always runs. EJO only gets the list of changed files from GitHub events; on GitLab and Azure DevOps repositories, steps with a paths filter always run.

Cache and artifacts#

- name: Build
  kind: SHELL
  image: node:20-alpine
  config:
    command: npm ci && npm run build
  cache:
    key: npm
    key_files: [package-lock.json]
    paths: [node_modules]
  artifacts:
    name: dist
    paths: [dist]
    when: on_success
    expire_in: 30d
Field Description
cache.key, cache.key_files The cache key and files added to it.
cache.paths Directories to keep. They must be inside the working directory; paths starting with ~/ are rejected.
cache.restore_keys Keys to try when there is no exact match.
artifacts.name, artifacts.paths Name and files of the artifact to upload.
artifacts.when on_success (default) or always (upload even if the step fails).
artifacts.expire_in How long to keep the artifact: a number and a unit (30d, 2w, 12h, 90m) or never. Empty uses the organization's artifact retention (default: the global artifacts.retention_days, 14). never means no time-based expiry; it does not pin the artifact (only an admin pin does), so the organization's artifact quota can still delete it. Checked when the pipeline is saved.
needs_artifacts[].name Name of the artifact to download. By default it comes from the latest successful run of the same binding for the same commit.
needs_artifacts[].from_step Name of the step that produced it (optional).
needs_artifacts[].source, artifact_id artifact_id picks one specific artifact.

Caches are kept on the agent's disk and also on the master (the remote cache), shared by every agent of the same repository binding. A restore tries the agent's disk first, then the master. Entries are removed when the organization's cache quota (default 5 GB) is exceeded, oldest hit first, or when they have not been hit for 14 days. Runs for a pull_request can read the branch caches but write only under their own pr-<number>- keys, and branch runs never restore those. A pull_request run never reads another pull request's keys, and a cache key or restore_keys entry cannot start with pr- (the pipeline is refused when saved; templates in keys must also parse). Manage the entries of a repository on its Caches tab.

Artifacts are deleted by an hourly sweeper once they pass their expire_in (or the retention default). Three kinds are never deleted: pinned artifacts (the pin button on the deployment's artifact list), the artifacts of the latest successful run that has artifacts, for each repository binding and pipeline, and the artifacts of the run that is currently deployed to a production environment (the newest successful run there, plus the build it promoted). The list marks the last two with a "kept" badge. After expiry the sweeper also enforces the organization's artifact quota (Settings > Organization > Storage; empty means unlimited, which is the default on self-hosted installs) by deleting the oldest unprotected artifacts first. Only artifacts of finished runs that are at least an hour old are deleted for the quota; protected, recent or unfinished artifacts can keep the total above it, and the master logs a warning when only such data remains.

Image build cache#

DOCKER_BUILD and KANIKO_BUILD can keep image layers in the registry the image is pushed to, so a build on a fresh agent or a fresh Kaniko pod reuses them.

- name: Build image
  kind: KANIKO_BUILD
  config:
    registry_id: "..."
    cache: registry          # off | registry (true also means registry)
    cache_repo: registry.example.com/team/app/cache   # optional
    cache_ttl: 168h          # optional, KANIKO_BUILD only
Key Description
cache registry turns the registry layer cache on; off (or leaving it out) keeps today's behavior. Blueprints and new steps created in the console start with registry; steps that already exist are not changed.
cache_repo Where the layers go. It must be on the same registry as the image, because the step only has credentials for that registry (the same credentials push the image and the cache). KANIKO_BUILD default: <image repository>/cache. DOCKER_BUILD default: <image repository>:buildcache.
cache_ttl KANIKO_BUILD only: how long a cached layer is reused, as a duration (168h) or days (7d). Default 168h.

KANIKO_BUILD passes --cache=true --cache-repo=... --cache-ttl=... to Kaniko. DOCKER_BUILD uses docker buildx build --cache-from type=registry,ref=... --cache-to type=registry,ref=...,mode=max --load. Without a registry_id there is nowhere to keep the cache, and when docker buildx is missing or the agent's builder cannot export a registry cache (the default docker driver needs the containerd image store, or a docker-container builder), the step logs a warning and builds with the Docker daemon's own layer cache. Without a cache key DOCKER_BUILD always builds that way.

The image cache is read-only for pull_request runs: they reuse the layers of branch builds but never write any (KANIKO_BUILD adds --no-push-cache, DOCKER_BUILD leaves out --cache-to), so untrusted code cannot seed layers a branch build would trust. Saving a pipeline checks cache, cache_ttl and cache_repo.

Notes for specific registries: Amazon ECR needs the cache repository to exist (for Kaniko, create <image repository>/cache or set cache_repo), and for BuildKit EJO adds image-manifest=true,oci-mediatypes=true to the cache export on ECR hosts. Cache layers are written under the same repository namespace as the image, so any credential that can push the image can push its cache. Kaniko works with every registry that accepts a push; the cache repository grows with every distinct layer until the registry's own lifecycle policy cleans it up, so set one on registries that bill by storage.

Step kinds#

The most common config keys are listed below. The console's step form shows every field with its description.

Kind What it does Common config keys
GIT_CLONE Clones the repository at the run's commit. depth (default 1)
SHELL Runs a shell command. command, working_dir, outputs; image on the step
DOCKER_BUILD Builds an image with Docker on the agent host; pushes it with registry_id. dockerfile_path, context_path, image_tag, build_args, registry_id, repository, cache, cache_repo
PODMAN_BUILD Builds an image with podman build on the agent host (no Docker daemon); pushes it with registry_id. Needs podman on the agent host; not available on a Kubernetes agent (use KANIKO_BUILD). dockerfile_path, context_path, image_tag, build_args, target, platform, format (oci or docker), layers, registry_id, repository, cache, cache_repo, cache_ttl. platform is one os/arch[/variant]; build_args values are plain scalars and must not be secrets.
TERRAFORM Runs terraform in the hashicorp/terraform image with the pipeline's OIDC cloud identity. plan stores the plan file and a summary (shown on the run page and in the approval card, posted on pull requests); apply applies the plan of the same deployment; destroy needs allow_destroy and an approving environment. Needs a Docker agent (terraform capability). command (plan, apply, destroy), working_dir, workspace, vars, var_files, backend_config, version, allow_destroy, auto_plan, lock_timeout
KANIKO_BUILD Builds and pushes on Kubernetes without a Docker daemon. dockerfile_path, image_tag, registry_id, repository, namespace, cache, cache_repo, cache_ttl
DOCKER_RUN Runs a container on the Docker host (replacing the previous one). container_name, image_tag, ports, env, wait_ready, healthcheck_url
DOCKER_COMPOSE_UP Brings up the repository's compose file. compose_file, project_name
DOCKER_STOP Stops and removes a container (PR teardown). container_name
K8S_DEPLOY Creates or updates a Deployment and a Service. image, name, namespace, replicas, container_port, env, wait_ready, wait_seconds
K8S_APPLY Applies the manifest you give with kubectl apply. manifest, namespace
K8S_DELETE Deletes a Deployment and Service (PR teardown). name, namespace
KUSTOMIZE_DEPLOY Applies the repository's kustomization with the image swapped, or writes it to a GitOps repository. path, image_name, image, strategy, gitops_repo_url, gitops_branch
HELM_UPGRADE Runs helm upgrade --install. chart, release, namespace, values_file, set, image_set, wait
HELM_UNINSTALL Removes a Helm release. release, namespace
TRIVY_SCAN Scans the image with Trivy (Kubernetes Job). image_ref, severity (default HIGH,CRITICAL), exit_code, ignore_unfixed
SBOM_GENERATE Produces a CycloneDX SBOM (Kubernetes Job). image_ref
COSIGN_SIGN Signs the image with cosign (Kubernetes Job). image_ref, tlog_upload
SONAR_SCAN Runs a SonarQube analysis. project_key, sources, quality_gate_wait
ARGOCD_SYNC Waits for, or starts, the sync of an Argo CD Application. instance, application, mode, revision, timeout
CLOUD_RUN_DEPLOY Deploys the image to Google Cloud Run with gcloud run deploy and the pipeline's OIDC identity (no stored key). Optional canary by revision traffic. service, region, image, env, secrets (Secret Manager references), cpu, memory, min_instances, max_instances, concurrency, port, allow_unauthenticated, service_account, strategy, cli_image, cloud
ECS_DEPLOY Registers a new task definition revision with one container's image replaced, updates the Amazon ECS service and waits for services-stable; rolls back to the previous task definition on failure. cluster, service, container_name, image, env, wait_seconds, circuit_breaker, circuit_breaker_rollback, desired_count, cli_image, cloud
AZURE_CONTAINERAPP_DEPLOY Updates an Azure Container App image with az containerapp update. Optional canary by revision weight. name, resource_group, image, env, secret_env, cpu, memory, min_replicas, max_replicas, wait_seconds, strategy, cli_image, cloud
LAMBDA_DEPLOY Updates an AWS Lambda function from a container image or a zip (zip_file), publishes a version and moves an alias to it. Optional canary by alias routing weights. function_name, region, alias, image, zip_file, env, memory_size, function_timeout, strategy, cli_image, cloud
CLOUD_FUNCTIONS_DEPLOY Deploys a Cloud Functions 2nd gen (HTTP) function from a source directory with gcloud functions deploy --gen2. Optional canary by Cloud Run revision traffic. function_name, region, runtime, entry_point, source_dir, env, secrets, memory, min_instances, max_instances, function_timeout, allow_unauthenticated, service_account, strategy, cli_image, cloud

Note: DOCKER_BUILD, PODMAN_BUILD and KANIKO_BUILD need a registry to push to. Define registries under Infrastructure > Registries and pick one on the step with registry_id. The automatic SBOM and scan also only run for images pushed to a registry.

Variables and templates#

config values may use {{.Name}} templates. The agent fills them in when the run starts.

Variable Example Description
Branch main The branch being deployed.
CommitSHA 9fceb02d0ae5… The full commit hash.
ShortSHA 9fceb02 The first characters of the commit hash.
Event commit The event that started the run.
IsPR false true when a pull request started the run.
RepoFullName acme/web The repository name with its owner.
RepoSlug acme-web The repository name, safe for resource names.

In addition:

  • Your variables: every key you define under Infrastructure > Variables is available as {{.KEY}}. See Secrets and variables.
  • Outputs of earlier steps: for example {{.ImageTag}} and {{.ImageDigest}} after a build step, {{.WorkDir}} after GIT_CLONE.

Step outputs#

Each step kind produces certain outputs:

Kind Outputs
GIT_CLONE WorkDir
DOCKER_BUILD, PODMAN_BUILD, KANIKO_BUILD ImageTag, ImageDigest
TERRAFORM TerraformAdd, TerraformChange, TerraformDestroy, TerraformChanges
K8S_DEPLOY K8sDeploymentName, K8sServiceName, K8sDeployNamespace, PreviewURL
HELM_UPGRADE HelmRelease, HelmNamespace
CLOUD_RUN_DEPLOY, AZURE_CONTAINERAPP_DEPLOY ServiceURL, Revision, PreviousRevision (Azure also Fqdn)
LAMBDA_DEPLOY FunctionVersion, PreviousVersion, FunctionArn, FunctionAlias (ServiceURL if the alias has a function URL)
CLOUD_FUNCTIONS_DEPLOY ServiceURL, Revision, PreviousRevision
ECS_DEPLOY TaskDefinitionArn, PreviousTaskDefinitionArn
TRIVY_SCAN ScanCritical, ScanHigh, ScanMedium, ScanLow, ScanFindingTotal and more
COSIGN_SIGN SignedImage, SignatureKeyID
SBOM_GENERATE SbomFormat, SbomComponents

A SHELL step writes its own outputs to the $EJO_OUTPUT file as NAME=value lines and lists them under config.outputs:

- name: Find the version
  kind: SHELL
  image: alpine:3.20
  config:
    command: echo "APP_VERSION=$(cat VERSION)" >> "$EJO_OUTPUT"
    outputs: [APP_VERSION]
- name: Use it
  kind: SHELL
  image: alpine:3.20
  config:
    command: echo "Version {{.APP_VERSION}}"

Outputs cannot overwrite system variables such as WorkDir, CommitSHA or Branch.

Manual run inputs#

A pipeline can ask for values on runs started by hand:

name: web
inputs:
  - {name: target, options: [staging, production], default: staging}
  - {name: dry_run, type: bool, default: "false"}
steps:
  - name: Show
    kind: SHELL
    image: alpine:3.20
    config:
      command: echo "target=$INPUT_TARGET dry_run=$INPUT_DRY_RUN"
  • Input fields: name, type (string, number, bool), required, default, options, pattern, label, help.
  • Values reach every step as INPUT_<NAME> environment variables.
  • Required inputs are only asked for on manual runs. Push and tag runs use the defaults; for a schedule, every required input needs a default.
  • Inputs are stored in the clear, so they cannot be secret. Read secrets from variables instead.

Next steps#

Last updated: October 21, 2026