Skip to main content

CI Configuration Schema

The .aeroflare-ci.yaml file configures aeroflare-ci and the GitHub Action in config mode. It is validated by a JSON Schema published alongside the source:

https://raw.githubusercontent.com/ItzEmoji/aeroflare/v1/schema/aeroflare-ci.schema.json

Reference it from the first line of your config to get completion and inline validation in any editor running a YAML language server:

# yaml-language-server: $schema=https://raw.githubusercontent.com/ItzEmoji/aeroflare/v1/schema/aeroflare-ci.schema.json

The schema sets additionalProperties: false, so an unknown or misspelled key is reported rather than silently ignored.

Keys​

KeyTypeRequiredDefaultDescription
buildsarray of stringsyes—Nix flake installables to build. At least one. The entry all discovers them; changed narrows them to what a commit changed.
cachesarray of stringsyes—Push targets, each <registry>;<repository>. At least one.
basestringnoinferredRef that changed diffs against. Only valid alongside changed.
on-missing-baseall | error | nonenoallWhat changed does with no reachable base. Only valid alongside changed.
compressionzstd | xz | gzip | nonenozstdNAR compression algorithm.
signing-keystringnounsignedPath to a signing key, or the name of an environment variable holding the key material.
workersinteger ≥ 1no50Concurrent upload workers.
upstream-cachestring or array of stringsnohttps://cache.nixos.orgCaches whose paths are skipped. none disables filtering.
release-repostring owner/reponoItzEmoji/aeroflareRepository the GitHub Action downloads aeroflare-ci from.
release-versionstringnothe pinned action's versionRelease to download, or latest.
skip-attestationbooleannofalseSkip provenance verification of the downloaded asset.

Every entry in builds is pushed to every entry in caches. There is no way to route one installable to one cache and a different installable elsewhere.

The last three keys configure the GitHub Action's install step, not a build. aeroflare-ci itself ignores them — see Release source keys.

Complete example​

.aeroflare-ci.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/ItzEmoji/aeroflare/v1/schema/aeroflare-ci.schema.json
builds:
- .#default
- .#packages.x86_64-linux.foo

caches:
- ghcr.io;itzemoji/nix-cache # primary
- docker.io;itzemoji/nix-cache

compression: zstd
workers: 100
signing-key: NIX_SIGNING_KEY

upstream-cache:
- https://cache.nixos.org
- https://nix-community.cachix.org

Key semantics​

builds​

Each entry is a Nix flake installable, built with nix build <installable>.

The single entry all is a sentinel: instead of naming one installable, it expands at run time into everything the flake in the working directory exposes for the runner's system.

.aeroflare-ci.yaml
builds:
- all
caches:
- ghcr.io;itzemoji/nix-cache

Three output classes are discovered:

ClassBuilt as
packages.<system>.<name>.#packages.<system>.<name>
devShells.<system>.<name>.#devShells.<system>.<name>
nixosConfigurations.<host>.#nixosConfigurations.<host>.config.system.build.toplevel

A class the flake does not expose contributes nothing; it is not an error. NixOS configurations built for another platform are skipped, since the runner cannot build them. Discovery only ever looks at the current checkout — there is no syntax for discovering a remote flake.

all may be mixed with explicit installables, and duplicates are dropped:

builds:
- all
- github:some/other#tool
note

Discovery reads the flake's outputs directly and applies no meta filtering. Unlike the NUR template's ci.nix, a package marked meta.broken or unfree is still attempted, and fails the run when it fails to build. Only the derivation's default output is built, not dev/man, and attribute sets marked recurseForDerivations are not descended into.

The entry changed is the second sentinel. It discovers the same outputs, then keeps only those whose derivation differs from the base commit's:

.aeroflare-ci.yaml
builds:
- changed
caches:
- ghcr.io;itzemoji/nix-cache

A commit bumping one package in a repository of twenty therefore builds one package. A commit touching only documentation builds nothing, and the run succeeds.

The comparison is on drvPath, not on file paths or commit messages. Because a derivation's hash covers its inputs transitively, a version bump, an edit to a shared helper and a flake.lock update are all caught by the same mechanism.

Three rules cover the rest, all erring towards building:

At baseAt HEADBuilt
same derivationsame derivationno
different derivation—yes
absentpresentyes — a new package must be built once
—fails to evaluateyes — so the build reports the real error
presentabsentno — there is nothing left to build

changed may be mixed with explicit installables, which are always built, and with all, which subsumes it.

warning

changed needs enough history to reach the base commit. On GitHub Actions that means fetch-depth: 0 on actions/checkout; the default shallow clone has only one commit, and the run falls back to building everything.

