Documentation
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::
| Trigger | Behaviour |
|---|---|
push | filtered 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_dispatch | started 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:
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:
| Operation | Takes |
|---|---|
| read a run and its log | reader on the repository |
| start a run by hand | writer on the repository |
| set a secret or variable | owner on the repository |
| register or remove a runner | system 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.includeandstrategy.matrix.exclude- a
runs-onthat 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.