Pipeline Reference

CI pipelines are defined in .forge/pipeline.toml at the root of your repository.

Basic Example

[pipeline]
triggers = ["push"]

[[jobs]]
name = "build"
image = "docker.io/library/alpine:3.19"
commands = [
  "apk add --no-cache make",
  "make build",
]
timeout_seconds = 600

[[jobs]]
name = "test"
depends_on = ["build"]
commands = ["make test"]
timeout_seconds = 300

Pipeline Section

[pipeline]
triggers = ["push"]
Field Required Description
triggers yes List of trigger types: "push", "merge_queue", "manual"

Trigger types:

  • push -- runs automatically when commits are pushed to a tracked branch
  • merge_queue -- runs when the merge queue rebases a change graph onto trunk
  • manual -- triggered manually from the web UI

Job Definition

[[jobs]]
name = "build"
image = "docker.io/library/alpine:3.19"
commands = ["make build"]
depends_on = ["setup"]
labels = ["linux", "x86_64"]
timeout_seconds = 600
secrets = ["NPM_TOKEN", "DEPLOY_KEY"]
Field Required Default Description
name yes -- Unique job identifier
commands yes -- Shell commands to execute sequentially
depends_on no [] Job names that must pass before this job runs
image no none Container image (Podman). Omit to run natively
labels no [] Required runner labels for scheduling
timeout_seconds no 600 Maximum execution time in seconds
secrets no [] Secret names to inject as environment variables

Execution Modes

Native: When image is omitted, commands run directly on the runner via sh -c. The runner must have all required tools installed.

Container: When image is set, commands run inside a rootless Podman container. The repository is bind-mounted into /work.

Artifacts

Artifacts allow passing files between jobs.

[[jobs]]
name = "build"
commands = ["make build"]

[[jobs.artifacts]]
name = "build-output"
path = "dist/"
direction = "upload"

[[jobs]]
name = "deploy"
depends_on = ["build"]
commands = ["./deploy.sh"]

[[jobs.artifacts]]
name = "build-output"
path = "dist/"
direction = "download"
Field Required Description
name yes Artifact identifier (unique per job)
path yes Relative path to upload or extract to
direction yes "upload" or "download"

Rules:

  • Upload artifacts are packaged as tar.gz after the job's commands complete
  • Download artifacts must reference a name uploaded by a dependency job
  • Artifact names must be unique within a job
  • Artifacts are retained for 7 days by default (server-configurable)

Secrets

Secrets are encrypted at rest and injected as environment variables at runtime.

[[jobs]]
name = "deploy"
secrets = ["DEPLOY_TOKEN", "AWS_SECRET_KEY"]
commands = ["./deploy.sh"]

Manage secrets via the repository settings page or API:

# Create a secret
curl -X POST https://bithyle.com/api/v1/ns/repo/secrets \
  -H "Authorization: ..." \
  -d '{"name": "DEPLOY_TOKEN", "value": "secret-value"}'

# List secret names
curl https://bithyle.com/api/v1/ns/repo/secrets \
  -H "Authorization: ..."

Secret values are:

  • Encrypted with AES-256-GCM before storage
  • Decrypted server-side and included in the job payload
  • Scrubbed from log output (both runner-side and server-side)

Dependencies and DAG

Jobs form a directed acyclic graph (DAG) via depends_on:

[[jobs]]
name = "lint"
commands = ["make lint"]

[[jobs]]
name = "build"
commands = ["make build"]

[[jobs]]
name = "test"
depends_on = ["build"]
commands = ["make test"]

[[jobs]]
name = "deploy"
depends_on = ["lint", "test"]
commands = ["make deploy"]

Job states:

Pending --(deps pass)--> Ready --(runner claims)--> Running --> Passed
                                                          \--> Failed
                                                          \--> Cancelled
  • Pending -- waiting for dependencies
  • Ready -- all deps passed, waiting for a runner
  • Running -- claimed and executing
  • Passed / Failed -- terminal
  • Cancelled -- a dependency failed

If any job fails, all dependent jobs are cancelled.

Validation

The pipeline parser validates:

  • No empty or duplicate job names
  • All depends_on entries reference existing jobs
  • No dependency cycles
  • No duplicate artifact names within a job
  • Download artifacts reference an upload in a dependency
  • Secret names are non-empty

Invalid pipelines are rejected at push time with a descriptive error message.

Full Example

[pipeline]
triggers = ["push", "merge_queue"]

[[jobs]]
name = "deps"
image = "node:20-alpine"
commands = ["npm ci"]

[[jobs.artifacts]]
name = "node-modules"
path = "node_modules/"
direction = "upload"

[[jobs]]
name = "lint"
depends_on = ["deps"]
image = "node:20-alpine"
commands = ["npm run lint"]

[[jobs.artifacts]]
name = "node-modules"
path = "node_modules/"
direction = "download"

[[jobs]]
name = "test"
depends_on = ["deps"]
image = "node:20-alpine"
commands = ["npm test"]
timeout_seconds = 300

[[jobs.artifacts]]
name = "node-modules"
path = "node_modules/"
direction = "download"

[[jobs]]
name = "build"
depends_on = ["lint", "test"]
image = "node:20-alpine"
commands = ["npm run build"]
secrets = ["API_KEY"]

[[jobs.artifacts]]
name = "node-modules"
path = "node_modules/"
direction = "download"