diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml new file mode 100644 index 0000000..50b0e70 --- /dev/null +++ b/.github/actions/setup-nix/action.yml @@ -0,0 +1,87 @@ +name: Setup Nix +description: >- + Install Nix and restore the devShell closure from the Actions cache. + +inputs: + cache: + description: >- + Restore and save the Nix store through the Actions cache. On Depot + runners the Actions cache is Depot Cache. + default: 'true' + primary-key: + description: >- + Cache key base. Empty derives one from the runner OS and architecture + and the digest of flake.lock. + default: '' + github-token: + description: >- + Token the Nix daemon uses for `github:` flake inputs (access-tokens in + nix.conf). The workflow token cannot read private sibling repositories, + so pass a bot token when the flake does. + default: ${{ github.token }} + git-auth-token: + description: >- + Token written into a global git `insteadOf` rewrite, for `git+https` + flake inputs from private repositories. Empty disables the rewrite. + default: '' + +outputs: + cache-hit: + description: >- + 'true' when the Nix store came from any cache entry, including a + partial match restored through the prefix. + value: ${{ steps.cache.outputs.hit }} + cache-key: + description: Cache key the store was saved under. + value: ${{ steps.key.outputs.primary }} + +runs: + using: composite + steps: + - name: Install Nix + uses: cachix/install-nix-action@75c75c31ebece974e48ebc36b4ad125bd97bd97b # 31.11.3 + with: + github_access_token: ${{ inputs.github-token }} + # Pinned so every repository runs the same Nix. Bump this line + # deliberately; pinact and Renovate do not track it. + install_url: https://releases.nixos.org/nix/nix-2.35.4/install + + # Nix's git fetcher shells out to git, so it honours this rewrite. The + # daemon's `access-tokens` from the installer covers `github:` inputs and + # does not cover `git+https` inputs. + - name: Authenticate git for private flake inputs + if: ${{ inputs.git-auth-token != '' }} + shell: bash + env: + GIT_AUTH_TOKEN: ${{ inputs.git-auth-token }} + run: >- + git config --global + url."https://${GIT_AUTH_TOKEN}@github.com/".insteadOf + "https://github.com/" + + - name: Compute cache key + id: key + if: ${{ inputs.cache == 'true' }} + shell: bash + env: + PRIMARY_KEY: ${{ inputs.primary-key }} + PREFIX: ${{ format('nix-v3-{0}-{1}-', runner.os, runner.arch) }} + LOCK: ${{ hashFiles('**/flake.lock') }} + run: | + set -euo pipefail + primary="${PRIMARY_KEY:-${PREFIX}${LOCK}}" + printf 'primary=%s\nprefix=%s\n' "$primary" "$PREFIX" >> "$GITHUB_OUTPUT" + + - name: Restore Nix store + id: cache + if: ${{ inputs.cache == 'true' }} + uses: nix-community/cache-nix-action@7df957e333c1e5da7721f60227dbba6d06080569 # v7.0.2 + with: + primary-key: ${{ steps.key.outputs.primary }} + # A cache miss after a flake.lock change still restores the previous + # store, so only the changed paths download. The entry is then saved + # under the new primary key. + restore-prefixes-first-match: ${{ steps.key.outputs.prefix }} + paths: | + ~/.cache/nix + /nix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0d1560b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: {} + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + setup-nix: + name: setup-nix + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Nix + id: nix + uses: $/.github/actions/setup-nix + + - name: Check the devshell evaluates + run: nix develop .#dev --command nix --version + + - name: Report cache + env: + CACHE_HIT: ${{ steps.nix.outputs.cache-hit }} + CACHE_KEY: ${{ steps.nix.outputs.cache-key }} + run: echo "cache-hit=$CACHE_HIT key=$CACHE_KEY" + + actions-lint: + name: zizmor and pinact + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Nix + uses: $/.github/actions/setup-nix + + - name: zizmor + run: nix develop .#dev --command zizmor --format=plain --min-confidence=medium . + + # --verify resolves every SHA back to its tag, and --min-age holds back + # releases younger than three days. Both read the GitHub API, so pass a + # token to stay clear of the anonymous rate limit. + - name: pinact + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + mapfile -t files < <(find .github -name '*.yml' -o -name '*.yaml') + nix develop .#dev --command pinact run --check --verify --min-age 3 "${files[@]}" diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml index 8206dd7..07f84c4 100644 --- a/.github/workflows/update-flake-lock.yml +++ b/.github/workflows/update-flake-lock.yml @@ -18,8 +18,11 @@ jobs: - name: Install Nix uses: DeterminateSystems/determinate-nix-action@bafaa638b9d5ec0e7e3ac1a7fc80453ef1fd265f # v3.20.0 + # nixpkgs stays put: the locked revision is the last one whose zizmor + # builds on both architectures. Update it by hand, with `nix flake lock + # --update-input nixpkgs`, after checking that. See AGENTS.md. - name: Update flake.lock - run: nix flake update + run: nix flake update flake-parts mattware treefmt-nix - name: Validate flake run: nix flake check diff --git a/AGENTS.md b/AGENTS.md index 2a09128..4b3dddc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,7 @@ This repo (`planetscale/nix-devshell`) exports shared flake-parts modules consum ``` flake.nix # exports flakeModules, owns the dev shell for this repo +.github/actions/setup-nix/ # composite action consumers use in CI: install Nix + Actions cache modules/ base.nix # company-wide tools, sets systems via lib.mkDefault go.nix # Go toolchain, parameterized by version (mattware + version args) @@ -106,10 +107,43 @@ A module that only uses nixpkgs (no nix-devshell inputs needed) skips the first - **`x86_64-darwin` is not supported** — base sets `lib.mkDefault` to the three supported systems; consumers can restrict further for Linux-only projects - **`inputsFrom` does not propagate env vars or shellHook** — consumers needing shared config across multiple shells must use a `let mkDevShell = ...` pattern +## The setup-nix Action + +`.github/actions/setup-nix` installs Nix and restores the Nix store through +`nix-community/cache-nix-action`. Consumers pin it by SHA: + +```yaml +- uses: planetscale/nix-devshell/.github/actions/setup-nix@ # vX.Y.Z +``` + +Constraints that must not regress: + +- `magic-nix-cache-action` is no faster than no cache on Depot runners. Measured + with all three installers, a warm `nix develop` took 29 to 32 seconds, the + same as a cold one. With plain Nix every store upload fails with + `405 Method Not Allowed`, once per path. With Determinate Nix the action + reports `Subscribing to Determinate Nixd build events` and logs no uploads at + all. `cache-nix-action` restores in 4 to 9 seconds and leaves `nix develop` + at under a second, so use that instead. + + Untested: whether `permissions: actions: write` changes the 405, and why + Determinate Nix logs no uploads. Do not restate either as fact. +- Every `run:` step in the composite takes its inputs through `env:`, never + through `${{ }}` interpolation, so zizmor's template-injection audit stays + quiet. +- All `uses:` in `.github/` stay SHA-pinned with a version comment. + `.github/workflows/ci.yml` enforces this with `pinact run --check --verify`. +- `pinact` and `zizmor` come from `devShells.dev`. zizmor only builds because + the `nixpkgs` input is pinned to a revision: 1.32.0 fails to build on both + architectures (aws-lc-sys wants a system AWS-LC that nixpkgs does not + provide). The pin is a full revision, not `nixos-unstable`, so `nix flake + update` leaves it alone. Move the pin forward only after checking that + zizmor builds and is in cache.nixos.org for both architectures. + ## Dev Shell for This Repo ```bash nix develop # enters the base shell (default = devShells.base) -nix develop .#dev # treefmt wrapper (nixfmt, statix, deadnix) +nix develop .#dev # treefmt wrapper (nixfmt, statix, deadnix) plus pinact and zizmor nix flake check # runs treefmt check + validates all devShells ``` diff --git a/README.md b/README.md index 77fe45a..6ce71bc 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,59 @@ leaves that file alone and the shell follows it as-is. The imported version provides `devShells.zig` (zig, matching zls, ziglint, zigdoc). - `nix` — Nix linting and formatting (nixfmt, statix, deadnix) - `queryPath` — Query Path team module + +## GitHub Actions + +The repository ships a composite action at `.github/actions/setup-nix`. It +installs Nix and restores the devShell closure from the Actions cache. On Depot +runners the Actions cache is Depot Cache. + +```yaml +jobs: + test: + runs-on: depot-ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@ # v7.0.1 + with: + persist-credentials: false + + - uses: planetscale/nix-devshell/.github/actions/setup-nix@ # v0.1.0 + + - run: nix develop .#dev --command just test +``` + +Pin the action by SHA and keep the version in a comment. Renovate and pinact +update both. A job that uses the action needs no other Nix setup. + +### Inputs + +| Input | Default | Description | +| --- | --- | --- | +| `cache` | `true` | Restore and save the Nix store through the Actions cache. | +| `primary-key` | derived | Cache key. The default uses the runner OS and architecture and the digest of `flake.lock`. | +| `github-token` | workflow token | Token for `github:` flake inputs. Pass a bot token when the flake reads private sibling repositories, because the workflow token cannot. | +| `git-auth-token` | empty | Token for a global git `insteadOf` rewrite. This covers `git+https` flake inputs from private repositories. | + +### Outputs + +| Output | Description | +| --- | --- | +| `cache-hit` | `true` when the Nix store came from a cache entry, including a partial match. | +| `cache-key` | Cache key the store was saved under. | + +### Cache behaviour + +The key contains the `flake.lock` digest. When the lock file changes, the action +restores the previous store through the key prefix and downloads only the paths +that changed. The job then saves a new entry under the new key. + +The action installs one Nix version for every repository: `install_url` is +pinned to a release, and it does not follow the latest Nix. Bump that line when +you mean to move the org. + +Measured on Depot runners, restoring one project devShell closure (3.3 GiB over +149 paths) takes 4 to 9 seconds. A cold download from cache.nixos.org takes 24 to +31 seconds. `DeterminateSystems/magic-nix-cache-action` gives no speedup on +Depot runners: the store uploads fail or never happen, so every job stays cold. diff --git a/flake.lock b/flake.lock index bd8eb0f..a3e9076 100644 --- a/flake.lock +++ b/flake.lock @@ -41,11 +41,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1794042290, - "narHash": "sha256-ziZn2PvCJE5JEEpqTdxXwJpPB+L3YqoCpTAMCzyaP3U=", + "lastModified": 1793750615, + "narHash": "sha256-KgigjFBOYHIMiRavePsh6zp8L0DzEl7wT51v6GfY7hA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "f4a03c008967841f2a0b3cb7c436fc6fc31671f6", + "rev": "40c7d8751ffb8d9faad086757b83d9ba7669a7b3", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index fc00932..6be06f9 100644 --- a/flake.nix +++ b/flake.nix @@ -70,7 +70,15 @@ devShells.dev = pkgs.mkShell { inputsFrom = [ config.devShells.nix ]; - packages = [ config.treefmt.build.wrapper ]; + packages = [ + config.treefmt.build.wrapper + # GitHub Actions auditing for this repo's .github/. Consumers do + # not need these, so they stay out of the shared modules. zizmor + # builds because the nixpkgs input is pinned to the last + # revision before 1.32.0. + pkgs.pinact + pkgs.zizmor + ]; }; devShells.default = config.devShells.base;