base​

The ref changed diffs against. Unset, it is inferred, first match winning:

  1. pull_request events: the target branch's tip.
  2. push events: the commit the branch pointed at before the push.
  3. Otherwise HEAD~1.

Set it explicitly to override — origin/main to diff a whole branch rather than a single push, for instance. Setting it without a changed entry in builds is an error rather than a silent no-op, since nothing else reads it.

on-missing-base​

What happens when no base commit is reachable. That is a first push to a branch (whose reported base is the null SHA), a shallow clone that lacks the commit, or a force-push that orphaned it.

A base that exists but does not evaluate is not this case. The diff first walks back through its first-parent ancestry, up to ten commits, and uses the nearest one that does evaluate, reporting the substitution. Only when none of them evaluates does on-missing-base apply.

ValueBehaviour
all (default)Report why, then build every discovered output.
errorFail the run.
noneReport why, build nothing, succeed.

all is the default because a cache job that over-builds is merely slow, whereas one that silently builds nothing leaves holes in the cache. As with base, setting this without a changed entry is an error.

caches​

Each entry is <registry>;<repository>, separated by a semicolon rather than a slash so the repository may itself contain slashes.

The first entry is the primary cache. It backs the substituter used during the build, and its push token is validated before any build starts — a missing one aborts the run immediately. A missing token for any other cache fails only that push.

An https:// or http:// prefix on the registry is stripped, so https://ghcr.io;me/cache and ghcr.io;me/cache are equivalent.

note

The schema's pattern permits exactly one semicolon. The binary is more lenient — it splits on the first one — but stay within the schema so editor validation keeps working.

Push tokens never appear in this file. They come from AEROFLARE_TOKEN_<HOST> environment variables; see Token resolution.

signing-key​

The value is resolved in this order:

  1. If it names an environment variable that is set and non-empty, that variable's contents are the key material.
  2. Otherwise it is a filesystem path.
  3. If neither, the run fails.

signing-key: NIX_SIGNING_KEY reads $NIX_SIGNING_KEY; signing-key: ./key.sec reads the file. Prefer the environment form in CI. Omit the key entirely to push unsigned NARs.

upstream-cache​

Accepts a bare string or a list:

upstream-cache: https://cache.nixos.org
upstream-cache:
- https://cache.nixos.org
- https://nix-community.cachix.org

An explicit value replaces the default rather than extending it. If you name another upstream and still want nixpkgs paths skipped, list cache.nixos.org yourself.

upstream-cache: none disables filtering entirely and uploads the full closure, making the cache self-contained. It cannot be combined with other entries.

warning

The schema rejects upstream-cache: []. The binary alone would read an empty list as "unset" and substitute the default — the opposite of what an empty list suggests. Write none if you mean no filtering.

Release source keys​

release-repo, release-version and skip-attestation decide which aeroflare-ci binary the GitHub Action downloads. They exist for running a fork or a test build instead of the published upstream release.

.aeroflare-ci.yaml
release-repo: me/aeroflare-fork
release-version: latest
skip-attestation: true # this fork publishes no attestations

release-version accepts 1.11.0, v1.11.0, or latest. Omitted, it resolves to the version declared by the action ref you pinned. latest is what a fork or throwaway test repository usually wants, since its release numbering rarely tracks upstream's.

skip-attestation disables a supply-chain check

Every upstream release archive carries SLSA build provenance, and the Action verifies it on every run. Setting skip-attestation: true means the binary you execute in CI is unverified. Set it only for a repository you control that publishes no attestations — and prefer publishing them, since the release workflow a fork inherits already does.

Three consequences of these keys being read before the binary exists:

  • aeroflare-ci ignores them. They only take effect through the Action. Running the binary directly, or from another CI system, they do nothing.
  • They are read by a minimal parser. The Action's install step extracts them from the file with sed, because the binary that normally parses the config is the very thing being downloaded, and no YAML tool is guaranteed on a runner. It handles top-level keys with optionally-quoted scalar values and inline comments. Anchors, block scalars and multi-document files are not supported for these three keys only; every other key is parsed normally by the binary.
  • Precedence is input, then file, then default. The release-repo, release-version and skip-attestation action inputs override the file. The legacy AEROFLARE_REPO environment variable still works, but ranks below the file.

Precedence​

Flags and environment variables override this file, and list values replace rather than merge. A single --build alongside a file listing three installables builds exactly one.

This is why the GitHub Action refuses config together with builds or cache. See Configuration resolution for the full table.