Docs

CI (Buildfyio Checks)

Every push and pull request checks out that exact commit into a fresh, isolated container, runs the steps in .buildfyio/ci.yml, and reports one GitHub commit status per job. Logs and artifacts stay on the run page.

Checks are separate from deployments: they never run in your production container, never see your project's environment variables, and never deploy anything. A project without .buildfyio/ci.yml runs no checks. Runs show up on the project's CI tab.

version: 1
jobs:
  check:
    cache: [pnpm]
    steps:
      - run: pnpm install --frozen-lockfile   # pnpm version from "packageManager"
      - run: pnpm test

Config format

The file lives at .buildfyio/ci.yml in the repository root and is read at the commit being checked, so a branch can change its own checks. Unknown keys are errors (a typo fails the run with a clear message instead of being ignored); validate locally with buildfy ci validate. A complete example: a security/quality job and a Playwright job.

# .buildfyio/ci.yml
version: 1

on:
  push:
    branches: ["**"]              # globs; "!" excludes. Default: every branch, production included
  pull_request: true              # opened, synchronize, reopened (default true)

jobs:
  # ── becomes the GitHub status "buildfyio/check" ──────────────────────────
  check:
    # image omitted → node:<major from .nvmrc>-bookworm (22 when there is no .nvmrc)
    timeout: 30m                  # default 30m, max 60m
    cache: [pnpm]                 # PNPM_STORE_DIR → a per-project cache volume
    env:
      APP_ENV: test
      NODE_ENV: test
      NEXT_TELEMETRY_DISABLED: "1"
      ASTRO_TELEMETRY_DISABLED: "1"
    # secrets: [NPM_TOKEN]        # only if a private registry needs it (never for fork PRs)
    steps:
      - name: Secret scan (gitleaks 8.30.1, checksum-verified)
        run: |
          V=8.30.1
          BASE=https://github.com/gitleaks/gitleaks/releases/download/v${V}
          cd /tmp
          curl -sSfLO "${BASE}/gitleaks_${V}_linux_x64.tar.gz"
          curl -sSfLO "${BASE}/gitleaks_${V}_checksums.txt"
          grep " gitleaks_${V}_linux_x64.tar.gz$" "gitleaks_${V}_checksums.txt" | sha256sum -c -
          tar -xzf "gitleaks_${V}_linux_x64.tar.gz" gitleaks
          cd /workspace
          /tmp/gitleaks git --redact --exit-code 1 .

      - name: Node matches .nvmrc
        run: |
          want="v$(tr -d 'v \n' < .nvmrc | cut -d. -f1)"
          have="$(node -v | cut -d. -f1)"
          echo "node $(node -v), .nvmrc wants ${want}"
          test "${have}" = "${want}"

      # pnpm: nothing to install. When the image has corepack but no pnpm, the
      # runner enables corepack (version from "packageManager") and puts it on PATH.

      - name: Install
        run: pnpm install --frozen-lockfile

      - name: Audit
        run: pnpm audit --prod --audit-level high

      - name: Lint
        run: pnpm lint

      - name: Typecheck
        run: pnpm typecheck

      - name: Unit tests
        run: pnpm test

      - name: Build
        run: pnpm build

      - name: Env check
        run: pnpm env:check

  # ── becomes "buildfyio/e2e"; runs in parallel with "check" ───────────────
  e2e:
    image: mcr.microsoft.com/playwright:v1.49.0-noble
    timeout: 40m
    cache: [pnpm, playwright]     # PLAYWRIGHT_BROWSERS_PATH is cached too
    env:
      APP_ENV: test
      NODE_ENV: test
    steps:
      - name: Install
        run: pnpm install --frozen-lockfile
      - name: Build what the web app needs
        run: pnpm turbo run build --filter=./apps/web...
      - name: Browsers
        run: pnpm --filter ./apps/web exec playwright install chromium
      - name: E2E
        run: pnpm --filter ./apps/web test:e2e
    artifacts:
      - path: apps/web/test-results  # relative to the repository root
        when: failure                # always | failure | success (default always)
        retention: 7d                # 1d … 30d (default 7d)
KeyDefaultNotes
version—Must be 1.
on.push.branchesevery branch, the production branch includedGlobs; ** matches any branch, a leading ! excludes.
on.pull_requesttrueOpened, synchronize (new commits) and reopened.
jobs.<name>—Name: lowercase letters, digits and -, up to 31 characters. Becomes the status context buildfyio/<name>.
imagenode:<.nvmrc major>-bookworm (22 without .nvmrc)Any public image.
timeout30mMax 60m. The job is killed and reported timed out.
env—Plain values for this job. Not secret, visible in the config.
secrets—Names of project CI secrets to expose as environment variables (see Secrets).
cache—Named caches or {name, path} (see Caches).
steps[].run—Shell, run with bash -eo pipefail; multi-line with |. The first failing step fails the job.
steps[].namestep NShown on the run page and in the log.
artifacts[]—path (relative to the repo root), when (always | failure | success, default always), retention (1d–30d, default 7d).

