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.ejofile. 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"]
ifis a template; the step runs when the result istrue,1oryes, and is skipped when it isfalse,0,noor empty. Any other result is an error.pathsare 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 apathsfilter 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_BUILDandKANIKO_BUILDneed a registry to push to. Define registries under Infrastructure > Registries and pick one on the step withregistry_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}}afterGIT_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.