Skip to main content

Pipelines

Pipelines bring pipeline-as-code CI/CD to Miabi. You describe the ordered steps that turn a specific commit into an image and a release — test, build, scan, deploy — and Miabi runs them on a build runner, streaming logs and per-step status to the console.

Pipeline runs

The spec

A pipeline is a declarative kind: Pipeline document. Keep it in your repo at .miabi/pipeline.yaml (versioned with the code) and register it from Workspace → Pipelines → New:

apiVersion: miabi.io/v1
kind: Pipeline
metadata: { name: web }
on:
push: { branches: [main] } # a push to main fires a run, pinned to that commit
manual: true # also runnable from the UI / API
schedule: "0 3 * * *" # optional cron (runs the app's branch HEAD)
env: # optional; applied to every step
NODE_ENV: production
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # resolved from the workspace vault
steps:
- name: test
image: node:20
env: { CI: "true" } # optional; adds to the pipeline's env
run: "npm ci && npm test"
- name: build
uses: build # builds the checked-out source, pushes it, captures the digest
dockerfile: docker/Dockerfile # optional; default "Dockerfile"
context: . # optional; default the repository root
- name: scan
image: aquasec/trivy:latest
continue-on-error: true # report vulnerabilities without blocking the deploy
run: "trivy image --exit-code 1 --severity HIGH,CRITICAL $MIABI_IMAGE"
- name: deploy
uses: deploy # deploys the built image by digest
Bind it to a Git-backed app

A pipeline is attached to an application whose source is a Git repo. The runner clones that repo once at the run's commit into a shared workspace (/workspace), then runs each step over it. The stored spec is what executes — the .miabi/pipeline.yaml file is your version-controlled source of truth, not auto-read (re-apply after editing).

What a pipeline builds

A pipeline is bound to one source, chosen when you create it. The binding decides what the runner checks out into the shared /workspace before the first step, and where a uses: build step pushes.

Bound toChecks outPushes touses: deploy
An applicationthe app's Git repository, at the run's commitws_<id>/<app-name>yes — deploys that app
A repositorythe registered Git repository, at the run's commitws_<id>/pl_<pipeline-name>no
Nothingnothing — steps run against an empty workspaceno

Binding to a repository is how you build and push an image without an application attached: a library, a base image, a tool — anything you want built from a repo but not deployed as a Miabi app. Pick the repository and, optionally, a branch.

Over the API the field is git_repository, and it takes a name or an id — the JSON type decides which, because an all-digit name is a valid handle:

"git_repository": "acme-api"   // the repository named acme-api
"git_repository": 3 // the repository with id 3
"git_repository": "123" // the repository *named* 123, not id 123

Prefer the name: repository names are immutable and unique per workspace, and unlike an id they mean the same thing on another install. Names resolve within the workspace only.

  • Branch is what manual and scheduled runs build. Leave it blank to use the repository's default branch. A push trigger always builds the commit it carries, whatever this says.
  • Images land under pl_<pipeline-name> in your workspace's registry namespace, kept apart from application images so a pipeline and an unrelated app of the same name never collide.
One source, not two

A pipeline binds to an application or a repository, never both — each supplies the checkout, so together there is no answer to what a run clones. Miabi refuses the combination.

The repository is picked from your registered Git repositories rather than typed as a URL. That keeps the choice of which repositories a workspace may reach inside its permissions, and reuses the credential already stored there.

Builds are uncached for a repository-bound pipeline

An application-bound build shares its layer cache with direct deploys of that app. A repository-bound pipeline has no application to key that cache off yet, so every run rebuilds from scratch.

A uses: deploy step needs an application, and a uses: build step needs a source. Both are rejected when you save the pipeline, not at the moment a runner picks it up.

Steps

Each step is either a container step (image + run) or a built-in (uses:).

Container steps — image + run

run: is a shell command executed in a non-login shell inside the step image — the same model as a GitHub Actions run: step, so pipes, &&, and environment expansion all work. It overrides the image's entrypoint, so images that ship their own entrypoint (e.g. aquasec/trivy) still run your script — write the full command, including the tool name.

  - name: lint
image: golangci/golangci-lint:latest
run: "golangci-lint run ./..."

uses: build

Turns the checked-out workspace into an image — a Dockerfile build or Cloud Native Buildpacks when no Dockerfile is present — pushes it to the built-in registry, captures the digest, and records an Image with build provenance (which runner produced it).

Three optional keys configure a Dockerfile build:

KeyDefaultDescription
dockerfileDockerfilePath to the Dockerfile, relative to the repository root.
contextthe repository rootBuild context directory, relative to the repository root.
build-argsARG values passed to the build (docker build --build-arg).

dockerfile and context are independent, exactly as in docker build -f <dockerfile> <context>. A monorepo commonly keeps its Dockerfile under docker/ while still building from the root:

  - name: build
uses: build
dockerfile: docker/Dockerfile

…or builds one service out of a subdirectory, with a Dockerfile that lives inside it:

  - name: build
uses: build
dockerfile: services/api/Dockerfile
context: services/api
build-args:
APP_ENV: production
VERSION: "1.4.0"

Both paths must stay inside the repository — an absolute path or one that climbs out with .. is rejected when the pipeline is saved, not at build time.

build-args are not secrets

A build argument is recorded in the image's own history, so anyone who can pull the image can read it back. Use them for versions, feature flags and build targets — never for tokens, keys or passwords. For credentials a build genuinely needs, use a container step with the run's environment instead, or fetch them inside the build from a service that authenticates the runner.

