Documentation

Last updated: 2026-09-27

Workflows

A push can start a build. Everlock speaks the runner protocol itself, so a stock forgejo-runner registers against it and takes work — there is no separate CI service to install, and no database behind it.

Workflows are served by the same backend as git over HTTP and the web UI. A runner's instance URL is the host it clones from, so the three arrive together with /backends enable git-http.

What starts a run

A push. When a push applies a branch or tag update, Everlock looks for .forgejo/workflows/ in the new commit's tree. Finding it is what enrols a repository, so there is no per-repository switch to turn on: a repository has workflows exactly when its pushed commit carries them.

Rejected ref updates are not news, so only updates a push actually applied are considered. The trigger lives with the push session rather than with a transport, so a push over SSH and a push over HTTP both reach it.

Two triggers are read from on::

TriggerBehaviour
pushfiltered by branches, tags, paths and their -ignore counterparts. A tag push is selected only by a tags filter, and a branch push only by a branches one — so on: push: tags: [v*] does not run on branch pushes
workflow_dispatchstarted by hand, from /git workflow start or the Workflows tab

Registering a runner

Configuration is not where workflows are gated — the runner registry is. An instance with no runner registered answers the protocol and hands out no work.

Mint a one-time registration token:

/git runner token

Then register a runner against the git host and run it:

forgejo-runner register --instance https://git.example.com --token <token>
forgejo-runner daemon

The instance URL is the same host the runner clones from. Registered runners are listed and removed from the console, or under Settings in the web UI's instance bar:

/git runner list
/git runner remove <name-or-uuid>

Because the token is what decides which machine executes, it is a better switch than a flag would be. Note the consequence: write access to any repository is access to the runner's execution environment, so a repository whose workflows should not run is one whose write grants are narrower.

Watching runs

/git workflow list [repo]                   list runs with their status
/git workflow log <repo> last|<run> [job]   print a run's log
/git workflow start <repo> <workflow> [ref] [key=value ...]

start only offers workflows that declare on: workflow_dispatch — the declaration is the author saying the workflow makes sense run by hand.

In the browser, a Workflows tab appears on a repository once a run exists. Each workflow at the default branch is a pill; selecting one filters the list beneath it. A writer gets a start control on each pill declaring workflow_dispatch. A run's page shows each job with its status, what it waited for, and its log.

Who may see a run

A run belongs to a repository, and is read at that repository's level — a build log quotes the code that produced it, so it is treated as the code. The rule is the same from the browser and from the console:

OperationTakes
read a run and its logreader on the repository
start a run by handwriter on the repository
set a secret or variableowner on the repository
register or remove a runnersystem administrator

/git workflow list without a repository spans every repository, so it lists only the ones the caller may read. Naming a repository the caller cannot read is refused whether or not it exists, so the answer carries no news about repositories either way. In the browser the same holds by construction: authorization runs before the repository is opened, so a run the viewer may not read is indistinguishable from one that does not exist.

Registering a runner is instance administration rather than any one repository's business, which is why it sits at a different level from everything else here.

Secrets and variables

/git secret set <repo> <name> <value>    set a secret, masked from logs
/git var set <repo> <name> <value>       set a variable
/git var list <repo>                     list both kinds; secret values are not shown
/git var remove <repo> <name>            remove either

owner on the repository sets and lists them; writer consumes them, since pushing a workflow is enough to use one. A secret has no value to show, so var list prints — in its value column, and job logs replace it with ***.

Values are stored as plaintext in the workflows store, at repos/<repo>/variables.toml. Masking matters more than encryption here: logs are shown to every reader of the repository, while the store itself needs a store grant. Encrypting the file would put its key in a versioned store beside the ciphertext, which protects nothing.

Jobs, matrices and concurrency

Each job is dispatched to a runner as a single-job workflow, leaving the steps to the runner. needs gates a job on the ones it names; a job whose dependency can never run is marked skipped rather than left blocked, so a broken run settles instead of hanging.

strategy.matrix expands to one task per combination. Legs share a job id and are named for their values — build (ubuntu, stable). Everlock resolves ${{ matrix.* }} in runs-on only, and each leg's payload carries just its own combination so the runner resolves the rest. needs gates on all legs.

Two things are refused outright rather than silently ignored:

  • strategy.matrix.include and strategy.matrix.exclude
  • a runs-on that still holds an unresolved ${{ }} expression

A workflow-level concurrency: group serialises runs that resolve to the same group. The group is resolved by substituting exactly the values the task context carries; github.token is excluded by rule, because it is a live clone credential. An unknown expression stays literal, which over-serialises — the safe direction. cancel-in-progress is the only thing that cancels a run.

Checkout, and the credential that does it

actions/checkout clones over the same smart HTTP any other client uses. The task carries a credential minted for it alone: a share identity holding reader on that one repository, deleted when the task ends. It clones its own repository and is refused on any other.

When a runner goes quiet

A lease is a silence budget, not a time limit: 90 seconds, extended by any report a runner sends. If it lapses, the task is taken back and requeued — the runner is assumed to have died, not the job. A second lapse fails the task.

Claim state is a field on the persisted task record rather than an in-memory queue, so a restart resumes tasks that were mid-flight.

Storage

Runs live in the everlock-workflows store, two files per run:

repos/<repo>/runs/<id>/run.toml        the run and all its jobs
repos/<repo>/runs/<id>/log-<task>.txt  one log per job
repos/<repo>/variables.toml            secrets and variables

Writes are about two per job. A claim, a lease refresh and a log batch cost nothing extra, and an idle runner costs nothing at all.

Not yet

These are designed but not built: schedule: triggers, pull_request as a trigger, artifacts, step summaries, and run retention.

git workflows ci runners secrets