Every job gets CI=true, BUILDFYIO_CI=1, BUILDFYIO_RUN_ID, BUILDFYIO_JOB, BUILDFYIO_SHA and BUILDFYIO_REF, plus its own env and listed secrets. Nothing else. Jobs run as a non-root user (uid 1000), so install tools into the workspace or $HOME (/workspace/.bfy/home), not into /usr/local. A PATH in env is merged with the image's PATH (use $PATH to place it); each step runs in its own shell, so cd and export do not carry over between steps.

Triggers and concurrency

  • Push to a branch that matches on.push.branches (deleted branches are ignored). The production branch is included by default, and a push to it starts CI before its deploy, so the deploy gate sees the run (see Deploy gate).
  • Pull request opened, updated or reopened, checked at the PR head commit.
  • Manual: Run on branch… on the CI tab, buildfy ci run --ref, or the run_ci MCP tool. Rerun repeats a finished run at the same commit.

A newer run on the same branch cancels older queued or running runs of that branch: the old run is marked cancelled and links to the one that replaced it. Jobs of one run start in parallel, up to your plan's concurrency; the rest wait in the queue.

Isolation

Each job runs in its own Kubernetes Job and is deleted after it finishes.

  • A fresh container per job: nothing carries over from previous runs except the caches you name.
  • No production environment variables. Project env vars (production, preview or any other environment) are never passed to a run. Only the job's env and the CI secrets it lists.
  • No access to the platform. The cluster network and private address ranges are blocked: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 (link-local, including cloud metadata), 100.64.0.0/10, and the platform's own public addresses. A run cannot reach your deployments' pods, databases, or internal services. DNS works.
  • Outbound internet is allowed (package registries, GitHub, your public staging URL). A per-project egress allow-list is planned.
  • Non-root (uid 1000), no privilege escalation, all Linux capabilities dropped, default seccomp profile. No inbound connections.

Secrets

CI secrets are set under Project → Settings → CI (owners and admins) or with buildfy ci secrets set. They follow stricter rules than environment variables:

  • Separate from env vars. A production DATABASE_URL is not a CI secret; add a test value under its own key if a job needs one.
  • Never automatic. A job sees a secret only when it names it under secrets:.
  • Fork pull requests get none, whatever the config says; the run page marks those runs as fork. Code from a fork cannot read your secrets.
  • Masked in logs. Every secret value, and the clone token, is replaced by *** before a log line is stored.
  • Write-only: values are never shown again, in the dashboard, API, CLI or MCP.

Repository access

