Your code, your hardware, your gate.
GATillForge CI runs the jobs you declare in .tillforge/ci.yml on runners you host. A push, a pull request, or a manual trigger freezes each job’s spec at the exact commit and queues it; a registered runner claims it, runs every step in its own locked-down container, and reports a check back. Those checks feed the same branch protection and merge queue that guard your branches — CI plugs into the gate, it doesn’t sit beside it. Deploys ride the same runner.
Declare jobs in .tillforge/ci.yml
Commit a .tillforge/ci.yml at the repo root. It’s read at the exact commit that triggered the run and frozen into the job — a config push mid-queue never retargets a job already waiting. A repo with no file simply runs no CI; a present-but-invalid file surfaces one visible error run rather than failing silently.
# .tillforge/ci.yml — read at the exact commit, frozen into each job.
# 1–10 jobs; each runs in its own locked-down container on your runner.
jobs:
test:
image: node:22-alpine # any plain container reference (default: alpine:3)
timeout: 900 # seconds, 30–3600 (default 900)
env: # up to 20 UPPER_SNAKE keys
CI: "true"
steps: # 1–50 shell steps, run in order; non-zero fails
- npm ci
- npm test
lint:
image: node:22-alpine
steps:
- npm ci
- npm run lint| Field | Rule |
|---|---|
jobs | 1–10 named jobs; names are 1–32 chars of [a-z0-9._-]. |
image | Any plain container reference. Default alpine:3. |
steps | 1–50 shell steps, run in order; the first non-zero exit fails the job. |
timeout | Seconds, 30–3600. Default 900. |
env | Up to 20 UPPER_SNAKE keys, values ≤ 500 chars. |
Register and run a runner
A runner is a machine you own that executes queued jobs — nothing runs on shared infrastructure. Register one under Forge → Runners to mint a tfr_ token (shown once, stored only as a hash), drop it into the runner’s env file, and start tillforge-runner on an isolated host.
# 1 · Register a runner in the dashboard (Forge → Runners). The tfr_ token
# is shown ONCE and stored only as a hash. Put it in the runner's env file:
# /etc/tillforge/tillforge-runner.env
TILLFORGE_RUNNER_URL=https://tilldev.dev
TILLFORGE_RUNNER_TOKEN=tfr_xxxxxxxxxxxxxxxx
TILLFORGE_RUNNER_NAME=ci-1
# 2 · Install tillforge-runner on an isolated machine and start it. It claims
# the oldest queued job for your org, runs each in its own container, streams
# logs back, and writes a ci/<job> check. With nothing queued it simply waits,
# polling for work — no jobs run until you push, open a PR, or trigger by hand.Checks feed branch protection
When a job finishes it writes a ci/<job> check against the commit. Name those checks in a branch’s protection rule under required checks and a pull request can’t merge until they pass — and if the branch uses the merge queue, the queue re-runs them on the exact merge result, not just the branch tip. This is why CI lives inside the same machinery as reviews and signed commits: one gate, every requirement.
Push, pull request, or on demand
Pushes and pull requests enqueue CI automatically. Trigger a run by hand — for a re-check or a branch that predates the config — from the CLI:
# Runs fire on push and on pull requests automatically. Trigger one by hand:
tilldev forge ci run --repo acme-api # default branch tip
tilldev forge ci run --repo acme-api --ref feat/x # a specific ref
tilldev forge ci list --repo acme-api # runs, newest first
tilldev forge ci logs <job-id> --repo acme-api # one run's full logDeploys ride the same runner
A deployment is a CI-shaped job the same runner claims, clones, and builds — then a final step ships the output through your provider’s CLI (or, for a desktop bundle, signs and publishes native installers). The provider credential is decrypted only at claim time and injected into that one build’s environment — never stored in the job, never printed in the log. So the runner you stand up for CI is the same one that ships production.
Next: Branch protection to require these checks, or Deployments to ship on the same runner. Back to the TillForge overview.