Skip to main content

Environment Preflight

Config-derived environment checks that run before any release stage

Anodizer derives everything the configured release needs from the resolved config — required CLI tools, env vars and secrets, endpoint reachability, docker daemon availability, and loadable key material — and verifies all of it before any stage runs. There is nothing to configure: requirements are declared next to each stage and publisher implementation, so the check surface cannot drift from what the pipeline actually reads.

Inside anodizer release

The preflight runs automatically at the start of anodizer release and anodizer release --publish-only (scoped to the stages that mode runs). Every failure is collected in one pass and the release aborts before any side effect:

• preflight: 4 of 24 check(s) failed:
•   ✗ required tool 'cosign' not found on PATH [needed by: stage:sign, stage:docker-sign]
•   ✗ env var(s) missing or empty: COSIGN_KEY [needed by: stage:sign, stage:docker-sign]
•   ✗ env var AUR_SSH_KEY does not hold a usable SSH private key: missing trailing newline after end marker [needed by: publish:aur]
•   ✗ endpoint 'http://minio.svc:9003' unreachable: connection refused [needed by: stage:blob]
Error: preflight: 4 environment failure(s) across 24 check(s); fix the issues above or re-run with --no-preflight to override

Secret values are never printed — only env-var names. Key material (SSH, PGP, cosign) is structurally parsed, not just checked for presence, so the classic "key works locally but the CI secret lost its trailing newline" failure is caught before a publisher half-runs.

Snapshot and dry-run invocations skip the preflight (no upstream side effects to guard); --split skips it because split legs are operator-orchestrated partial pipelines. --announce-only runs a preflight scoped to the announce stage's requirements: announcers fire sequentially with real side effects, so a missing token aborts before the first post instead of after half the channels are notified. --no-preflight overrides it explicitly.

Standalone command

The same engine is exposed as a command — useful as a CI canary or a local "can this machine cut the release?" check:

$ anodizer preflight                    # full pipeline surface
$ anodizer preflight --publish-only     # only what `release --publish-only` runs
$ anodizer preflight --json             # machine-readable report
$ anodizer preflight --skip=docker,blob # same stage names as release --skip

The exit code is non-zero when anything is missing, and the JSON report carries a kind per failure (missing_tool, missing_env, endpoint_unreachable, docker_unavailable, bad_key_material).

Secrets-only pre-tag gate

Decoupled CI pipelines split a release across many runners — build and determinism shards on different hosts, plus a dedicated publish runner — that carry different host-local tools but the same injected secrets (CI secrets are exported into every job). For those pipelines, a single up-front gate should answer "are all the publish credentials present and well-formed?" without false-failing on a tool that only the eventual publish host has.

anodizer release --preflight-secrets is that gate. It collects the full release surface, then keeps only the runner-agnostic credential requirements — env vars and env-borne key material (COSIGN_KEY, GITHUB_TOKEN ladders, env:// SSH/PGP keys, …) — and drops every host-local requirement (CLI tools, the docker daemon, endpoint reachability, and on-disk key files, which may not be materialized on the gate runner). Env-borne key material is still structurally validated, so a malformed secret key is caught before a tag is minted; on-disk key files are not checked by this gate. The check runs zero mutations: no before: hooks, no network probes, no pipeline.

$ anodizer release --preflight-secrets
 preflight-secrets: all required publish secrets / credentials present

Wire it as the root job a release depends on, so a missing CI secret aborts before the tag is created (and before the expensive determinism matrix runs):

jobs:
  preflight:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # so OIDC-provenance request vars are present to check
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup-rust
      # Build from the checkout (not a published anodizer) so a brand-new
      # release that adds a publisher's secret is gated by THIS commit.
      # --skip=<stage> any stage whose secret is RUNNER-AMBIENT rather than a
      # registered CI secret (e.g. a self-hosted runner's ambient cloud creds):
      # this github-hosted gate cannot see those, so demanding them here would
      # false-fail and block every release. They are validated in-pipeline on
      # the runner that holds them.
      - run: cargo run -q --release -p anodizer -- release --preflight-secrets --skip=blob
        env:
          GITHUB_TOKEN: ${{ secrets.GH_PAT }}
          COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          # …every secret the downstream publish jobs consume as REGISTERED CI
          # secrets (exclude runner-ambient ones via --skip above)…

  tag:
    needs: [preflight]
    if: needs.preflight.result == 'success'
    # …auto-tag only once the secret gate is green…

--preflight-secrets and --no-preflight are mutually exclusive. The gate runs even when HEAD carries no release tag (it is a pre-tag check) and ignores dirty-tree / dist state, since it never reads or writes either.

What gets derived

SurfaceDerived requirements
buildscargo
nfpms / srpmsnfpm / rpmbuild + signing key material from signature: blocks
snapcraftssnapcraft, unsquashfs; SNAPCRAFT_STORE_CREDENTIALS when publish: true
signs / binary_signs / docker_signsthe signing cmd, env refs in args/stdin, env://VAR cosign keys validated as key material
sboms / makeselfs / appimages / upxsyft (or custom cmd), makeself, linuxdeploy, upx
dockers_v2docker + reachable daemon
blobsrendered S3 endpoint reachability, static keypair for custom endpoints, KMS CLIs
verify_release.install_smokedocker + reachable daemon
msiswix (v4) or candle + light (v3) — same explicit-version: > .wxs-namespace-sniff > installed-tool-probe policy the build uses; only when a Windows target is configured
nsismakensis; only when a Windows target is configured
pkgspkgbuild; only when a macOS target is configured
dmgsany of hdiutil / genisoimage / mkisofs (the stage's own detection ladder); only when a macOS target is configured
flatpaksflatpak-builder + flatpak; only when a Linux target is configured
app_bundlesnothing — the stage assembles the .app layout with pure file operations
notarizercodesign + env refs in certificate: / password: / API-key fields (cross-platform), codesign + xcrun + env refs in identity: / keychain: / profile_name: (native)
announceper-announcer secrets exactly as the senders read them — e.g. SLACK_WEBHOOK (or env refs in a templated webhook_url:), TELEGRAM_TOKEN, DISCORD_WEBHOOK_ID+DISCORD_WEBHOOK_TOKEN, full Twitter/Reddit/Mastodon credential sets, and SMTP_HOST / SMTP_USERNAME / SMTP_PASSWORD for email (password only when encryption is enabled)
publishersper-publisher token ladders (e.g. HOMEBREW_TAP_TOKENGITHUB_TOKEN), per-entry secret env names, AUR SSH keys

Entries disabled via skip: / skip_upload: / a falsy if: contribute nothing, and in per-crate workspace mode the requirements are the union across every publishable crate — one preflight covers the whole run.

The per-platform bundler stages (msis, nsis, pkgs, dmgs, flatpaks) contribute requirements only when the configured build targets include their platform — mirroring each stage's own run gate, so a Linux-only matrix never demands WiX. Announce requirements are checked in both the full and --publish-only scopes (the publish-only pipeline runs announce), and --announce-only checks them alone — the only stage that mode runs.