Skip to main content

Nightly Builds

Automated rolling nightly releases

Nightly builds publish an immutable, commit-stamped prerelease on every run — vX.Y.Z-<shortcommit>-nightly. Each one is its own permanent tag and GitHub release; there is no moving nightly tag to race or overwrite.

Usage

anodizer release --nightly

Behavior

  • Version is rendered from nightly.version_template. The default ({{ incpatch(v=Version) }}-{{ ShortCommit }}-nightly) bumps the patch of the current tag and appends the short commit — e.g. tag v0.13.00.13.1-a1b2c3d-nightly — so two same-day commits yield two distinct, commit-immutable nightly versions.
  • Creates a new tag and GitHub release per run, named for the rendered version (v0.13.1-a1b2c3d-nightly). Nothing is replaced — a second run on the same commit resolves to the same version and converges rather than moving a shared tag.
  • All normal pipeline stages run (build, archive, checksum, release, publish)
  • Distinct from --snapshot — nightlies publish, snapshots don't
  • --nightly does not skip the environment preflight. It runs as the first step unless the mode implies otherwise (--snapshot, --dry-run, --split, --publish-only). Pair the action's auto-install with it so the toolchain preflight checks for is the one anodizer tools provisioned.

Crate selection: pass --all --force

A nightly should build the whole workspace every night. Without an explicit selection, release falls back to reading the tags that point at HEAD:

HEAD stateSelectionResult
untagged (the usual case)empty → no filterevery crate builds
carries a release tagonly crates whose tag_template matchesa subset builds

In a lockstep workspace (one shared tag_template) that subset is still every crate, so the fallback is harmless. In an independent-version workspace it is not: the day a release tag lands on master, the nightly narrows to that one crate — and with it the union of build targets, so any shard whose targets all belonged to the dropped crates fails with no matching targets.

args: release --nightly --split --all --force

--all selects every crate; --force drops the has-unreleased-changes requirement, which a nightly by definition does not want to depend on. Pass the same pair on the merge leg — it validates shard contexts against its own selection, so the two must agree.

Nightly-shaped tags are excluded from crate selection and from stable semver resolution automatically, so a nightly can never narrow a later release. That exclusion landed after v0.22.2; on earlier versions --all --force is what keeps a nightly deterministic.

Config

nightly:
  version_template: "{{ Base }}-nightly.{{ NightlyBuild }}+{{ ShortCommit }}"
  name_template: "{{ Version }}-nightly.{{ Now | date(format='%Y%m%d') }}"
  tag_name: nightly
  publish_release: true       # default true — create a GitHub Release for each nightly run
  publish_repo: nushell/nightly  # optional — publish to a DIFFERENT repo than the source
  retention:
    keep_last: 10             # keep the 10 newest nightly releases, delete the rest (+ tags)
  draft: false                # optional — override release.draft for nightly runs only
FieldTypeDefaultDescription
nightly.tag_namestring"nightly"Name of the rolling git tag created for nightly releases (moved on each run rather than accumulating semver tags).
nightly.name_templatestring"{{ ProjectName }}-nightly"Template for the nightly release name. Distinct from version_template, which renders the version string.
nightly.version_templatestring"{{ incpatch(v=Version) }}-{{ ShortCommit }}-nightly"Template for the rendered nightly version. May reference {{ NightlyBuild }} and {{ Base }} (see below).
nightly.publish_releasebooltrueWhether to create a GitHub Release at all. Set false to build and publish packages without creating a release entry.
nightly.publish_repostring(source repo)Publish the nightly release to a different "owner/repo" than the one resolved from release.github (e.g. a dedicated org/nightly repo). The release create, asset upload, and retention deletes all target this repo. The active token must have write access to it. GitHub-only.
nightly.retention.keep_lastint(none)Keep the N newest nightly releases (matched by the rendered nightly release name) and delete the older ones, including the git tags anodizer created for them. Operates on publish_repo when set.
nightly.keep_single_releaseboolfalseBack-compat alias for retention: { keep_last: 1 } (a single rolling nightly release). When both are set, retention wins.
nightly.draftbool(inherits release.draft)Override the draft flag for nightly runs only.

