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
VariableDescription
branchThe branch being built
OperatorDescriptionExample
==Equalsbranch == "main"
!=Not equalsbranch != "develop"
=~Glob matchbranch =~ "release/*"
!~Glob not matchbranch !~ "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.0mysql, redis:7redis. 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

  1. Navigate to your project
  2. Go to Secrets (in the project sidebar)
  3. Enter a name and value
  4. 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

TemplateDescription
{{ 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

ChannelType KeyDescription
Email (SMTP)email_smtpSend via your own SMTP server
Email (SendGrid)email_sendgridSend via SendGrid API
Email (Mailgun)email_mailgunSend via Mailgun API
SlackslackPost to a Slack channel via incoming webhook
DiscorddiscordPost to a Discord channel via webhook
PushoverpushoverSend push notifications to devices

Adding a Notification Channel

  1. Navigate to your project
  2. Go to Notifications (in the project sidebar)
  3. Click Add Channel
  4. Select the channel type and fill in the configuration
  5. Choose which events to notify on (success, failure, cancellation)
  6. Click Create

Channel Configuration

Email (SMTP)

  • SMTP Host — Mail server hostname (e.g. smtp.example.com)
  • SMTP Port — Server port (typically 587 for 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

  1. When the workflow reaches an approval step, the build status shows waiting
  2. The build page displays an Approve button on the approval step
  3. Any authenticated user with access to the project can click Approve
  4. 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

FeatureGitHub ActionsCircleCI
Workflow namenameWorkflow key name
Triggerson.push / on.pull_request with branch filtersDefaults to all pushes (with warning)
Docker imagescontainer fielddocker[0].image
Commandsrun: blocksrun: steps
Dependenciesneedsrequires
Cachingactions/cache (key + path)save_cache / restore_cache
Secrets${{ secrets.X }} references
Conditionsgithub.ref expressions → branchFilters (warning)
Approval gatestype: approval
Environment variablesGlobal and job-level envenvironment and Docker env
Timeoutstimeout-minutes
Continue on errorcontinue-on-error
Service containersdocker[1:]services
Custom commandsInlined with shell equivalents
Common orbsExpanded 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 commands
  • runs-on: without container — 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 pipeline
  • store_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

  1. Workers self-register with the master on startup
  2. Workers poll for ready steps every 2 seconds
  3. When a step is found, the worker claims it, clones the repo, injects secrets (fetched from master), and runs the step in Docker
  4. 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)
  5. 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