# SPDX-License-Identifier: FSL-1.1-Apache-2.0 { description = "AppRafter platform development environment"; inputs = { # A RELEASE branch, not nixos-unstable. Unstable already carries # kubernetes-helm 4.x and nixpkgs has no `kubernetes-helm_3` fallback, so on # unstable a routine `nix flake update` would swap the dev shell's helm # major — a toolchain change arriving as a side effect of a lockfile # refresh, with nothing naming it. The release branch still moves (it is # a branch, and flake.lock pins the rev), it just does not cross majors # underneath us. # # HELM 3 HERE, HELM 4 IN CI — ON PURPOSE. Every workflow that installs # helm pins 4.3.0 (scripts/upstream-pins.json `tool-helm`), while this # shell and the dev container that mirrors it (`devcontainer-helm`) run # this branch's helm 3, 3.20. `apprafter` runs whatever helm the user has # installed, users have either major, and the two apply a release # differently: helm 4 installs server-side, which is why re-running # cluster-bootstrap after a loader change conflicts with Argo CD there # (WI-369). The CLI has to work under both, and with CI on 4 this # tool-belt is where helm 3 still gets run. Two facts bound the split: # 3.20 receives no patches (the last was 3.20.2, 2026-04-09; helm 3 went on # to 3.22), and helm 3 as a whole gets security fixes only until # 2026-11-11. Move the shell and the dev container to helm 4 together # before then: the next release branch (nixos-26.11) will carry helm 4, # and if it is not out in time, pin a helm release binary here the way # `cuePinned` pins cue. nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem ( system: let pkgs = nixpkgs.legacyPackages.${system}; # restic comes from nixpkgs (0.18.1 on nixos-26.05), unlike cue: the # CLI's local backup verbs run it against the same repositories the # in-cluster runner writes, so what matters is that both are on one # MINOR, and the runner's Dockerfile already asserts that minor at # build time (`ARG RESTIC_MINOR=`). It is read from there, not written # a second time, and the dev shell warns when a lock update moves # nixpkgs' restic to another minor. resticMinor = let prefix = "ARG RESTIC_MINOR="; lines = pkgs.lib.splitString "\n" (builtins.readFile ./cli/apprafter-backup/Dockerfile); arg = pkgs.lib.findFirst (pkgs.lib.hasPrefix prefix) null lines; in if arg == null then null else pkgs.lib.removePrefix prefix arg; resticWarning = if resticMinor == null then "flake.nix: no `ARG RESTIC_MINOR=` in cli/apprafter-backup/Dockerfile, so the dev shell's restic ${pkgs.restic.version} is checked against nothing." else if pkgs.lib.versions.majorMinor pkgs.restic.version != resticMinor then "flake.nix: nixpkgs ships restic ${pkgs.restic.version}, but the backup runner asserts ${resticMinor}.x (cli/apprafter-backup/Dockerfile RESTIC_MINOR): the CLI and the runner would work one repository with different restic minors." else null; # CUE is pinned to the SAME version every other place in this repo pins # it — the setup-cue inputs in .github/workflows, the CMP sidecar's # Dockerfile ARG, and .devcontainer/post-create.sh. # # It is taken from the upstream release rather than from nixpkgs on # purpose. `scripts/crd-check.sh` says it runs under `nix develop` so # that cue is "the flake.lock-pinned version: ONE cue version across # local and CI" — and until 2026-09 that claim was simply false. The # flake gave whatever nixpkgs happened to carry (0.16.1) while CI's # setup-cue installed 0.10.0, six minors apart, and the byte-identity # gate that sentence exists to justify was comparing output from two # different evaluators. Fetching the release binary is what makes the # claim true, and it costs one hash instead of a Go rebuild. # # Bumping cue means editing THIS version and hash together with the # workflow inputs, the Dockerfile ARG and the devcontainer script. The # hash is the sha256 of the release tarball: # nix-prefetch-url --type sha256 \ # https://github.com/cue-lang/cue/releases/download/vX.Y.Z/cue_vX.Y.Z_linux_amd64.tar.gz # Written WITH the leading "v" so it is byte-identical to the string # every other cue pin in this repo carries — the version watcher # compares the captured strings literally and reports a mismatch as # drift, which is the right behaviour and which a bare "0.17.1" here # would trip on every run. cueVersion = "v0.17.1"; cuePinned = pkgs.stdenv.mkDerivation { pname = "cue"; version = pkgs.lib.removePrefix "v" cueVersion; src = pkgs.fetchurl { url = "https://github.com/cue-lang/cue/releases/download/${cueVersion}/cue_${cueVersion}_${ if pkgs.stdenv.hostPlatform.isDarwin then "darwin" else "linux" }_${if pkgs.stdenv.hostPlatform.isAarch64 then "arm64" else "amd64"}.tar.gz"; sha256 = { "x86_64-linux" = "sha256-o5sMl2lQadldJ22Zvg9dursIHYAb/cm6Sbdu+vlOI2k="; } .${system} or (throw "flake.nix: no cue ${cueVersion} hash recorded for ${system} — add one beside the x86_64-linux entry"); }; sourceRoot = "."; # A single static binary; there is nothing to build or patch. dontBuild = true; dontConfigure = true; installPhase = "install -Dm755 cue $out/bin/cue"; }; in { devShells.default = pkgs.lib.warnIf (resticWarning != null) resticWarning (pkgs.mkShell { name = "apprafter"; packages = with pkgs; [ # Configuration language — the version pinned above, NOT nixpkgs'. # See the `cuePinned` comment for why. cuePinned # Argo CD ships custom resource health as Lua in `argocd-cm`, and a # broken script fails SILENTLY — Argo logs it and falls back. This # is what `scripts/check-argocd-health-lua.sh` runs them under. lua # Rust toolchain cargo rustc rustfmt clippy rust-analyzer # Dependency health, which the version watcher cannot see: an # abandoned crate sits on its own final release forever and so is # never "behind". `scripts/cargo-deny.sh` falls back to # `nix run nixpkgs#cargo-deny` without this, but having it in the # shell keeps the dev-loop run fast. cargo-deny # JavaScript / TypeScript runtime (for Backstage tooling) bun # Kubernetes tooling kubectl k9s kubernetes-helm k3d kind # local e2e cluster on podman (rootless) — k3d needs docker argocd cilium-cli # Talos / bare-metal talosctl # Container build / supply-chain cosign syft trivy grype # Repo tooling just lefthook age sops jq git # Backups: `apprafter backup create/list/show/check/prune`, # `restore` and `export` run restic locally. Held to the runner's # minor by `resticWarning` above. restic # Documentation site. ONE python env — `nix shell # nixpkgs#python3Packages.mkdocs-material` ships no `mkdocs` # binary, and adding `python3Packages.mkdocs` alongside it # yields a second env whose site-packages lacks the theme # ("Unrecognised theme name: 'material'"). literate-nav # renders the generated CLI reference nav (docsgen SUMMARY.md); # redirects is installed for the W5 IA move. (python3.withPackages (ps: [ ps.mkdocs-material ps.mkdocs-literate-nav ps.mkdocs-redirects ])) ]; # mkdocs-material 9.7.6 prints a red MkDocs-2.0 advocacy banner on # EVERY mkdocs invocation (material/templates/__init__.py gates it # on this variable). It is upstream's opinion of a framework # release this site does not run — the line readers react to, # "Currently unlicensed - unsuitable for production use", is about # MkDocs 2.0 and not about the theme here. # # Silenced in the devShell rather than in the Justfile because # every call site goes through `nix develop`: `just docs-serve`, # `just docs-build`, `scripts/docs-check.sh` and release-docs.yml. # Three prefixed recipes would leave the CI logs noisy and would # miss any future `nix develop --command mkdocs ...` typed by hand. # # It also shrinks a real hazard: scripts/docs-check.sh tees mkdocs # stderr into a file it then pattern-matches, so third-party output # in that stream is one grep collision away from a false failure. NO_MKDOCS_2_WARNING = "1"; shellHook = '' echo "AppRafter dev shell ready." echo echo "Useful commands:" echo " just --list # available targets" echo " just bootstrap # install git hooks" echo " just lint # CUE + SPDX + docs + conditional Rust/TS" echo " just e2e-up # local k3d cluster" echo ''; }); # Exposed so `scripts/cue` can reach the pinned binary WITHOUT # `nix develop`, whose shellHook prints a banner onto stdout and would # corrupt every caller that captures cue's output. packages.cue = cuePinned; formatter = pkgs.nixfmt; } ); }