Guides
Workflow Syntax
FeatherCI workflows are defined in .featherci/workflow.yml at the root of your repository.
Basic Structure
name: My Pipeline
on:
push:
pull_request:
steps:
- name: build
image: golang:1.22
commands:
- go build ./...
A workflow has three top-level keys: name, on (triggers), and steps.
Triggers
The on key controls when the workflow runs.
Push triggers — run on every push, or filter by branch/tag patterns (glob supported):
on:
push:
branches: [main, release/*]
tags: [v*]
Pull request triggers — run on all PRs, or filter by target branch:
on:
pull_request:
branches: [main]
Combined:
on:
push:
branches: [main, develop]
pull_request:
Steps
Each step runs inside an isolated Docker container.
steps:
- name: test # Unique identifier (required)
image: node:20 # Docker image (required for command steps)
commands: # Shell commands to execute
- npm install
- npm test
Environment Variables
- name: deploy
image: alpine:latest
env:
NODE_ENV: production
API_URL: https://api.example.com
commands:
- ./deploy.sh
Reference secrets with $SECRET_NAME:
env:
DEPLOY_TOKEN: $DEPLOY_TOKEN
Working Directory
- name: test-frontend
image: node:20
working_dir: /workspace/frontend
commands:
- npm test
Timeout
Set a maximum execution time in minutes (default: 60):
- name: long-test
image: golang:1.22
timeout_minutes: 120
commands:
- go test -race ./...
Continue on Error
- name: lint
image: golang:1.22
continue_on_error: true
commands:
- golangci-lint run
Dependencies
Use depends_on to control execution order. Steps without dependencies run in parallel.
steps:
- name: test
image: golang:1.22
commands:
- go test ./...
- name: lint
image: golang:1.22
commands:
- golangci-lint run
# Runs after both test and lint succeed
- name: build
image: golang:1.22
depends_on: [test, lint]
commands:
- go build -o app ./cmd/app
FeatherCI renders this as a visual DAG in the build view.
Conditional Steps
Use the if field to run a step only when a condition is met:
- name: deploy
image: alpine:latest
if: branch == "main"
commands:
- ./deploy.sh
| Variable | Description |
|---|---|
branch | The branch being built |
| Operator | Description | Example |
|---|---|---|
== | Equals | branch == "main" |
!= | Not equals | branch != "develop" |
=~ | Glob match | branch =~ "release/*" |
!~ | Glob not match | branch !~ "feature/*" |
Values must be quoted with double quotes.
Service Containers
Run sidecar containers (databases, caches, etc.) alongside your step. Services are accessible by hostname on a shared Docker network.
- name: integration-test
image: ruby:3.4
services:
- image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: app_test
- image: redis:7
env:
DATABASE_URL: mysql2://root:test@mysql/app_test
REDIS_URL: redis://redis:6379
commands:
- bundle exec rspec spec/integration
The hostname is derived from the image name: mysql:8.0 → mysql, redis:7 → redis. Services are started before the step and cleaned up automatically after it completes.
Full Example
name: Production Pipeline
on:
push:
branches: [main, develop]
pull_request:
steps:
- name: test
image: golang:1.22
timeout_minutes: 30
cache:
key: go-{{ checksum "go.sum" }}
paths:
- /go/pkg/mod
commands:
- go test -v -race ./...
- name: lint
image: golang:1.22
continue_on_error: true
commands:
- golangci-lint run
- name: build
image: golang:1.22
depends_on: [test, lint]
commands:
- CGO_ENABLED=0 go build -o app ./cmd/app
- name: staging-approval
type: approval
depends_on: [build]
- name: deploy-staging
image: alpine:latest
depends_on: [staging-approval]
if: branch == "main"
secrets: [DEPLOY_KEY, STAGING_HOST]
env:
DEPLOY_KEY: $DEPLOY_KEY
DEPLOY_HOST: $STAGING_HOST
commands:
- ./deploy.sh staging
Secrets
FeatherCI provides encrypted secret storage for credentials, tokens, and other sensitive values. Secrets are encrypted at rest using AES-256-GCM and injected as environment variables during builds.
Adding Secrets
- Navigate to your project
- Go to Secrets (in the project sidebar)
- Enter a name and value
- Click Add Secret
Secret values are encrypted immediately and can never be viewed again through the UI.
Using Secrets in Workflows
1. Declare in the secrets list:
- name: deploy
image: alpine:latest
secrets: [DEPLOY_TOKEN, SSH_KEY]
commands:
- ./deploy.sh
The listed secrets are made available as environment variables in the container.
2. Map to environment variables with env:
- name: deploy
image: alpine:latest
secrets: [DEPLOY_TOKEN]
env:
MY_TOKEN: $DEPLOY_TOKEN
commands:
- echo "Deploying with token..."
- ./deploy.sh
Log Masking
Secret values are automatically masked in build logs. If a command outputs a secret value, it is replaced with *** in the log output.
Security Details
- Encryption: AES-256-GCM with a 32-byte key derived from
FEATHERCI_SECRET_KEY - Storage: Encrypted values stored in the SQLite database
- Access: Secrets are scoped to the project they belong to
- Exposure: Values cannot be retrieved through the UI or API after creation
- Deletion: Secrets can be deleted through the project's secrets page
Caching
Build caching stores directories between builds so you don't re-download dependencies every time. Caches are stored on the local filesystem at FEATHERCI_CACHE_PATH (default: ./cache).
Basic Usage
- name: test
image: node:20
cache:
key: node-{{ checksum "package-lock.json" }}
paths:
- node_modules
commands:
- npm ci
- npm test
Before the step runs, FeatherCI restores the cached directories if a cache entry matches the key. After the step completes, the paths are saved back.
Cache Keys
| Template | Description |
|---|---|
{{ checksum "file" }} | SHA-256 hash of the file contents |
{{ .Branch }} | Current branch name |
Examples by Language
Go:
cache:
key: go-{{ checksum "go.sum" }}
paths:
- /go/pkg/mod
Node.js:
cache:
key: node-{{ checksum "package-lock.json" }}
paths:
- node_modules
Python:
cache:
key: pip-{{ checksum "requirements.txt" }}
paths:
- /root/.cache/pip
Rust:
cache:
key: cargo-{{ checksum "Cargo.lock" }}
paths:
- /usr/local/cargo/registry
- target
Cache Behavior
- Restore: Before a step runs, FeatherCI looks for a matching cache entry. If found, cached directories are extracted into the container.
- Save: After a step succeeds, specified paths are archived under the cache key.
- Overwrite: Existing cache entries with the same key are overwritten.
- No partial matches: Cache lookup is exact — no fallback to prefix matching.
Notifications
FeatherCI can notify you when builds succeed, fail, or are cancelled. Notification channels are configured per-project through the web UI.
Supported Channels
| Channel | Type Key | Description |
|---|---|---|
| Email (SMTP) | email_smtp | Send via your own SMTP server |
| Email (SendGrid) | email_sendgrid | Send via SendGrid API |
| Email (Mailgun) | email_mailgun | Send via Mailgun API |
| Slack | slack | Post to a Slack channel via incoming webhook |
| Discord | discord | Post to a Discord channel via webhook |
| Pushover | pushover | Send push notifications to devices |
Adding a Notification Channel
- Navigate to your project
- Go to Notifications (in the project sidebar)
- Click Add Channel
- Select the channel type and fill in the configuration
- Choose which events to notify on (success, failure, cancellation)
- Click Create
Channel Configuration
Email (SMTP)
- SMTP Host — Mail server hostname (e.g.
smtp.example.com) - SMTP Port — Server port (typically
587for TLS) - Username — SMTP authentication username
- Password — SMTP authentication password
- From Address — Sender email address
- To Addresses — Comma-separated recipient email addresses
Email (SendGrid)
- API Key — Your SendGrid API key
- From Address — Verified sender address
- To Addresses — Comma-separated recipient email addresses
Email (Mailgun)
- API Key — Your Mailgun API key
- Domain — Your Mailgun domain
- From Address — Sender address
- To Addresses — Comma-separated recipient email addresses
Slack
Create an Incoming Webhook in your Slack workspace. Copy the webhook URL and paste it into the Webhook URL field.
Discord
In Discord, go to Channel Settings → Integrations → Webhooks. Create a new webhook and copy the URL into the Webhook URL field.
Pushover
- User Key — Your Pushover user key
- API Token — Your Pushover application API token
Event Filters
Each channel can be configured to trigger on any combination of: Success, Failure, and Cancelled.
Testing
After creating a channel, use the Test button to send a test notification and verify your configuration.
Manual Approvals
Approval steps let you gate your pipeline with human sign-off. The workflow pauses at the approval step and waits for a user to approve before downstream steps continue.
Adding an Approval Step
Set type: approval on a step. Approval steps don't need image or commands:
steps:
- name: build
image: golang:1.22
commands:
- go build -o app ./cmd/app
- name: deploy-approval
type: approval
depends_on: [build]
- name: deploy
image: alpine:latest
depends_on: [deploy-approval]
commands:
- ./deploy.sh
How It Works
- When the workflow reaches an approval step, the build status shows waiting
- The build page displays an Approve button on the approval step
- Any authenticated user with access to the project can click Approve
- Once approved, downstream steps resume execution
Multi-Stage Promotion
steps:
- name: test
image: golang:1.22
commands:
- go test ./...
- name: build
image: golang:1.22
depends_on: [test]
commands:
- go build -o app ./cmd/app
- name: staging-gate
type: approval
depends_on: [build]
- name: deploy-staging
image: alpine:latest
depends_on: [staging-gate]
if: branch == "main"
commands:
- ./deploy.sh staging
- name: prod-gate
type: approval
depends_on: [deploy-staging]
- name: deploy-prod
image: alpine:latest
depends_on: [prod-gate]
if: branch == "main"
commands:
- ./deploy.sh production
This creates a two-stage promotion workflow: build → approve → staging → approve → production.
Tips
- Approval steps appear as distinct nodes in the pipeline DAG visualization
- If a build is cancelled, pending approval steps are also cancelled
- Approval steps don't have a timeout — they wait indefinitely until approved or cancelled
Migrating from Other CI
If you're already using GitHub Actions or CircleCI, FeatherCI can automatically convert your existing configuration to a FeatherCI workflow.
Usage
cd your-project
featherci convert
The command auto-detects your CI configuration and generates .featherci/workflow.yml. The original file is renamed with a .bak suffix so you can review the result and delete it when ready.
You can also specify a directory:
featherci convert /path/to/project
What Gets Converted
| Feature | GitHub Actions | CircleCI |
|---|---|---|
| Workflow name | name | Workflow key name |
| Triggers | on.push / on.pull_request with branch filters | Defaults to all pushes (with warning) |
| Docker images | container field | docker[0].image |
| Commands | run: blocks | run: steps |
| Dependencies | needs | requires |
| Caching | actions/cache (key + path) | save_cache / restore_cache |
| Secrets | ${{ secrets.X }} references | — |
| Conditions | github.ref expressions → branch | Filters (warning) |
| Approval gates | — | type: approval |
| Environment variables | Global and job-level env | environment and Docker env |
| Timeouts | timeout-minutes | — |
| Continue on error | continue-on-error | — |
| Service containers | — | docker[1:] → services |
| Custom commands | — | Inlined with shell equivalents |
| Common orbs | — | Expanded to shell commands (ruby, node, python, go) |
Unsupported Features
The converter prints yellow warnings for features that can't be automatically converted. These need manual adjustment:
GitHub Actions:
uses:actions — replace with equivalent shell commandsruns-on:withoutcontainer— choose an appropriate Docker image- Build matrices (
strategy.matrix) — create separate steps for each variant - Artifacts, permissions, concurrency, reusable workflows
CircleCI:
- Uncommon orbs — replace with equivalent shell commands (common orbs like ruby, node, python, go are auto-expanded)
- Machine and macOS executors — FeatherCI uses Linux Docker containers only
persist_to_workspace/attach_workspace— use caching or restructure your pipelinestore_artifacts/store_test_results- Parallelism (test splitting)
Example
Given this GitHub Actions workflow:
name: CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
container: golang:1.22
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v3
with:
path: /go/pkg/mod
key: go-${{ hashFiles('go.sum') }}
- run: go test ./...
build:
needs: [test]
runs-on: ubuntu-latest
container: golang:1.22
steps:
- uses: actions/checkout@v4
- run: go build -o app .
Running featherci convert produces:
name: CI
on:
push:
branches:
- main
steps:
- name: test
image: golang:1.22
commands:
- go test ./...
cache:
key: go-{{ checksum "go.sum" }}
paths:
- /go/pkg/mod
- name: build
image: golang:1.22
depends_on:
- test
commands:
- go build -o app .
Distributed Workers
FeatherCI can scale build capacity by running additional worker nodes. Workers poll the master instance for jobs and execute builds independently.
Architecture
In distributed mode, FeatherCI runs in two roles:
- Master — Accepts webhooks, serves the web UI, stores data, and distributes build jobs
- Worker — Polls the master for pending jobs, executes build steps in Docker containers, and reports results back
In the default standalone mode, a single instance acts as both.
Master Setup
FEATHERCI_MODE=master
FEATHERCI_WORKER_SECRET=your-shared-secret
FEATHERCI_BASE_URL=https://ci.example.com
FEATHERCI_SECRET_KEY=...
FEATHERCI_ADMINS=admin-user
# ... OAuth config ...
Worker Setup
FEATHERCI_MODE=worker
FEATHERCI_MASTER_URL=https://ci.example.com
FEATHERCI_WORKER_SECRET=your-shared-secret
The FEATHERCI_WORKER_SECRET must match between master and all workers.
Worker with Docker
docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-e FEATHERCI_MODE=worker \
-e FEATHERCI_MASTER_URL=https://ci.example.com \
-e FEATHERCI_WORKER_SECRET=your-shared-secret \
ghcr.io/featherci/featherci:latest
How It Works
- Workers self-register with the master on startup
- Workers poll for ready steps every 2 seconds
- When a step is found, the worker claims it, clones the repo, injects secrets (fetched from master), and runs the step in Docker
- On completion, the worker reports results; the master handles all state transitions (skipping dependents, unblocking ready steps, recalculating build status, sending notifications, posting commit statuses)
- Workers send heartbeats every 15 seconds; the master resets steps from stale workers after 60 seconds
Scaling
Add more workers to increase parallel build capacity. Each worker runs up to 2 steps concurrently by default (configurable via FEATHERCI_MAX_CONCURRENT). Multiple workers can process steps from the same build simultaneously.
Requirements
- Workers must be able to reach the master URL over the network
- Workers must have Docker installed and accessible
- All instances must share the same
FEATHERCI_WORKER_SECRET - Workers do not need OAuth configuration, a database, or a secret key
- Workers need local disk space for workspaces and build cache