Shared workflows and small actions for nix-forge repositories. Package and platform build definitions remain in their source repositories.
For tagged source archives, see release provenance and verification.
Read the workflow architecture and migration guide.
| Workflow | Caller permissions | Contract |
|---|---|---|
flake-lock.yml |
contents: read |
Required lockfile input; fails on missing or unhealthy lockfiles |
codeql.yml |
contents: read, security-events: write |
languages input; analysis using build-mode none |
dependency-review.yml |
contents: read |
Fail on new vulnerabilities at low severity or higher; optional base-ref and head-ref for dispatch |
request-review.yml |
pull-requests: write |
Metadata-only pull_request_target; human PRs excluding the maintainer |
nur.yml |
contents: read |
Caller supplies scripts/check-nur.py and tests/nur-supported.nix |
Call reusable workflows at the job level using a full commit SHA. Callers retain events, concurrency and repository-specific inputs. Permissions cannot increase inside a called workflow. No shared workflow inherits all caller secrets.
jobs:
lock:
permissions:
contents: read
uses: nix-forge/ci/.github/workflows/flake-lock.yml@RELEASE_COMMIT_SHA
with:
lockfile: flake.lockReplace RELEASE_COMMIT_SHA with the full commit listed in a tested release.
CodeQL builds requiring custom commands, such as Swift, stay in the caller.
Scorecard publication also stays local because its publishing API restricts the
workflow's steps and OIDC context.
actions/fuzz-atheris runs a bounded Atheris campaign against an existing
fuzz/*.py file in the checked-out caller. The caller retains its event triggers,
required job name, checkout, permissions, runner, and any target-specific setup
such as Nix. The action pins Python setup and Atheris, rejects paths outside the
top-level fuzz/ directory, limits the campaign to 600 seconds and each input to
120 seconds, and uploads crashing inputs for seven days on failure. Its defaults
are Python 3.12, a 180-second campaign, and a 10-second input timeout. Repositories
that use Python 3.14 or invoke Nix during fuzzing set python-version: '3.14' or
input-timeout: '60' respectively. Call the action at a full tested commit SHA:
- uses: nix-forge/ci/actions/fuzz-atheris@RELEASE_COMMIT_SHA
with:
target: fuzz/target_fuzzer.pyactions/repository-checks materializes the caller's partitions, then enters its
Nix shell once to run hooks and publication checks. Check out full history with
fetch-depth: 0. The default scan checks HEAD history and an archive of the complete
committed tree. An optional historical baseline never suppresses current-tree
findings. Repositories can supply a publication script for stricter local policy.
These are read-only build steps; do not use them in privileged metadata jobs.
The optional dev-shell input selects a repository shell and defaults to
default. A dedicated hook shell can avoid building product binaries or pulling
interactive development tools into lint jobs. Keep native product validation in
the repository's build and integration jobs.
actions/validate-workflows runs the library's pinned actionlint, Zizmor,
Yamllint and consumer contract validator against the caller's workflows, composite
actions and onboarding templates. It rejects mutable external references, mixed
shared releases, persisted checkout credentials, shallow history scans, missing
queue triggers and callbacks, missing timeouts, broad default permissions and
incomplete template metadata. Set up Nix before calling either action.
Validation includes both action.yml and action.yaml beneath actions/ and
.github/actions/. Repositories containing only composite actions run the
applicable action checks without invoking the workflow-only linter.
actions/setup-nix installs the tested Determinate Nix version. Its optional
cache: 'true' uses GitHub's repository-scoped cache. Give different build jobs
different cache-scope values. Cache keys include platform, Nix version, lockfiles
and commit; restore prefixes remain scoped to matching lockfiles. The action only
saves on trusted default-branch pushes or dispatches, attempts to reduce the store
to 2 GiB before saving, and never requests cache-purge permissions. GC cannot remove
live roots, so the size target is not a hard quota. Measure restore and upload time
before enabling it broadly. The cache is disabled by default.
This action archives the Nix store. Enable it only for workloads whose entire
store can be shared with contributors. Fork pull requests can read base-branch
caches (GitHub cache access rules).
Nix's allowSubstitutes = false controls substitution; it does not exclude a path
from this archive. Keep jobs producing secrets or redistribution-restricted
outputs uncached.
actions/reconcile-queue executes the packaged script from this repository,
without checking out caller code. Supply the JSON workflows list and optional
source-run-id; the workflow grants checks read and actions, contents,
pull-requests and statuses write. It preserves commit/run-attempt checks and never
mirrors skipped validation as success. actions/queue-completion notifies this
trusted reconciler from dispatched queue runs. Retain the caller's existing
Queue completion callback name and event guard.
The actions read the default branch from GitHub's repository event metadata.
Use that same default branch in the caller's callback guard. A package updater
can opt in to admission with automation-branch; it accepts only that exact
branch from the same repository and the github-actions[bot] author. Dependabot
admission remains enabled. Automation file changes still require human admission.
Set base-sha-workflows: '["ci.yml"]' for a workflow that declares a base_sha
dispatch input. This supplies the queue commit's first parent for derivation
comparison without teaching the shared library repository names.
Reconcile after pull-request and merge-group validation completes. Dispatched queue validation uses its completion callback. Push scans and scheduled security scans do not need another reconciliation job. Keep hourly scheduled recovery for delayed queue refs or missed callbacks. Reducing the previous three hourly polls to one saves 48 scheduled runs per repository per day, while normal queue progress remains event-driven. A missed callback can wait until the next hourly run, and GitHub can delay scheduled jobs further.
After observing a queued bot PR, reconciliation waits up to 55 seconds for the live front entry's validation ref. GitHub can acknowledge admission before that ref exists; old refs do not satisfy readiness for a different queued commit. Empty queues do not wait. Keep scheduled reconciliation as a backup for longer GitHub delays; exhaustion emits a warning and never invents passing checks.
Completed failures, timeouts and cancellations from the latest dispatch attempt are reported as failures, so they release the queue instead of blocking later PRs until timeout. Other workflows need not finish before reporting a verified failure. Passing results still require the complete configured workflow set to succeed. Missing, skipped and stale evidence never becomes a pass.
The queue fallback remains necessary with current GITHUB_TOKEN admission.
Replacing it requires an installed GitHub App and proof that app-authenticated
admission produces native merge-group checks. No app key is required for this
release. Do not provide privileged tokens to PR build steps.
actions/discover-lockfiles emits a JSON lockfiles output for a workflow matrix.
It requires a tracked flake.lock for each tracked flake.nix in the caller
repository. Deleting only the lockfile fails discovery; deleting the entire
partition removes its matrix entry. Added, renamed and deleted partitions change that inventory
without edits to the workflow. Submodules own their lockfile health jobs.
Require a stable aggregate job that waits for the complete matrix and fails for
any failed, cancelled or skipped matrix. Protect that aggregate instead of
individual lockfile names, so adding coverage cannot be bypassed and deleting a
partition does not leave an obsolete required status. Migrate existing branch
rules only after the aggregate has passed on PR and merge-group runs.
actions/materialize-partitions discovers nested flakes across the checkout and
initialized submodules. actions/repository-checks now uses this discovery by
default. Its partitions input still accepts explicit newline-separated paths
when a repository needs a different boundary.
actions/flake-checks takes a native system and builds every name in
checks.<system>. Each check gets a separate Nix evaluation with one evaluator
thread and one build job. Build cores default to two and can be configured.
A caller whose complete native check set exceeds one runner's practical time
limit can run deterministic, disjoint subsets in parallel. Give each matrix job
the same partition-count and a distinct zero-based partition-index; sorted
check names are assigned round-robin, and an empty partition fails instead of
silently dropping coverage.
When check costs differ substantially, a caller can pass a weights-file path
to a JSON file with a positive default number and optional weights mapping
from check names to positive relative costs. The action then assigns heavier
checks first to the least-loaded partition. The weights stay with the caller so
its CI can tune them without changing the shared action.
A failed check does not suppress later checks; the action fails if any fail.
Failed, malformed or empty discovery cannot accidentally build a default package
or report success. Register platform availability in the flake that owns the
check, rather than maintaining a second CI allowlist. This follows the
Nix checks contract.
On pull requests and merge groups, actions/flake-checks compares every current
check derivation with the event's base commit. The action performs a bounded
fetch when a shallow checkout does not contain that commit and marks its local
Git flake URL as shallow for Nix evaluation. It skips a build only
when both derivation paths are identical. New checks, changed checks, unavailable
history, and base evaluation failures retain the conservative full-build
behavior. The optional base-revision input overrides event discovery for manual
dispatches. Each base check evaluation has a 60-second limit by default; a
timeout builds the current check instead of waiting on historical evaluation.
Set base-eval-timeout to zero only when complete comparison is worth an
unbounded wait.
Workflow syntax and policy validation share one inventory, including nested
composite actions using either .yml or .yaml. Explicit supported runner maps,
security expectations, action commit pins and deployment policy remain deliberate
configuration. Discovery does not change repository permissions or merge rules.
nix flake check
nix run .#validate-workflows -- /path/to/consumer
nix develop --command bash scripts/check.shvalidate-workflows packages the workflow validators, policy file and Python
contract checker together. It accepts the consumer repository directory and
supplies its own tool PATH. The composite action invokes this same package.
checks.validation also runs shell lint, Python lint and the regression suite
inside the Nix build sandbox. The privileged queue actions retain their
API-only execution model and do not enter a Nix environment.
Validation covers actionlint, pedantic Zizmor, YAML, Ruff and the consolidated queue regression cases. Consumer CI must also pass on PR, merge-group and manual dispatch events. A called workflow can change check names; inspect GitHub's actual checks and update required-check configuration together with each migration.
Publish semver releases after validation. Consumers pin the release commit and Dependabot proposes updates. Shared privileged-automation changes require review even when labelled as patch updates. Roll back a consumer by restoring its prior SHA. Action pins do not automatically pin every downloaded runtime dependency.
The source is MIT licensed. Third-party actions and Nix packages retain their own licenses. The library neither builds the nix-conf desktop closure on hosted runners nor publishes package outputs to an external binary cache.
Version 2 removes automerge.yml. Remove its consumer wrapper and its name from
reconciler workflow-run triggers. The existing reconciler owns automatic admission
after required checks pass. It requires human admission for .github/, actions/,
scripts/ and workflow-templates/ changes, including rename source paths. This
leaves one admission policy and avoids a redundant privileged workflow per bot PR.
Existing immutable version 1 references still resolve to their original contents.
Update every shared reference in one consumer to the same tested release commit. Composite actions preserve existing required job names. Preserve specialized builds and Scorecard publication, and validate both PR and merge-group events. See the research and measurements for the decisions and limits of the performance evidence.
Use the release-sync command to prepare a rollout across local checkouts:
python3 scripts/sync-release.py --check /path/to/consumer /path/to/community
python3 scripts/sync-release.py /path/to/consumer /path/to/communityIt resolves GitHub's latest published stable release to a commit SHA and updates
workflows, nested composite actions and onboarding templates together. --check
reports drift without writing and exits nonzero when updates are needed. Use
--release v2.4.0 for a deliberate rollback to that published release. The command
does not commit, push or merge. Validate each changed checkout before publication.
Keep nix-forge/ci in its own Dependabot group so unrelated action updates do not
make the shared release diff harder to review.
Dependabot scans both root workflows and /actions/*, grouping each action
across directories. Without the explicit composite-action directories, the root
configuration misses their nested third-party pins. New composite actions are
covered by the glob. See the
Dependabot directory and grouping reference.
Repositories with host-only deployment checks can expose a separate, system-indexed
CI output and pass checks-output: ciChecks. Derive that output from the ordinary
checks and the deployment component's own check inventory. Keep the host-only
checks available through checks and the existing deployment commands. Missing
or empty CI outputs fail discovery, and every selected check still must pass.