Build-counter and base-version template vars

Two template vars support nushell-style nightly versioning (<base>-nightly.<build>+<sha6>):

VarSourceResets when
{{ Base }}The numeric base semver (no prerelease / build metadata), captured before nightly templating overwrites Version.Never within a tag; reflects the current tag's MAJOR.MINOR.PATCH.
{{ NightlyBuild }}Stateless per-base build counter — git rev-list --count <last-tag>..HEAD.A new version tag lands (the count returns to a small number automatically — no state anodizer persists).
# nushell-style: 0.103.0-nightly.42+a1b2c3
nightly:
  version_template: "{{ Base }}-nightly.{{ NightlyBuild }}+{{ ShortCommit }}"
  publish_repo: myorg/nightly
  retention:
    keep_last: 10

nightly.publish_repo and nightly.retention are configured at the top level (nightly:); they apply across all crates in a workspace. The {{ NightlyBuild }} counter is global (derived from the repo's git history), so it is identical for every crate in a lockstep or per-crate workspace release.

Publisher skip behavior

Some publishers opt out of nightly runs automatically to avoid polluting stable package manager indexes with date-stamped pre-release versions.

Publishers that skip on nightly by default:

  • homebrew, homebrew_casks — formula/cask updates for nightlies break brew upgrade for stable users
  • scoop — bucket manifests for nightlies shadow the stable manifest
  • aur, aur_source — AUR packages are expected to be stable releases
  • krew — kubectl plugin index is semver-gated
  • nix — nixpkgs and tap entries track stable releases
  • cargo — crates.io does not allow pre-release overwrites
  • npm — npm does not allow re-publishing the same version
  • chocolatey — community gallery is moderation-gated
  • winget — PR-based; nightly versions are rejected by automated review

Publishers that do not skip on nightly (they accept clobber):

  • dockerhub, dockers_v2 — image tags like nightly or edge are conventional
  • cloudsmith, artifactory — private registries; republish is explicit via republish: true
  • blob — object storage; nightly assets overwrite by key
  • mcp — registry entry is idempotent

To override skip behavior for a specific publisher, set skips_on_nightly: false in that publisher's config block.

CI integration

Run nightly builds on a schedule:

# GitHub Actions
on:
  schedule:
    - cron: "0 2 * * *"    # 2 AM UTC daily
  workflow_dispatch: {}    # also allow on-demand nightly runs

permissions:
  contents: write          # create/replace the rolling `nightly` release + tag
  id-token: write          # keyless cosign signing via Fulcio/OIDC
  attestations: write      # mint SLSA build provenance

jobs:
  nightly:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - uses: tj-smith47/anodizer-action@v1
        with:
          install-rust: true
          # auto-install queries `anodizer tools` for the exact toolchain this
          # config needs and installs it — including the cross-compile tools
          # (cargo-zigbuild + zig, or `cross`) resolved from each build's
          # target and `cross:` strategy. No need to hand-list them.
          auto-install: true
          args: release --nightly --all --force
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

anodizer dogfoods this on its own repo with a scheduled nightly.yml (from-branch: master, daily 0 4 * * * cron plus on-demand workflow_dispatch), and cfgd runs the same shape against a pinned release.

Two things that example carries which the minimal one above does not:

  • Split/merge sharding. One runner cannot serve a target matrix that spans Apple frameworks and MSVC. Each host builds its partial.by: os slice with --split, and a final leg fuses them with --merge. See Split / merge.
  • A per-shard job timeout. A nightly that builds from source pays for compiling anodizer itself before it can run, on top of every target it then builds. On a Windows runner that is roughly 18 minutes before the first target starts and ~35 minutes per MSVC target. Size timeout-minutes per shard against the slowest one — a job that exceeds it is reported as cancelled, not failed, so an undersized timeout looks like an infrastructure blip rather than a build that needs longer.