# Workflow files

The format of .lorx/actions/*.yml, every key a job takes, and what each does.

A workflow is a YAML file in `.lorx/actions/` in your repository, such as `.lorx/actions/ci.yml`. The file's name is the workflow's name. You can have as many workflows as you like: each plans its own jobs, and `needs` names jobs in the same file.

Each top-level key is a job. The shortest job is a name and a command:

```yaml
lint: npm run lint
test: npm test
```

The longer form is a map:

```yaml
test:
  needs: [lint]
  paths: ["src/**", "package.json"]
  steps:
    - npm ci
    - name: unit tests
      run: npm test
  timeout: 20m
```

On a push, Lorx runs the jobs whose `paths` the push touched (every job without `paths`) on the branches they allow. Jobs start in levels: a job waits for everything in its `needs` to pass, and if one fails, the jobs that need it don't run.

## Job keys

| Key | Takes | What it does |
|---|---|---|
| `run` | a command | The job's one step. Use `run` or `steps`, not both. |
| `steps` | a list | Each item is a command, or `name:` plus `run:`. Steps run in order in the same directory, and the job stops at the first that fails. |
| `needs` | job names | Jobs in the same file that must pass first. |
| `paths` | globs | Run on a push only when a changed file matches one of them. Globs are relative to the repository root. |
| `branches` | globs | Run on pushes to matching branches only. |
| `matrix` | axes and values | Run the job once per combination: `matrix: { os: [linux, macos], node: [20, 22] }` makes four instances. Each gets its values as environment variables named `MATRIX_<AXIS>`, such as `MATRIX_OS`. |
| `env` | names and values | Environment variables for every step. |
| `image` | an image name | Run the steps in this container image, with docker or podman, instead of on the machine itself. |
| `cache` | directories | Directories kept between runs on the same machine: container volumes for jobs with an `image`, shared by every job of the repository with the same image. |
| `cache_inputs` | commands | Commands whose output is part of each step's cache key, for what the files don't pin, such as `clang --version`. See [The step cache](#the-step-cache). |
| `runner` | a label | Only a runner with this label takes the job, such as `runner: gpu`. See [Runners](/docs/ci/runners). |
| `timeout` | a duration | `30` or `30m` is minutes, `2h` hours, `90s` seconds, up to a day. The job is stopped when it runs over. |
| `if` | a condition | Run only when it holds. See below. |
| `on` | a schedule | `on: schedule 0 3 * * *` (five cron fields) makes a scheduled job, which never runs on pushes. See the note below. |
| `secrets` | names | The secrets the job is given, as environment variables. See [Secrets](#secrets). |
| `artifacts` | paths | Not supported yet: a warning says the files aren't kept. |

A key Lorx doesn't know is a warning, not an error: the rest of the workflow still runs. A GitHub Actions file (with top-level `on:` and `jobs:`) is an error that says so: Lorx's format is the one on this page.

A command with `: ` in it needs the whole value quoted, as YAML requires: `lint: 'echo "lint: 0 warnings"'`, or a block with `lint: |` and the command on the next line. `bit ci check` shows both for the line in question.

## Conditions

`if` compares what set the run off. It knows two names:
- `branch`: the branch pushed to (empty for a schedule);
- `event`: `push`, `schedule` or `manual`.

Values are quoted, with `'single'` or `"double"` quotes, and comparisons combine with `&&`, `||`, `!` and parentheses:

```yaml
deploy:
  needs: [test]
  if: branch == 'main' && event != 'schedule'
  run: ./deploy.sh
```

## Every step's environment

| Variable | Value |
|---|---|
| `CI`, `LORX` | `true` |
| `LORX_JOB` | The job instance, such as `test (os=linux)` |
| `LORX_WORKFLOW` | The workflow's name |
| `MATRIX_<AXIS>` | Each matrix value |

Your own `env` comes last, and wins.

## The step cache

A step that passed before with the same inputs is skipped, on your machine and on runners. Its inputs are:
- its command, and the steps before it in its job;
- its environment, matrix values included;
- every file in the checkout, by content;
- for a job with an `image`, the image by digest, so a moved `latest` is a new image;
- for a job in the machine's own shell, the versions of the well-known tools its commands call, asked of each tool in the checkout: `cargo` and `rustc` (so a `rust-toolchain.toml` counts), `node`, `bun`, `go`, `python3`, `git`, `clang` and more. A new compiler on the runner runs the step again;
- the output of the job's `cache_inputs`.

A skipped step says what it matched on: `skipped, it passed before with the same inputs (cargo 1.98.0, rustc 1.98.0)`. `bit run --no-cache` runs every step anyway.

## Secrets

`secrets: [DEPLOY_TOKEN]` gives the job's steps `$DEPLOY_TOKEN`, and only the secrets it names.

- **On a runner**, they come from the repository's secrets, then its owner's account's: a repository's secret overrides an account's of the same name. They are encrypted at rest and sent only to the runner that holds the job.
- **An account's secret reaches only the repositories you list for it.** A new one reaches none until you give it some. That way, someone who can write to one of your repositories can't get at a secret meant for another.
- **Only code on a branch or tag is given secrets.** A run you start by hand at any other commit (an imported pull request's, say) runs without them, and its jobs that need them fail saying why.
- **With `bit run`**, they come from your environment, then from `.lorx/secrets.env` in the checkout. See [Run jobs on your machine](/docs/ci/run-locally#secrets).
- Every value is replaced with `***` in the job's log, along with its base64 and URL-encoded forms. Values under 4 characters aren't masked, so keep secrets longer than that.
- A job with secrets is never cached: its steps run every time, and a container job mounts no shared cache volume.
- A job that names a secret nobody set fails before its first step, naming it.

## Scheduled jobs

A job with `on: schedule …` is left out of push runs. Lorx does not fire schedules yet, so today a scheduled job runs only when you name it: `bit run nightly`.

## Check a workflow

```sh
bit ci check
```

This prints every problem with its line and column, the way compilers do, and shows the plan a push would make. It exits with status 1 when there are errors. See [Run CI on your machine](/docs/ci/run-locally).
