Skip to content

Build and test with Nix CI ​

This example builds a small executable, runs a separate check, and passes both results to a dependent app. Start here if your repository does not yet expose CI outputs; use the Nix workflow reference for the complete schema.

Upcoming functionality

Nix workflows require an experimental Nix-enabled coordinator and compatible trusted providers. These instructions describe the upcoming implementation, not a feature available from every deployed coordinator.

First request coordinator service. Ask the operator whether it has trusted x86_64-linux capacity. Two available provider slots allow the independent jobs below to run concurrently; they can be on different machines. A repository selects a system, not a hostname. One slot is sufficient for this example, but the jobs will run sequentially.

1. Define the work in the root flake ​

Create flake.nix:

nix
{
  description = "A small remote CI example";
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/b5aa0fbd538984f6e3d201be0005b4463d8b09f8";

  outputs = { nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
      hello = pkgs.writeShellApplication {
        name = "hello-ci";
        text = ''echo "hello from Nix CI"'';
      };
      tests = pkgs.runCommand "ci-tests" { } ''
        test "$(printf '%s' 'hello from Nix CI' | wc -w)" -eq 4
        mkdir -p "$out"
        printf 'PASS\n' > "$out/report.txt"
      '';
      verify = pkgs.writeShellApplication {
        name = "verify-ci";
        runtimeInputs = [ pkgs.coreutils pkgs.bash ];
        text = ''
          report="$NGIT_CI_INPUTS/test/artifacts/report/report.txt"
          binary="$NGIT_CI_INPUTS/build/artifacts/binary/bin/hello-ci"
          test "$(cat "$report")" = PASS
          test "$(bash "$binary")" = "hello from Nix CI"
          printf 'Both artifacts verified\n' > "$NGIT_CI_OUTPUT_DIR/summary.txt"
        '';
      };
    in {
      packages.${system}.hello = hello;
      checks.${system}.tests = tests;
      apps.${system}.verify = {
        type = "app";
        program = "${verify}/bin/verify-ci";
      };
    };
}

The small check is deliberately independent of the package so the example has two independently schedulable jobs. Replace it with your project's tests. If your tests depend on the package, express that dependency in Nix; it can reuse the same derivation rather than rebuilding through an extra CI step.

The verification app invokes the downloaded script through Bash. Artifact transfer supplies file bytes, not a Nix runtime closure or executable mode. For a real binary, package its required runtime or provide it in the consuming app's closure. Declared artifacts are individual files; archive a directory before exporting it.

2. Check the outputs locally ​

On an x86_64-linux Nix machine:

sh
nix flake lock
git add flake.nix flake.lock
nix flake check
nix build .#packages.x86_64-linux.hello --out-link result-build
./result-build/bin/hello-ci
nix build .#checks.x86_64-linux.tests --out-link result-tests
cat result-tests/report.txt

Commit both flake files. In a Git repository, Nix needs new source files to be tracked before it includes them. The lock file pins the inputs used locally and by providers. A cached check counts as a successful realization; use a changed test input when you need to demonstrate a fresh execution.

3. Run pure checks on pull requests ​

Create .ngit/nix/workflows/check.yaml:

yaml
version: 1
name: Build and test
on:
  pull_request:
jobs:
  test:
    build: checks.x86_64-linux.tests
    artifacts:
      report: report.txt
  build:
    build: packages.x86_64-linux.hello
    artifacts:
      binary: bin/hello-ci

Ordinary PR runs require hardware-isolated providers for both builds and secret-free apps. They cannot request repository secrets or write to the shared cache, including when the PR author is a maintainer. Start with the two builds here, then add the dependent app once you have checked its behavior.

If you are migrating from act, remove the corresponding act PR trigger when you enable this one. Both workflow families run independently; leaving both matching triggers repeats the checks.

4. Verify the handoff in an authorized run ​

Create .ngit/nix/workflows/verify.yaml:

yaml
version: 1
name: Verify remote artifacts
on:
  workflow_dispatch:
jobs:
  test:
    build: checks.x86_64-linux.tests
    artifacts:
      report: report.txt
  build:
    build: packages.x86_64-linux.hello
    artifacts:
      binary: bin/hello-ci
  verify:
    needs: [test, build]
    run: apps.x86_64-linux.verify
    artifacts:
      summary: summary.txt

Commit and publish the files on a pr/ branch. A confirmed maintainer can then request this workflow for the exact published commit:

sh
ngit ci trigger <COORDINATOR> <COMMIT> \
  --workflow .ngit/nix/workflows/verify.yaml --json
ngit ci status <COMMIT> --json

verify becomes eligible only after both successful provider-signed results are accepted. Its app reads the downloaded files under NGIT_CI_INPUTS and writes its own declared artifact under NGIT_CI_OUTPUT_DIR. The coordinator coordinates these jobs; providers execute them and transfer artifacts through Blossom. No shared filesystem between machines is required.

To run verification automatically on every PR, move the verify job into check.yaml and remove the separate verification workflow. The same pattern supports a preview publisher with a disposable identity and a public nsite_preview URL, without production credentials. Do not enable identical PR triggers in both files, which would repeat the builds.

A manual verification run selects the same build outputs as the PR checks, so Nix may reuse them. These are separate attestations and workflow runs; the manual run does not attach an extra job to the earlier PR run.

5. Check what actually happened ​

In GitWorkshop, open the run and inspect each job's provider, result and artifacts. Download the final summary.txt; it should contain Both artifacts verified. Two jobs in a workflow do not alone prove that two machines executed simultaneously: verify the provider identities and timings.

A pending job can mean no eligible provider has capacity. Check advertised systems, trust and isolation with the operator before changing the workflow. An advertisement is a recent capacity report, not a reservation; a provider can decline an allocation if another job took its slot. See provider waiting and dependencies.

For automation, read ci.state and ci.conclusion from ngit ci status --json. Top-level command_status: "ok" means the query succeeded. Use a fresh online query to observe progress; --offline only rereads cached results.

Continue with secret-bearing work for publishing and previews, or run a Nix provider for the operator setup. Publishing apps need their own authorization and credentials; a passing build is not a deployment.