YAML workflows & compatibility¶
TOVIO reads your existing GitHub Actions workflows as they are — it imports them verbatim and runs the supported subset on a runner you connect (see the compatibility ladder below for what that subset is). Point it at a repo that already has .github/workflows/*.yml files and it imports each one verbatim into a structural model, maps its triggers onto TOVIO change events, and classifies every job and step for compatibility — all before the workflow runs for the first time. There is nothing new to author to get a first pipeline green; if it runs on GitHub Actions today, it is your starting point here. The import is one-way: TOVIO reads your YAML and executes it, it does not round-trip changes back to GitHub.
If you'd rather define pipelines in typed TypeScript instead of YAML, that front-end compiles down to the same structural model and flows through the identical path — see the SDK & tovio ci compile section. Everything in this section applies equally to both front-ends.
Which files are picked up¶
A file is registered as a workflow only when its repo-relative path is .github/workflows/<file>.yml or .github/workflows/<file>.yaml, sitting directly in that directory. Nested files (.github/workflows/nested/ci.yml), local action definitions (.github/actions/thing/action.yml), and files elsewhere in the tree are not workflows and are skipped.
Each registered file is content-addressed by the blake3: hash of its raw bytes. A one-byte edit changes that address and re-registers the workflow deterministically, so a stale registration is never executed. The workflow's label comes from its name: field, or the file stem if there is none (.github/workflows/build.yml → build).
Parsing is a structural read only — it extracts the shape needed for trigger-mapping and compatibility classification. It does not evaluate ${{ }} expressions or step semantics at import time. A file that isn't valid UTF-8, isn't YAML, isn't a mapping, or has neither on: triggers nor jobs: fails with TVO-CI-001, and so do three shape refusals that run before the loader builds a tree: a file over the 1 MiB import ceiling, a document nested or listed past its bounds, and any YAML anchor/alias (which GitHub Actions does not support either). That failure is non-fatal: the importer reports it and skips that one file rather than aborting the whole import.
Understood fields¶
TOVIO reads this subset of the GitHub Actions schema:
| Field | Where | Notes |
|---|---|---|
name |
workflow | The workflow label; falls back to the file stem. |
on: |
workflow | Triggers and their filters — see the mapping table below. |
jobs.<id> |
workflow | Each entry is one job, in document order. |
runs-on |
job | A string, a list, or a {group, labels} map are all flattened to labels. Only Linux labels are supported (see the ladder). |
needs |
job | Job dependencies — the DAG edges. |
if |
job | Captured as the job condition. |
strategy.matrix |
job | Simple scalar axes (e.g. node: [18, 20]) fan the job out into multiple instances. include:/exclude:-only matrices are flagged as present but have no fan-out axis. |
environment |
job | Names a deployment environment — the gate target. See the deploy gates section. |
steps |
job | Each step is a run: shell step or a uses: action step. |
run: / uses: / name: / shell: |
step | The step shape the compatibility ladder classifies. |
with: / env: |
step | Flattened to string scalars and carried into the model — a non-scalar value (a list, a nested map) is dropped. A step's env: overlays the job environment at execution. They are modeled so a ${{ secrets.* }} reference riding an action input or a step environment is visible to the runner's clearance gate rather than silently lost at parse — and what the gate then does with it is fail the step closed: this runner can neither inject a secret into an action input nor evaluate a step-env expression, so it declines rather than run with the secret dropped. |
Anything outside this set is not modeled by the front half — it is neither parsed into the structural model nor a parse error; it is simply not part of what the front half reasons about. That includes permissions:, concurrency:, and env: at the workflow or job level (only a step's env: is read).
Triggers: on: → TOVIO change events¶
TOVIO has no pull requests or pushes — it has changes. Your on: block is mapped onto TOVIO change events. The TriggerEvent a workflow maps onto is one of exactly three, whose serialized dotted names match the events the Forge emits:
GitHub on: |
TOVIO result | TriggerEvent (dotted name) |
|---|---|---|
pull_request |
Runs when a proposal is opened or amended | ChangeProposed (change.proposed) + ChangeAmended (change.amended) |
push |
Never fires. Mapped, but nothing schedules a run for a land | ChangeLanded (change.landed) |
workflow_dispatch |
Never fires. Recorded, but there is no dispatch surface behind it | (not a change event — sets manual_dispatch) |
schedule (cron:) |
Never fires. Cron expressions are collected, but there is no scheduler | (not a change event — collects cron expressions) |
pull_request_target, workflow_run, release, repository_dispatch, issue/label/… |
Unsupported in v1 | (reported, never silently dropped) |
Unsupported triggers ride along in the compat report with a reason. For example, pull_request_target reports runs untrusted code with a privileged token — no safe TOVIO equivalent; workflow_run, repository_dispatch, and release report has no TOVIO change-event equivalent in v1.
One YAML gotcha is handled for you: a bare unquoted on: key can be scanned as a boolean under YAML 1.1 (the "Norway problem"). TOVIO looks the key up both ways, so on: push is detected regardless of parser schema.
Branch, path, and tag filters¶
Each trigger's filters decide whether a workflow actually runs for a given change. A filtered-out workflow does not run and does not post a pending check — it is simply absent, not stuck. Filter evaluation applies the branch and path filters:
branches/branches-ignore— matched against the target lane. Ignore wins: a lane matchingbranches-ignoreis excluded even ifbrancheswould admit it. An emptybranchesmeans "any lane."paths/paths-ignore— matched against the change's changed-path set, with GitHub semantics.pathsadmits only if at least one changed file matches a pattern.paths-ignoreadmits only if at least one changed file is not matched by any ignore pattern (so a change touching only ignored paths is excluded; a change touching a mix runs).
tags and tags-ignore are parsed into the trigger spec but are captured fields, not part of the change-event filter evaluation in the front half.
Glob patterns use a GitHub-subset matcher:
**matches any run of characters including/(sosrc/**matchessrc/a/b/c.rs, and**/*.rsmatchesa/b/c.rs).*matches any run not crossing/(sofeature/*matchesfeature/xbut notfeature/x/y, and*.jsmatchesindex.jsbut notsrc/index.js).?matches exactly one non-/character.
The [], {}, +, and inline leading-! forms are not implemented and simply won't match — the conservative direction for a required check.
on:
pull_request:
branches: [main, 'release/**']
paths:
- 'src/**'
- '*.toml'
push:
branches: [main]
paths-ignore:
- 'docs/**'
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
Here the pull_request job runs on proposals into main or any release/* lane, but only when a src/ file or a top-level .toml changed. The push entry is parsed and its filters recorded, but it never fires — nothing schedules a run for a land — so that job does not execute. The schedule and workflow_dispatch entries are parsed and reported, but neither fires: there is no cron scheduler and no dispatch surface, so a workflow triggered only by those never runs.
The compatibility ladder¶
Every step is classified into one of four CompatClass values. Classification fails toward unsupported when it cannot prove a step is supported — it never claims a step is runnable that it can't back.
One caveat on shimmed: it means TOVIO recognizes the action, not that the shim does the work the upstream action would. actions/checkout and the four setup-* actions currently classify as shimmed while doing nothing (see the shim set), so a workflow made only of those plus run: steps reports fully_supported: true and will still not behave as it does on GitHub.
| Class | What it is | How it runs |
|---|---|---|
native |
A run: step |
Executed directly as a shell script. This is the Tier-1 path. If a step has both run: and uses:, the run: is what executes. |
shimmed |
A first-party action TOVIO reimplements natively | Backed by a TOVIO primitive instead of fetching the upstream action. Carries the resolved action name and its kind. See the shim set below. |
container |
A docker://… action |
Needs a Docker-in-Docker runner tier (Tier-4). |
unsupported |
An arbitrary marketplace action, a local action, or a step with neither run: nor uses: |
Cannot run; carries a reason. |
The unsupported reasons are specific:
- A generic
owner/repo@refaction →non-shimmed action \owner/repo@ref` requires a Tier-3 (JavaScript) or Tier-4 (container) runner`. Without fetching it, TOVIO can't prove whether it's a JS or container action, so it's classified conservatively. - A local action (
./…or/…) →local action \./path` requires a Tier-3 (composite/JS action) runner`. - A step with neither key →
step has neither \run:` nor `uses:``.
Beyond per-step classes, a job can be marked not runnable as a whole (runner_supported: false):
- A
runs-onlabel containingwindowsormacos→ unsupported; only Linux runners are supported. - A job-level
uses:(a reusable workflow call) → unsupported in v1.
And some conditions are non-fatal advisories attached to a job's notes:
container:orservices:→ requires a full-DinD runner (Tier-4).strategy.matrix→ fans out into multiple job instances.- A
run:step callingapi.github.comdirectly → flagged, becauseGITHUB_TOKENauthenticates TOVIO, not GitHub.
The shim set¶
These are the exact first-party actions TOVIO implements natively. Matching is version-independent and by prefix — actions/checkout@v4, actions/checkout@<sha>, and any other ref all resolve to the same shim:
Action (any @version) |
kind |
Backed by |
|---|---|---|
actions/checkout |
checkout |
The step itself does nothing — it succeeds on the assertion that the workspace already holds the change's tree. The runner crate ships the primitive that would make that assertion true (prepare_workdir: materialize the job's pinned commit from the content-addressed store, cone-scoped, re-hashed on read, so a re-run is byte-identical or fails), but it is designed to run before the job's steps and no shipped runner calls it — the distributable BYO binary runs the job in whatever directory it was launched in. So today the pinned-commit determinism that primitive carries does not apply on the CI path: materializing the reviewed commit is the operator's job, done before the runner starts. |
actions/cache |
cache |
Tars the keyed paths into the Forge blob plane under the run's trust scope — written to its own proposal's scope, read from that scope then the target lane's, never a repository-wide namespace. Keyed by key/restore-keys; R2 on TOVIO Cloud, <root>/blobs on a native Forge, a local directory for an offline runner. Advisory: a miss never fails, and the entry is saved only after the job succeeds. |
actions/upload-artifact |
upload-artifact |
Packs the path: files into one deterministic tar and publishes it to the run's namespace with a real size, a BLAKE3 digest and a retention-days (default 90). A published name is never replaced. |
actions/download-artifact |
download-artifact |
Fetches one artifact from the run's namespace and unpacks it under path. A missing artifact fails the step (TVO-CI-013) — never an empty directory. |
actions/setup-node |
setup-toolchain |
Installs nothing today. The step succeeds and your run: steps use whatever Node is already on the runner's PATH; node-version: and its siblings are ignored, and $GITHUB_PATH is not written. Bake the toolchain into your runner image. |
actions/setup-python |
setup-toolchain |
Installs nothing today — resolves Python from the runner's PATH; the requested version is ignored. |
actions/setup-java |
setup-toolchain |
Installs nothing today — resolves Java from the runner's PATH; the requested version is ignored. |
actions/setup-go |
setup-toolchain |
Installs nothing today — resolves Go from the runner's PATH; the requested version is ignored. |
A workflow built from run: steps plus these shims classifies as fully compatible, and executes through a provisioned runner with no marketplace fetch — there is no hosted executor, so that means a customer-operated (BYO) runner you connect yourself. Note the rows above whose step body does nothing: a workflow relying on the checkout step to produce its tree, or on setup-* to install a toolchain, will not behave as it does on GitHub. This is the common shape:
name: CI
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # shimmed
- uses: actions/setup-node@v4 # shimmed
- run: npm ci # native
- run: npm test # native
Bringing your own tool¶
Because a run: step is native and executes as a plain shell script, any build tool you already use runs with no special support from TOVIO — Dagger, make, just, a shell script, whatever. Invoke it from a run: step:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make lint test
- run: just build
- run: dagger call test --source=.
TOVIO schedules the job and runs the step; the tool inside is opaque to it. This holds from either front-end — TOVIO does not adopt a third-party engine as its orchestrator, so bringing one in as an in-job tool never changes how the pipeline itself is scheduled or gated.
Checking your own workflow¶
To see exactly how your workflows classify — every job and step's CompatClass, the mapped and unsupported triggers, and each job's advisory notes — run:
It produces the compat report before a workflow first runs. Add --json for the machine-stable shape (each step carries a flattened class discriminator plus its fields, e.g. {"class":"shimmed","action":"actions/checkout","kind":"checkout"}) so you can gate on it in tooling. A workflow's fully_supported is true only when every job is runner_supported, every step is anything but unsupported, and the workflow declares no unsupported trigger — a pull_request_target alone is enough to take it out of the green.
Last reviewed September 9, 2026
Suggest an improvement to this page Not for security reports — see disclosure