Skip to content

Repository files navigation

nix-forge CI

OpenSSF Best Practices OpenSSF Developer Best Practices OpenSSF Scorecard

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.

Shared workflows

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.lock

Replace 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

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.py

actions/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.

Discovered inputs

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.

Development and releases

nix flake check
nix run .#validate-workflows -- /path/to/consumer
nix develop --command bash scripts/check.sh

validate-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 migration

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/community

It 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.

About

Pinned, least-privilege GitHub Actions workflows and reusable CI contracts for nix-forge repositories.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages