Workflows
gitfor runs GitHub Actions–style workflows from .gitfor/workflows/*.yml.
# .gitfor/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: cloudflare
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v4
- run: bun install && bun test
- uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.node }}
path: coverage
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: [self-hosted, linux]
steps:
- run: ./deploy.sh
env:
TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Triggers
push (branches, tags and paths filters), pull_request (types, branches, paths), release and
workflow_dispatch (with inputs).
Steps
runscripts in bash, sh, python, node or bun- Built-in
checkout,cacheandupload-artifact setup-nodeandsetup-bun(built in unless a version is requested)- JavaScript and Docker actions from public github.com repositories, or from the repository itself (
uses: ./path) uses: docker://imagesteps, withwith.argsandwith.entrypoint
Job status is published as commit statuses named <workflow> / <job>, which branch protection can require.
CI jobs fetch only the commit they run on (depth 1). Set GITFOR_FETCH_DEPTH in a workflow's env to fetch
more history, or to 0 for a full fetch.
Third-party actions
- A tag or branch is resolved to its commit when the run is scheduled. That SHA is stored with the run, shown in the log ("Download action repository … (SHA: …)") and used for every retry.
- A full commit SHA is accepted only if a branch or tag of the named repository reaches it, so a commit that only exists in a fork can't pose as the repository. Short SHAs are refused.
- Code is fetched over git and every object is checked against its SHA. The archive is stored once per commit and the runner verifies its sha256 before extracting. Actions run only inside the job's runner, never in the Worker.
- Repository settings → Allowed actions chooses between any public action, only SHA-pinned actions, or only built-in ones.
${{ github.token }}or${{ secrets.GITHUB_TOKEN }}written in an action step'swith:reads theGITHUB_COM_TOKENsecret (a github.com token, for actions that call the GitHub API). An action's own${{ github.token }}defaults get an empty value, so no action receives that secret unless the workflow passes it. Fork pull request runs get no secrets.- Composite actions are not supported yet. A pin covers the action's code, not what it downloads while it runs.
Cloud runner image
Cloud jobs run on Ubuntu 24.04 as root, like ubuntu-latest on GitHub: sudo, apt-get, PPAs
(add-apt-repository), build-essential, Python 3, Node.js 24 with npm, the latest Go (at image build time), Bun, git, curl, wget, jq and zip are
installed.
Docker actions
Jobs run on a lightweight image without Docker. When a job uses a Docker action (runs.using: docker) or a
docker:// step, it automatically moves to the Docker-capable twin of its runner size: cloudflare →
cloudflare-docker, cloudflare-large → cloudflare-large-docker, cloudflare-xlarge →
cloudflare-xlarge-docker. You can also ask for a twin directly in runs-on to use docker in run steps.
- Billing: a twin is the same instance type, so it uses the same meter and weight as its base size. Nothing is double-counted; image pulls and builds simply take job time.
- Pinning: an image tag is resolved to a linux/amd64 manifest digest when the run is scheduled, and the job
runs that digest, so a moving tag can't change a run. With Allowed actions → only SHA-pinned,
docker://steps must be pinned by digest (@sha256:…). - Image cache: registry images are pulled through the forge into R2 (
ci-images/, content-addressed and sha256-verified on write) and loaded on the runner from there, so popular images aren't re-pulled from the registry by every job. Cached blobs expire 30 days after they were cached; the next run re-pulls them. Images may be at most 2 GiB compressed. Only public images are supported. - Dockerfile actions (
runs.image: Dockerfile) are built on the runner; their base images come straight from their registry. - How a step runs: dockerd starts on the first Docker step (as root, with iptables and IP forwarding off,
as Cloudflare Containers require). The action container gets
--network host, the workspace at/github/workspace, the file-command files (GITHUB_OUTPUT,GITHUB_ENV, …) and its inputs asINPUT_*. - Isolation: every job gets a fresh Container, so the Docker daemon and everything it ran disappear with the job. Fork pull request runs still get no secrets. On self-hosted runners, Docker actions need a job image with Docker; never mount the host's Docker socket into jobs.
Self-hosted runners
Create a registration token under Repository settings → Runners (or /orgs/<owner>/runners for owner-wide
runners), choosing the labels and the isolation. Then, on a host with Docker or Podman:
gfd runner register --url https://gitfor.dev --token gitfor_reg_...
gfd runner run
Each job runs in a fresh, non-root container with a read-only root filesystem and an egress firewall that blocks cloud metadata and private networks.
Fork pull request jobs never reach self-hosted runners unless a repository admin opts in, and then only runners
whose container isolation the admin declared on the token. See the gfd README for what is and isn't isolated.
The old runner/gitfor-runner.ts is retired.