Before the job starts, a separate clone container checks the commit out with full history into /workspace. It authenticates with the GitHub App installation token of the account that owns the repository (valid for about an hour, scoped to every repository that installation can see, with the App's permissions, not read-only and not limited to this repository). Without an App installation on that account it uses the workspace's GitHub OAuth token (scope repo). The token is removed from .git/config (the remote is reset to the plain URL) before your steps run, is never in the step container's environment or arguments, and is masked if it ever appears in output. CI does not push or write back to the repository; the only thing it writes to GitHub is commit statuses.

Reporting to GitHub

Each job posts a commit status with the context buildfyio/<job>: pending when the run is created, then success or failure/error. “Details” links to the run page on Buildfyio.

Required checks in branch protection

In GitHub, Settings → Branches → Branch protection rules (or a ruleset) → Require status checks to pass, search for buildfyio/check, buildfyio/e2e, … and add them. A context appears in that search after it has run once. Pull requests then cannot be merged until those jobs pass.

Deploy gate

Turn on Require passing checks before production in Project → Settings → Workflow. A production deploy then waits for every check run and commit status on its commit, buildfyio/* statuses included, and is refused when one fails. This applies to pushes, manual deploys, promoting a preview and “Redeploy to production”. While a Buildfyio Checks job on that commit is still queued or running the deploy waits (up to 90 minutes); a failed, timed-out, errored or cancelled job blocks it. If the commit's .buildfyio/ci.yml runs on that push but its run does not exist yet, the deploy waits up to 2 minutes for it, then is refused.

What still reaches production without a green Buildfyio Checks run: a rollback to a release production already ran (exempt by design, so a bad release can always be undone), a commit whose .buildfyio/ci.yml does not run on pushes to that branch (see below) and that has no other checks, and every deploy while the setting is off. Uploaded source trees and prebuilt images have no commit to check and are not gated.

Pushes to the production branch are checked by default, so a deploy from a push to main waits for that run when the gate is on. To skip CI there, exclude it: branches: ["**", "!main"].

Logs, artifacts and retention

  • Logs stream live on the run page (each step is a section) and stay downloadable after the job finishes (buildfy ci logs, Download on the run page).
  • Artifacts: each artifacts[].path that exists when the job ends is packed as a .tgz, subject to when. Retention is 1d to 30d, default 7d; expired artifacts are deleted and listed as expired.

Caches

Caches live on a per-project volume and are keyed by your lockfile (pnpm-lock.yaml, package-lock.json or yarn.lock): a lockfile change starts a fresh cache.

Caches are separated by trust, so a run can never plant files that a more trusted run later executes. Runs on your production branch read and write the project's main cache. Runs on any other branch get their own cache for that branch, which starts empty and cannot see the main one. Fork pull requests get a throwaway cache that is deleted with the job and never read by another run.

NameSetsNote
pnpmPNPM_STORE_DIR, npm_config_store_dirpnpm store
npmnpm_config_cachenpm download cache
yarnYARN_CACHE_FOLDERyarn cache
playwrightPLAYWRIGHT_BROWSERS_PATHplaywright install fills it once
{name, path}—A path under /workspace linked to the cache, e.g. {name: next, path: apps/web/.next/cache}

Limits

LimitValue
Job timeoutdefault 30 min, max 60 min
Jobs per config10
Steps per job20
Resources per job2 CPU, 4 GiB memory, 20 GiB disk (fork pull requests: 1 CPU, 2 GiB memory, 10 GiB disk)
Artifact retention1–30 days (default 7)
Concurrent jobs per workspaceHobby 1 · Starter 2 · Pro 4 · Team 8

Pricing

CI minutes are build minutes. Each job counts the time it ran, rounded up to the minute, including jobs that fail, time out or are cancelled mid-run, against the same monthly allowance as your builds; the usage card shows the total. Above the allowance, paid plans pay $0.008 per minute, and the minutes count toward your spend cap. Hobby is never billed per minute: it stops at the allowance (100 build + CI minutes a month). At 80% the dashboard shows a warning; once the allowance is used up, new builds and CI jobs are refused until the monthly reset or an upgrade. Running sites stay up, and rollbacks to an existing image still work.

PlanBuild + CI minutes included / monthThen
Hobby100Hard stop: new builds and CI jobs pause until the monthly reset
Starter400$0.008 per build minute
Pro1,000$0.008 per build minute
Team10,000$0.008 per build minute
EnterpriseUnlimitedContract

Converting from GitHub Actions

Most workflows translate one-to-one: a job is a job, a run: step is a step. Actions you uses: become a line of shell or a config key:

GitHub Actions.buildfyio/ci.yml
on: push / pull_requeston.push.branches, on.pull_request
runs-on: ubuntu-latestimage: (any public image; default Node from .nvmrc)
timeout-minutes: 20timeout: 20m
env:env: (job level)
${{ secrets.X }}secrets: [X], then $X
actions/checkoutRemove it: the commit is already in /workspace, full history
actions/setup-nodeRemove it, or set image: node:20-bookworm
pnpm/action-setupRemove it: pnpm from packageManager is enabled automatically
actions/cachecache: [pnpm] or {name, path}
actions/upload-artifactartifacts: with when and retention
if: failure() (upload)when: failure
${{ github.sha }} / github.ref_name$BUILDFYIO_SHA / $BUILDFYIO_REF

Before and after

# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm test
        env: { API_KEY: ${{ secrets.API_KEY }} }
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: results, path: test-results, retention-days: 7 }
# .buildfyio/ci.yml
version: 1
jobs:
  test:
    # actions/setup-node → the image (default follows .nvmrc)
    timeout: 20m
    cache: [pnpm]                       # setup-node "cache: pnpm" / actions/cache
    secrets: [API_KEY]                  # ${{ secrets.API_KEY }} → $API_KEY
    steps:
      # actions/checkout → nothing: the commit is already in /workspace (full history)
      # pnpm/action-setup → nothing: the runner enables corepack's pnpm automatically
      - run: pnpm install --frozen-lockfile
      - run: pnpm test
    artifacts:                          # actions/upload-artifact
      - path: test-results
        when: failure                   # if: failure()
        retention: 7d                   # retention-days: 7

Not supported

  • Matrices (strategy.matrix): write one job per combination, or loop in shell.
  • Reusable workflows (workflow_call) and composite actions.
  • Marketplace actions (uses:): replace each with its shell equivalent, as above.
  • Service containers (services:): start the dependency inside the job or use an embedded/in-memory variant for tests.
  • needs: between jobs: jobs run independently and in parallel.

CLI

buildfy ci runs [--ref <branch>] [--status <status>] [--limit <n>]   # list runs
buildfy ci run --ref <branch>                  # start a run at the branch head (manual)
buildfy ci logs <runId|jobId> [--job <name>] [--follow]
buildfy ci rerun <runId>
buildfy ci cancel <runId>
buildfy ci artifacts <runId>                   # list artifacts
buildfy ci artifact <artifactId> --out <file>  # download one
buildfy ci validate [path]                     # check .buildfyio/ci.yml locally (default path)
buildfy ci secrets ls
buildfy ci secrets set <KEY>                   # value read from stdin / prompt, never an argument
buildfy ci secrets rm <KEY>

API tokens need the ci:read scope to list runs, read logs and download artifacts (a read token also works), and ci:run to start, rerun or cancel. Writing CI secrets needs admin.

MCP

ToolDoesScope
list_ci_runsRuns of a project, filter by branch and statusci:read
get_ci_runStatus, jobs, steps, failure reason and the last error linesci:read
get_ci_artifactA download URL for an artifactci:read
validate_ci_configValidate a ci.yml (text or the file at a ref)ci:read
run_ciStart a run on a branchci:run
rerun_ciRerun a finished runci:run
cancel_ci_runCancel a queued or running runci:run