Build-arg names must be usable from a Dockerfile — letters, digits and underscores, not starting with a digit. Docker itself accepts APP ENV or APP=ENV and then produces an ARG nothing can reference, so Miabi rejects those names up front.

Version requirements

dockerfile works on any runner. context and build-args need Miabi <MIABI_VERSION> with a runner on <RUNNER_VERSION> or newer. Until both are in place, a pipeline using either is refused when you save it, with a message naming the key — rather than being accepted and silently ignored.

Buildpacks

When no Dockerfile is found at the resolved path, the runner falls back to Cloud Native Buildpacks. Buildpack builds need a docker-backed runner (MIABI_RUNNER_BUILDER=docker); the rootless BuildKit backend builds Dockerfiles only.

uses: deploy

Deploys the image the build step produced, by digest — no rebuild, no registry round-trip on the node. It targets the pipeline's bound app by default; app: <name-or-id> overrides the target. This produces a normal Miabi deployment, so releases, health checks, and rollback all apply.

Triggers

on: decides how a run starts — a run is always pinned to a specific commit:

  • push — a native webhook fires a run for the pushed commit (branch-filtered). See Git push deploy.
  • manual — the Run now button or POST …/trigger (HEAD of the app's ref, or a commit you pass).
  • schedule — a cron entry that runs the bound app's branch HEAD.

A git push can also start a run three ways (native webhook, CI calling the trigger API, or schedule) — full setup is in the pipeline example.

Environment & step outputs

Defining your own

env sets variables for every step; a step's own env adds to it and wins on a collision:

env:
NODE_ENV: production
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # resolved from the workspace vault
steps:
- name: test
image: node:22
env:
CI: "true"
run: npm ci && npm test

Values may reference a workspace secret as ${{ secrets.NAME }}. The reference is resolved by the control plane when the job is dispatched — a runner never reads your vault — and the resolved value is masked out of the live log stream. A reference to a secret that does not exist fails the run before it starts, rather than running a build with a blank token.

When the same name is set twice, the last one wins:

pipeline env  <  values exported via $MIABI_ENV  <  step env

The run itself records what you asked for, not what it resolved to: the run and step detail (and the API) show ${{ secrets.NPM_TOKEN }} verbatim, so a run stays a readable audit record and no plaintext credential is stored alongside it.

What the runner can see

A resolved secret travels to the runner and lives in that job's process environment for the duration of the step — the same trust already placed in a runner for registry credentials. A workspace's secrets are therefore only as protected as the runners registered to it, which is worth weighing before pointing a pipeline that references production credentials at a self-hosted runner on a shared machine.

caution

MIABI_* names are reserved for the build context below and are rejected at save time, so a pipeline cannot shadow the credentials the deploy step authenticates with. env is also rejected on a uses: step: a built-in step runs no container of its own, so the value would be accepted and silently dropped.

Getting a credential into a Dockerfile build

A uses: build step sees neither pipeline nor step env — it shells out to docker build, and only build-args cross that boundary. But a build arg is recorded in the image history, so anyone who can pull the image can read it back with docker history: it must not carry a secret.

Until BuildKit build secrets are supported, do the credentialed work in a container step before the build and leave the result in the shared /workspace, which the build context is read from:

  - name: npmrc
image: node:22
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: 'echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc'
- name: build
uses: build

Then make sure the file does not end up in the image — add it to .dockerignore, or write it outside the build context.

Provided by the runner

The runner exports run context to every step as environment variables:

VariableMeaning
MIABI_IMAGEImage ref the build step produced (repo:tag) — set for steps after build
MIABI_IMAGE_DIGESTImmutable repo@sha256:… form of the same image
MIABI_COMMITResolved commit the workspace was checked out at
MIABI_RUN_NUMBERPer-pipeline run counter
MIABI_WORKDIRThe shared workspace path inside steps (/workspace)
MIABI_REGISTRY / MIABI_REGISTRY_USER / MIABI_REGISTRY_TOKENRegistry host + per-job credentials (e.g. for a scanner to pull $MIABI_IMAGE)

Exporting values between steps — $MIABI_ENV

Any step can hand values to later steps by appending KEY=VALUE to $MIABI_ENV — the same contract as GitHub Actions' $GITHUB_ENV:

  - name: version
image: alpine
run: "echo VERSION=$(cat VERSION) >> $MIABI_ENV"
- name: tag
image: alpine
run: "echo building $VERSION" # later steps see it

MIABI_IMAGE is published through this same channel by the build step.

continue-on-error

By default any step that fails aborts the run. Set continue-on-error: true to let the run keep going and still succeed when that step fails (the step is still recorded failed) — for report-only stages like a vulnerability scan:

  - name: scan
image: aquasec/trivy:latest
continue-on-error: true
run: "trivy image --exit-code 1 --severity HIGH,CRITICAL $MIABI_IMAGE"

In the run view the step shows a red failed state with a continue-on-error marker and a "failure ignored" note, while the run itself is green — the same semantics as GitHub Actions.

Running & observing a run

Runs execute on a registered runner (a co-located built-in one ships for single-node/homelab). The run view shows a live stepper — each step transitions running → succeeded/failed in real time over SSE — with streaming logs you can filter per step, plus the built image (repository, digest, commit, size, runner). Every run records its resolved commit and produced image, and the history is retained for audit.

Relation to deployments

A pipeline's deploy step produces a deployment — the same release object you see everywhere in Miabi, so deployment history, health checks, and rollback all apply. Pipelines orchestrate how a release is produced; deployments are what gets produced. For the simplest path without build/test gates, a plain Git push deploy may be all you need.