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 branchmerge_queue-- runs when the merge queue rebases a change graph onto trunkmanual-- 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 dependenciesReady-- all deps passed, waiting for a runnerRunning-- claimed and executingPassed/Failed-- terminalCancelled-- 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_onentries 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"