mirror of
https://github.com/moons-14/dotfiles.git
synced 2026-10-06 01:58:12 +09:00
flake: add coding-agent validation workflow
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
[mcp_servers.nixos]
|
||||||
|
command = "mcp-nixos"
|
||||||
|
startup_timeout_sec = 30
|
||||||
|
tool_timeout_sec = 60
|
||||||
|
required = false
|
||||||
|
|
||||||
|
[mcp_servers.github]
|
||||||
|
url = "https://api.githubcopilot.com/mcp/"
|
||||||
|
bearer_token_env_var = "GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||||
|
http_headers = { X-MCP-Readonly = "true", X-MCP-Toolsets = "repos,pull_requests,actions" }
|
||||||
|
startup_timeout_sec = 30
|
||||||
|
tool_timeout_sec = 60
|
||||||
|
required = false
|
||||||
@@ -1,27 +1,15 @@
|
|||||||
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
|
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
|
||||||
name: Check NixOS configurations
|
name: Build Linux checks
|
||||||
description: Build every NixOS configuration and the Registry tests
|
description: Build every x86_64-linux flake check in parallel
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Build every NixOS configuration
|
- name: Build all Linux checks
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
nix run .#nix-fast-build -- \
|
||||||
mapfile -t hosts < <(
|
--flake .#checks.x86_64-linux \
|
||||||
nix eval --raw .#nixosConfigurations \
|
--skip-cached \
|
||||||
--apply 'configs: builtins.concatStringsSep "\n" (builtins.attrNames configs)'
|
--no-nom \
|
||||||
)
|
--no-link
|
||||||
|
|
||||||
installables=(.#checks.x86_64-linux.registry)
|
|
||||||
for host in "${hosts[@]}"; do
|
|
||||||
installables+=(".#nixosConfigurations.${host}.config.system.build.toplevel")
|
|
||||||
done
|
|
||||||
|
|
||||||
nix build \
|
|
||||||
--keep-going \
|
|
||||||
--no-link \
|
|
||||||
--print-build-logs \
|
|
||||||
--show-trace \
|
|
||||||
"${installables[@]}"
|
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ runs:
|
|||||||
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock') }}
|
primary-key: nix-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('flake.lock') }}
|
||||||
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}-
|
restore-prefixes-first-match: nix-${{ runner.os }}-${{ runner.arch }}-
|
||||||
gc-max-store-size-linux: 4G
|
gc-max-store-size-linux: 4G
|
||||||
|
gc-max-store-size-macos: 4G
|
||||||
|
|||||||
+159
-10
@@ -1,36 +1,50 @@
|
|||||||
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
|
# Reference: https://github.com/ryoppippi/dotfiles/blob/main/.github/workflows/nix-build.yaml
|
||||||
name: "CI: NixOS"
|
name: "CI: Nix"
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
|
- .gitignore
|
||||||
|
- AGENTS.md
|
||||||
|
- README.md
|
||||||
|
- "docs/**"
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- flake.lock
|
- flake.lock
|
||||||
|
- ".codex/**"
|
||||||
|
- ".github/**"
|
||||||
|
- ".vscode/**"
|
||||||
- "flake/**"
|
- "flake/**"
|
||||||
- "hosts/**"
|
- "hosts/**"
|
||||||
- "libs/**"
|
- "libs/**"
|
||||||
- "modules/**"
|
- "modules/**"
|
||||||
|
- "opencode.json"
|
||||||
- "overlays/**"
|
- "overlays/**"
|
||||||
|
- "scripts/**"
|
||||||
- "shells/**"
|
- "shells/**"
|
||||||
|
- "skills/**"
|
||||||
- "tests/**"
|
- "tests/**"
|
||||||
- ".github/actions/check-nixos/**"
|
|
||||||
- ".github/actions/setup-nix/**"
|
|
||||||
- ".github/workflows/nixos.yaml"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
|
- .gitignore
|
||||||
|
- AGENTS.md
|
||||||
|
- README.md
|
||||||
|
- "docs/**"
|
||||||
- flake.nix
|
- flake.nix
|
||||||
- flake.lock
|
- flake.lock
|
||||||
|
- ".codex/**"
|
||||||
|
- ".github/**"
|
||||||
|
- ".vscode/**"
|
||||||
- "flake/**"
|
- "flake/**"
|
||||||
- "hosts/**"
|
- "hosts/**"
|
||||||
- "libs/**"
|
- "libs/**"
|
||||||
- "modules/**"
|
- "modules/**"
|
||||||
|
- "opencode.json"
|
||||||
- "overlays/**"
|
- "overlays/**"
|
||||||
|
- "scripts/**"
|
||||||
- "shells/**"
|
- "shells/**"
|
||||||
|
- "skills/**"
|
||||||
- "tests/**"
|
- "tests/**"
|
||||||
- ".github/actions/check-nixos/**"
|
|
||||||
- ".github/actions/setup-nix/**"
|
|
||||||
- ".github/workflows/nixos.yaml"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -38,14 +52,149 @@ concurrency:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
validate:
|
||||||
name: Check all NixOS configurations
|
name: Plan, lint, and evaluate
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
|
outputs:
|
||||||
|
build_linux: ${{ steps.plan.outputs.build_linux }}
|
||||||
|
build_darwin: ${{ steps.plan.outputs.build_darwin }}
|
||||||
|
nix_validation: ${{ steps.plan.outputs.nix_validation }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
- name: Check NixOS configurations
|
- name: Plan validation
|
||||||
|
id: plan
|
||||||
|
env:
|
||||||
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
PUSH_BASE_SHA: ${{ github.event.before }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
case "${{ github.event_name }}" in
|
||||||
|
pull_request)
|
||||||
|
plan="$(nix run .#check -- plan --base "$PR_BASE_SHA" --json)"
|
||||||
|
;;
|
||||||
|
push)
|
||||||
|
plan="$(nix run .#check -- plan --base "$PUSH_BASE_SHA" --json)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
plan="$(nix run .#check -- plan --all-files --all-hosts --json)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf '%s\n' "$plan"
|
||||||
|
nix_validation="$(jq -r '.requiresNixValidation' <<<"$plan")"
|
||||||
|
|
||||||
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
|
build_linux="$(jq -r '.nativeBuildSystems["x86_64-linux"] // false' <<<"$plan")"
|
||||||
|
build_darwin="$(jq -r '.nativeBuildSystems["aarch64-darwin"] // false' <<<"$plan")"
|
||||||
|
else
|
||||||
|
build_linux="$nix_validation"
|
||||||
|
build_darwin="$nix_validation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "nix_validation=$nix_validation"
|
||||||
|
echo "build_linux=$build_linux"
|
||||||
|
echo "build_darwin=$build_darwin"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Run fast checks
|
||||||
|
env:
|
||||||
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
PUSH_BASE_SHA: ${{ github.event.before }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
case "${{ github.event_name }}" in
|
||||||
|
pull_request)
|
||||||
|
nix run .#check -- fast --base "$PR_BASE_SHA"
|
||||||
|
;;
|
||||||
|
push)
|
||||||
|
nix run .#check -- fast --base "$PUSH_BASE_SHA"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
nix run .#check -- fast --all-files
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
- name: Evaluate configurations
|
||||||
|
if: steps.plan.outputs.nix_validation == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
|
env:
|
||||||
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
PUSH_BASE_SHA: ${{ github.event.before }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
case "${{ github.event_name }}" in
|
||||||
|
pull_request)
|
||||||
|
nix run .#check -- eval --base "$PR_BASE_SHA"
|
||||||
|
;;
|
||||||
|
push)
|
||||||
|
nix run .#check -- eval --base "$PUSH_BASE_SHA" --all-systems
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
nix run .#check -- eval --all-hosts --all-systems
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
name: Build Linux checks
|
||||||
|
needs: validate
|
||||||
|
if: needs.validate.outputs.build_linux == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 180
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Nix
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
- name: Build affected Linux checks
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
nix run .#check -- build --base "$PR_BASE_SHA"
|
||||||
|
- name: Build all Linux checks
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
uses: ./.github/actions/check-nixos
|
uses: ./.github/actions/check-nixos
|
||||||
|
|
||||||
|
build-darwin:
|
||||||
|
name: Build Darwin checks
|
||||||
|
needs: validate
|
||||||
|
if: needs.validate.outputs.build_darwin == 'true'
|
||||||
|
runs-on: macos-15
|
||||||
|
timeout-minutes: 180
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Nix
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
- name: Build affected Darwin checks
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
env:
|
||||||
|
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
nix run .#check -- build --base "$PR_BASE_SHA"
|
||||||
|
- name: Build all Darwin checks
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
nix run .#nix-fast-build -- \
|
||||||
|
--flake .#checks.aarch64-darwin \
|
||||||
|
--skip-cached \
|
||||||
|
--no-nom \
|
||||||
|
--no-link
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ permissions:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
jobs:
|
jobs:
|
||||||
update:
|
update:
|
||||||
name: Update and validate flake inputs
|
name: Update and check Linux
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 120
|
timeout-minutes: 180
|
||||||
|
outputs:
|
||||||
|
changed: ${{ steps.update.outputs.changed }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
@@ -22,18 +24,78 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
- name: Update flake inputs
|
- name: Update flake inputs
|
||||||
id: update
|
id: update
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
nix flake update
|
nix flake update
|
||||||
if git diff --quiet -- flake.lock; then
|
if git diff --quiet -- flake.lock; then
|
||||||
echo 'changed=false' >> "$GITHUB_OUTPUT"
|
echo 'changed=false' >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo 'changed=true' >> "$GITHUB_OUTPUT"
|
echo 'changed=true' >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
- name: Check updated NixOS configurations
|
- name: Evaluate every flake system
|
||||||
|
if: steps.update.outputs.changed == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
nix flake check \
|
||||||
|
--no-build \
|
||||||
|
--all-systems \
|
||||||
|
--keep-going \
|
||||||
|
--show-trace
|
||||||
|
- name: Build Linux checks
|
||||||
if: steps.update.outputs.changed == 'true'
|
if: steps.update.outputs.changed == 'true'
|
||||||
uses: ./.github/actions/check-nixos
|
uses: ./.github/actions/check-nixos
|
||||||
- name: Create update pull request
|
- name: Upload updated lock file
|
||||||
if: steps.update.outputs.changed == 'true'
|
if: steps.update.outputs.changed == 'true'
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
with:
|
||||||
|
name: flake-lock
|
||||||
|
path: flake.lock
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
check-darwin:
|
||||||
|
name: Check Darwin
|
||||||
|
needs: update
|
||||||
|
if: needs.update.outputs.changed == 'true'
|
||||||
|
runs-on: macos-15
|
||||||
|
timeout-minutes: 180
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
- name: Download updated lock file
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
|
with:
|
||||||
|
name: flake-lock
|
||||||
|
path: .
|
||||||
|
- name: Setup Nix
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
- name: Build Darwin checks
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
nix run .#nix-fast-build -- \
|
||||||
|
--flake .#checks.aarch64-darwin \
|
||||||
|
--skip-cached \
|
||||||
|
--no-nom \
|
||||||
|
--no-link
|
||||||
|
|
||||||
|
pull-request:
|
||||||
|
name: Create update pull request
|
||||||
|
needs:
|
||||||
|
- update
|
||||||
|
- check-darwin
|
||||||
|
if: needs.update.outputs.changed == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
- name: Download validated lock file
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||||
|
with:
|
||||||
|
name: flake-lock
|
||||||
|
path: .
|
||||||
|
- name: Create update pull request
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -45,4 +107,5 @@ jobs:
|
|||||||
body: |
|
body: |
|
||||||
Automated update of `flake.lock`.
|
Automated update of `flake.lock`.
|
||||||
|
|
||||||
The updated inputs passed the Registry tests and a build of every NixOS configuration.
|
The updated inputs passed all-system evaluation and native builds of
|
||||||
|
the Linux and Darwin check sets.
|
||||||
|
|||||||
+7
-1
@@ -1,4 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
|
**/__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!README.md
|
!README.md
|
||||||
@@ -6,8 +8,10 @@
|
|||||||
!AGENTS.md
|
!AGENTS.md
|
||||||
!/docs/
|
!/docs/
|
||||||
|
|
||||||
!.github/
|
!/.codex/
|
||||||
|
!/.github/
|
||||||
!.gitea/
|
!.gitea/
|
||||||
|
!/.vscode/
|
||||||
|
|
||||||
!.envrc
|
!.envrc
|
||||||
|
|
||||||
@@ -15,7 +19,9 @@
|
|||||||
|
|
||||||
!/flake.nix
|
!/flake.nix
|
||||||
!/flake.lock
|
!/flake.lock
|
||||||
|
!/opencode.json
|
||||||
|
|
||||||
|
!/scripts/
|
||||||
!/shells/
|
!/shells/
|
||||||
!/flake/
|
!/flake/
|
||||||
!/overlays/
|
!/overlays/
|
||||||
|
|||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"nix.enableLanguageServer": true,
|
||||||
|
"nix.serverPath": "nixd",
|
||||||
|
"nix.serverSettings": {
|
||||||
|
"nixd": {
|
||||||
|
"formatting": {
|
||||||
|
"command": ["nixfmt"]
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"expr": "import (builtins.getFlake (builtins.toString ./.)).inputs.nixpkgs { }"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"nixos": {
|
||||||
|
"expr": "(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.galleria.options"
|
||||||
|
},
|
||||||
|
"home-manager-nixos": {
|
||||||
|
"expr": "(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.galleria.options.home-manager.users.type.getSubOptions []"
|
||||||
|
},
|
||||||
|
"darwin": {
|
||||||
|
"expr": "(builtins.getFlake (builtins.toString ./.)).darwinConfigurations.m2.options"
|
||||||
|
},
|
||||||
|
"home-manager-darwin": {
|
||||||
|
"expr": "(builtins.getFlake (builtins.toString ./.)).darwinConfigurations.m2.options.home-manager.users.type.getSubOptions []"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
# moons14 dotfiles
|
# moons14 dotfiles
|
||||||
|
|
||||||
My NixOS + Home Manager configurations build with flake.
|
My NixOS, nix-darwin, and Home Manager configurations built with flakes.
|
||||||
|
|
||||||
|
See [Coding-agent validation](docs/coding-agents.md) for the non-activating,
|
||||||
|
change-aware validation workflow used by Codex, OpenCode, and CI.
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Coding-agent workflow
|
||||||
|
|
||||||
|
This repository exposes deterministic validation and narrowly scoped research
|
||||||
|
tools for Codex, OpenCode, and editor agents. Live system activation is outside
|
||||||
|
this workflow.
|
||||||
|
|
||||||
|
## Validation commands
|
||||||
|
|
||||||
|
Use task-owned paths during the edit loop:
|
||||||
|
|
||||||
|
```console
|
||||||
|
nix run .#check -- plan --paths modules/applications/example/home.nix --json
|
||||||
|
nix run .#check -- fast --paths modules/applications/example/home.nix
|
||||||
|
nix run .#check -- eval --paths modules/applications/example/home.nix
|
||||||
|
nix run .#check -- build --paths modules/applications/example/home.nix
|
||||||
|
nix run .#check -- all --paths modules/applications/example/home.nix
|
||||||
|
```
|
||||||
|
|
||||||
|
For a committed pull-request range, replace `--paths ...` with
|
||||||
|
`--base <base-sha>`. `full` is reserved for CI, scheduled maintenance, or an
|
||||||
|
explicit repository-wide audit:
|
||||||
|
|
||||||
|
```console
|
||||||
|
nix run .#check -- full
|
||||||
|
```
|
||||||
|
|
||||||
|
The stages have distinct meanings:
|
||||||
|
|
||||||
|
- `plan` maps changed paths to Registry units, reverse `meta.includes`
|
||||||
|
dependencies, real hosts, and compatible build systems.
|
||||||
|
- `fast` parses changed Nix files, validates JSON, TOML, Python, and Agent Skill
|
||||||
|
frontmatter, checks whitespace, and runs configured hooks only for the
|
||||||
|
selected files.
|
||||||
|
- `eval` instantiates affected NixOS and nix-darwin configurations. Flake-wide,
|
||||||
|
validation-tool, shell, overlay, and test changes additionally evaluate every
|
||||||
|
flake system.
|
||||||
|
- `build` realizes affected configurations supported by the current platform
|
||||||
|
with no result link. Incompatible targets remain evaluation-only until a
|
||||||
|
matching runner handles them.
|
||||||
|
- `all` performs task-scoped file checks, all-system evaluation, and compatible
|
||||||
|
targeted builds.
|
||||||
|
- `full` runs all-file hooks, all-system evaluation, and every check for the
|
||||||
|
current platform through `nix-fast-build`.
|
||||||
|
|
||||||
|
The validation app constructs a filtered temporary `path:` flake from the
|
||||||
|
committed `HEAD` tree and overlays only task-owned changed paths. This isolates
|
||||||
|
unrelated worktree changes, makes new untracked Registry fragments visible to
|
||||||
|
Nix without staging them, and avoids copying ignored state such as `.direnv`.
|
||||||
|
|
||||||
|
None of these commands runs `nh os switch`, `nixos-rebuild switch`,
|
||||||
|
`darwin-rebuild switch`, `home-manager switch`, or another activation command.
|
||||||
|
The user performs activation separately.
|
||||||
|
|
||||||
|
## Agent Skills
|
||||||
|
|
||||||
|
Read `AGENTS.md` first. Use the repository skills as follows:
|
||||||
|
|
||||||
|
- `validate-nix-change` controls validation scope and evidence.
|
||||||
|
- `debug-nix-failure` classifies parse, evaluation, build, test, activation-log,
|
||||||
|
and runtime failures before proposing a correction.
|
||||||
|
- `test-nixos-service` adds a `pkgs.testers.runNixOSTest` check when a build
|
||||||
|
cannot prove service behavior.
|
||||||
|
- `update-flake-input` limits lock-file updates and validates them without
|
||||||
|
activating a host.
|
||||||
|
- `add-application-or-service` preserves Registry ownership and delegates
|
||||||
|
validation to `validate-nix-change`.
|
||||||
|
|
||||||
|
## MCP and language-server setup
|
||||||
|
|
||||||
|
The development workload installs `mcp-nixos`, `nixd`, `nix-fast-build`, and
|
||||||
|
`nix-tree`.
|
||||||
|
|
||||||
|
Project-local Codex and OpenCode configuration exposes:
|
||||||
|
|
||||||
|
- `mcp-nixos` for current NixOS, Home Manager, nix-darwin, package, and Nix
|
||||||
|
documentation queries;
|
||||||
|
- GitHub's remote MCP endpoint for Codex and OpenCode in read-only mode,
|
||||||
|
restricted to repository, pull-request, and Actions toolsets.
|
||||||
|
|
||||||
|
Set `GITHUB_PERSONAL_ACCESS_TOKEN` in the launching environment when GitHub MCP
|
||||||
|
access is needed. Do not commit the token or put it in a Nix expression because
|
||||||
|
that would expose it through source control or the Nix store.
|
||||||
|
|
||||||
|
The workspace VS Code settings use `nixd` and expose option sets for the
|
||||||
|
`galleria` NixOS configuration, its integrated Home Manager configuration, the
|
||||||
|
`m2` nix-darwin configuration, and its integrated Home Manager configuration.
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
./formatter.nix
|
./formatter.nix
|
||||||
./git-hooks.nix
|
./git-hooks.nix
|
||||||
./registry.nix
|
./registry.nix
|
||||||
|
./validation.nix
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
actionlint.enable = true;
|
||||||
deadnix.enable = true;
|
deadnix.enable = true;
|
||||||
|
ruff.enable = true;
|
||||||
statix.enable = true;
|
statix.enable = true;
|
||||||
shellcheck.enable = true;
|
shellcheck.enable = true;
|
||||||
};
|
};
|
||||||
|
|||||||
+47
-6
@@ -25,23 +25,63 @@ let
|
|||||||
else
|
else
|
||||||
{ };
|
{ };
|
||||||
configurations = dotfilesLib.hosts.mkConfigurations hostSpecs;
|
configurations = dotfilesLib.hosts.mkConfigurations hostSpecs;
|
||||||
|
|
||||||
|
validationMetadata = {
|
||||||
|
schemaVersion = 1;
|
||||||
|
|
||||||
|
hosts = lib.mapAttrs (
|
||||||
|
name: spec:
|
||||||
|
let
|
||||||
|
isDarwin = lib.hasSuffix "-darwin" spec.system;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit (spec) system user;
|
||||||
|
kind = if isDarwin then "darwin" else "nixos";
|
||||||
|
homeManager = spec.homeManager or true;
|
||||||
|
selectedUnits = dotfilesLib.hosts.selectedUnits spec;
|
||||||
|
buildAttr =
|
||||||
|
if isDarwin then
|
||||||
|
"darwinConfigurations.${name}.system"
|
||||||
|
else
|
||||||
|
"nixosConfigurations.${name}.config.system.build.toplevel";
|
||||||
|
}
|
||||||
|
) hostSpecs;
|
||||||
|
|
||||||
|
units = lib.mapAttrs (
|
||||||
|
_id: unit: {
|
||||||
|
inherit (unit) id relativePath;
|
||||||
|
directory = "modules/${lib.concatStringsSep "/" unit.relativePath}";
|
||||||
|
includes = unit.meta.includes;
|
||||||
|
fragments = builtins.attrNames (lib.filterAttrs (_: value: value != null) unit.fragments);
|
||||||
|
}
|
||||||
|
) dotfilesLib.registry.units;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
flake = {
|
flake = {
|
||||||
inherit (configurations) darwinConfigurations nixosConfigurations;
|
inherit (configurations) darwinConfigurations nixosConfigurations;
|
||||||
lib = dotfilesLib;
|
lib = dotfilesLib // { inherit validationMetadata; };
|
||||||
};
|
};
|
||||||
|
|
||||||
perSystem =
|
perSystem =
|
||||||
{ pkgs, system, ... }:
|
{ pkgs, system, ... }:
|
||||||
let
|
let
|
||||||
nixosChecks =
|
nixosChecks = lib.mapAttrs' (
|
||||||
lib.mapAttrs' (name: nixos: lib.nameValuePair "nixos-${name}" nixos.config.system.build.toplevel)
|
name: nixos:
|
||||||
(
|
lib.nameValuePair "nixos-${name}" nixos.config.system.build.toplevel
|
||||||
|
) (
|
||||||
lib.filterAttrs (
|
lib.filterAttrs (
|
||||||
_: nixos: nixos.pkgs.stdenv.hostPlatform.system == system
|
name: _: hostSpecs.${name}.system == system
|
||||||
) configurations.nixosConfigurations
|
) configurations.nixosConfigurations
|
||||||
);
|
);
|
||||||
|
darwinChecks = lib.mapAttrs' (
|
||||||
|
name: darwin:
|
||||||
|
lib.nameValuePair "darwin-${name}" darwin.system
|
||||||
|
) (
|
||||||
|
lib.filterAttrs (
|
||||||
|
name: _: hostSpecs.${name}.system == system
|
||||||
|
) configurations.darwinConfigurations
|
||||||
|
);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
checks = {
|
checks = {
|
||||||
@@ -49,6 +89,7 @@ in
|
|||||||
inherit inputs lib pkgs;
|
inherit inputs lib pkgs;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// nixosChecks;
|
// nixosChecks
|
||||||
|
// darwinChecks;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
_: {
|
||||||
|
perSystem =
|
||||||
|
{ pkgs, config, ... }:
|
||||||
|
let
|
||||||
|
script = pkgs.writeText "dotfiles-check.py" (builtins.readFile ../scripts/dotfiles-check.py);
|
||||||
|
dotfilesCheck = pkgs.writeShellApplication {
|
||||||
|
name = "dotfiles-check";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.git
|
||||||
|
pkgs.nix
|
||||||
|
pkgs.python3
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
exec python3 ${script} "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
apps = {
|
||||||
|
check = {
|
||||||
|
type = "app";
|
||||||
|
program = "${dotfilesCheck}/bin/dotfiles-check";
|
||||||
|
};
|
||||||
|
nix-fast-build = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pkgs.nix-fast-build}/bin/nix-fast-build";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = {
|
||||||
|
dotfiles-check = dotfilesCheck;
|
||||||
|
inherit (pkgs) nix-fast-build;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.validation = config.pre-commit.devShell;
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
validation-tool = pkgs.runCommand "dotfiles-check-self-test" {
|
||||||
|
nativeBuildInputs = [ dotfilesCheck ];
|
||||||
|
} ''
|
||||||
|
dotfiles-check self-test
|
||||||
|
touch "$out"
|
||||||
|
'';
|
||||||
|
|
||||||
|
dotnix-shell = config.devShells.dotnix;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -118,7 +118,7 @@ let
|
|||||||
];
|
];
|
||||||
userSettings = {
|
userSettings = {
|
||||||
"nix.enableLanguageServer" = true;
|
"nix.enableLanguageServer" = true;
|
||||||
"nix.serverPath" = "nil";
|
"nix.serverPath" = "nixd";
|
||||||
"nix.serverSettings" = {
|
"nix.serverSettings" = {
|
||||||
nixd = {
|
nixd = {
|
||||||
formatting.command = [ "nixfmt" ];
|
formatting.command = [ "nixfmt" ];
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
home.packages = with pkgs; [
|
home.packages = with pkgs; [
|
||||||
bind
|
bind
|
||||||
bun
|
bun
|
||||||
nil
|
mcp-nixos
|
||||||
|
nix-fast-build
|
||||||
|
nix-tree
|
||||||
|
nixd
|
||||||
python312
|
python312
|
||||||
uv
|
uv
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"nixos": {
|
||||||
|
"type": "local",
|
||||||
|
"command": ["mcp-nixos"],
|
||||||
|
"enabled": true,
|
||||||
|
"timeout": 30000
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://api.githubcopilot.com/mcp/",
|
||||||
|
"enabled": true,
|
||||||
|
"oauth": false,
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer {env:GITHUB_PERSONAL_ACCESS_TOKEN}",
|
||||||
|
"X-MCP-Readonly": "true",
|
||||||
|
"X-MCP-Toolsets": "repos,pull_requests,actions"
|
||||||
|
},
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+526
@@ -0,0 +1,526 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Change-aware, non-activating validation for this Nix dotfiles repository."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from contextlib import contextmanager
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import tomllib
|
||||||
|
from typing import Any, Iterator, Sequence
|
||||||
|
|
||||||
|
GLOBAL_FILES = {"flake.nix", "flake.lock", "hosts/default.nix"}
|
||||||
|
GLOBAL_PREFIXES = ("flake/", "libs/", "overlays/")
|
||||||
|
FULL_EVAL_PREFIXES = GLOBAL_PREFIXES + ("scripts/", "shells/", "tests/")
|
||||||
|
FULL_BUILD_PREFIXES = GLOBAL_PREFIXES + ("scripts/", "shells/", "tests/")
|
||||||
|
DOC_SUFFIXES = (".md", ".png", ".jpg", ".jpeg", ".webp")
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def command_text(command: Sequence[str]) -> str:
|
||||||
|
return " ".join(json.dumps(part) if any(c.isspace() for c in part) else part for part in command)
|
||||||
|
|
||||||
|
|
||||||
|
def run(
|
||||||
|
command: Sequence[str],
|
||||||
|
*,
|
||||||
|
cwd: Path,
|
||||||
|
capture: bool = False,
|
||||||
|
check: bool = True,
|
||||||
|
input_text: str | None = None,
|
||||||
|
) -> subprocess.CompletedProcess[str]:
|
||||||
|
print(f"+ {command_text(command)}", file=sys.stderr)
|
||||||
|
return subprocess.run(
|
||||||
|
list(command),
|
||||||
|
cwd=cwd,
|
||||||
|
check=check,
|
||||||
|
text=True,
|
||||||
|
input=input_text,
|
||||||
|
stdout=subprocess.PIPE if capture else None,
|
||||||
|
stderr=subprocess.PIPE if capture else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def git(root: Path, *args: str, check: bool = True) -> str:
|
||||||
|
return run(["git", *args], cwd=root, capture=True, check=check).stdout
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(run(["git", "rev-parse", "--show-toplevel"], cwd=Path.cwd(), capture=True).stdout.strip()).resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(root: Path, raw: str) -> str:
|
||||||
|
candidate = Path(raw)
|
||||||
|
absolute = candidate.resolve() if candidate.is_absolute() else (root / candidate).resolve()
|
||||||
|
try:
|
||||||
|
return absolute.relative_to(root).as_posix()
|
||||||
|
except ValueError as error:
|
||||||
|
raise ValidationError(f"path escapes repository root: {raw}") from error
|
||||||
|
|
||||||
|
|
||||||
|
def nonempty_lines(value: str) -> list[str]:
|
||||||
|
return [line for line in value.splitlines() if line]
|
||||||
|
|
||||||
|
|
||||||
|
def tracked_paths(root: Path) -> set[str]:
|
||||||
|
return set(nonempty_lines(git(root, "ls-files")))
|
||||||
|
|
||||||
|
|
||||||
|
def untracked_paths(root: Path) -> set[str]:
|
||||||
|
return set(nonempty_lines(git(root, "ls-files", "--others", "--exclude-standard")))
|
||||||
|
|
||||||
|
|
||||||
|
def expand_paths(root: Path, raw_paths: Sequence[str], available: set[str]) -> list[str]:
|
||||||
|
result: set[str] = set()
|
||||||
|
for raw in raw_paths:
|
||||||
|
relative = normalize(root, raw)
|
||||||
|
target = root / relative
|
||||||
|
if target.is_dir():
|
||||||
|
prefix = f"{relative.rstrip('/')}/" if relative else ""
|
||||||
|
result.update(path for path in available if path.startswith(prefix))
|
||||||
|
else:
|
||||||
|
result.add(relative)
|
||||||
|
return sorted(result)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_paths(root: Path, args: argparse.Namespace) -> tuple[list[str], set[str]]:
|
||||||
|
tracked = tracked_paths(root)
|
||||||
|
untracked = untracked_paths(root)
|
||||||
|
if args.paths:
|
||||||
|
paths = expand_paths(root, args.paths, tracked | untracked)
|
||||||
|
elif args.all_files:
|
||||||
|
paths = sorted(tracked)
|
||||||
|
elif args.base:
|
||||||
|
paths = nonempty_lines(
|
||||||
|
git(root, "diff", "--name-only", "--no-renames", "--diff-filter=ACMRTUXBD", f"{args.base}...HEAD", "--")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
paths = nonempty_lines(git(root, "diff", "--name-only", "--no-renames", "--diff-filter=ACMRTUXBD", "HEAD", "--"))
|
||||||
|
paths += sorted(untracked)
|
||||||
|
return sorted(set(paths)), untracked
|
||||||
|
|
||||||
|
|
||||||
|
def remove_path(path: Path) -> None:
|
||||||
|
if path.is_symlink() or path.is_file():
|
||||||
|
path.unlink()
|
||||||
|
elif path.is_dir():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def flake_reference(root: Path, changed: Sequence[str]) -> Iterator[str]:
|
||||||
|
"""Create a clean HEAD snapshot and overlay only task-owned worktree paths."""
|
||||||
|
with tempfile.TemporaryDirectory(prefix="dotfiles-flake-") as temporary:
|
||||||
|
source = Path(temporary) / "source"
|
||||||
|
source.mkdir()
|
||||||
|
archive = subprocess.run(
|
||||||
|
["git", "archive", "--format=tar", "HEAD"],
|
||||||
|
cwd=root,
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
).stdout
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as tar:
|
||||||
|
tar.extractall(source, filter="data")
|
||||||
|
for relative in changed:
|
||||||
|
src, dst = root / relative, source / relative
|
||||||
|
remove_path(dst)
|
||||||
|
if src.is_symlink():
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
dst.symlink_to(os.readlink(src))
|
||||||
|
elif src.is_file():
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
yield f"path:{source}"
|
||||||
|
|
||||||
|
|
||||||
|
def load_metadata(root: Path, reference: str) -> dict[str, Any]:
|
||||||
|
result = run(
|
||||||
|
["nix", "eval", "--json", "--show-trace", f"{reference}#lib.validationMetadata"],
|
||||||
|
cwd=root,
|
||||||
|
capture=True,
|
||||||
|
)
|
||||||
|
metadata = json.loads(result.stdout)
|
||||||
|
if metadata.get("schemaVersion") != 1:
|
||||||
|
raise ValidationError("unsupported validation metadata schema")
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_dependencies(units: dict[str, Any]) -> dict[str, set[str]]:
|
||||||
|
result: dict[str, set[str]] = defaultdict(set)
|
||||||
|
for unit_id, unit in units.items():
|
||||||
|
for dependency in unit.get("includes", []):
|
||||||
|
result[dependency].add(unit_id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def users_of(unit_id: str, reverse: dict[str, set[str]]) -> set[str]:
|
||||||
|
result, queue = {unit_id}, deque([unit_id])
|
||||||
|
while queue:
|
||||||
|
for dependent in reverse.get(queue.popleft(), set()):
|
||||||
|
if dependent not in result:
|
||||||
|
result.add(dependent)
|
||||||
|
queue.append(dependent)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def owner_for(path: str, units: dict[str, Any]) -> str | None:
|
||||||
|
matches = []
|
||||||
|
for unit_id, unit in units.items():
|
||||||
|
directory = unit["directory"].rstrip("/")
|
||||||
|
if path == directory or path.startswith(f"{directory}/"):
|
||||||
|
matches.append((len(directory), unit_id))
|
||||||
|
return max(matches)[1] if matches else None
|
||||||
|
|
||||||
|
|
||||||
|
def path_classes(path: str, unit: dict[str, Any]) -> set[str]:
|
||||||
|
directory = unit["directory"].rstrip("/")
|
||||||
|
relative = path[len(directory) :].lstrip("/")
|
||||||
|
if relative == "nixos.nix" or relative == "home/nixos.nix":
|
||||||
|
return {"nixos"}
|
||||||
|
if relative == "darwin.nix" or relative == "home/darwin.nix":
|
||||||
|
return {"darwin"}
|
||||||
|
if relative == "meta.nix":
|
||||||
|
return {"nixos", "darwin"}
|
||||||
|
if relative in {"common.nix", "home.nix", "home/common.nix"}:
|
||||||
|
return {"nixos", "darwin"}
|
||||||
|
fragments = set(unit.get("fragments", []))
|
||||||
|
classes: set[str] = set()
|
||||||
|
if fragments & {"common", "nixos", "home", "homeCommon", "homeNixos"}:
|
||||||
|
classes.add("nixos")
|
||||||
|
if fragments & {"common", "darwin", "home", "homeCommon", "homeDarwin"}:
|
||||||
|
classes.add("darwin")
|
||||||
|
return classes or {"nixos", "darwin"}
|
||||||
|
|
||||||
|
|
||||||
|
def is_docs_only(path: str) -> bool:
|
||||||
|
return path.endswith(DOC_SUFFIXES)
|
||||||
|
|
||||||
|
|
||||||
|
def plan(metadata: dict[str, Any], paths: Sequence[str], *, all_hosts: bool = False) -> dict[str, Any]:
|
||||||
|
hosts: dict[str, Any] = metadata["hosts"]
|
||||||
|
units: dict[str, Any] = metadata["units"]
|
||||||
|
reverse = reverse_dependencies(units)
|
||||||
|
affected_units: set[str] = set()
|
||||||
|
unit_classes: dict[str, set[str]] = defaultdict(set)
|
||||||
|
affected_hosts = {"nixos": set(), "darwin": set()}
|
||||||
|
global_change = all_hosts
|
||||||
|
requires_full_eval = False
|
||||||
|
requires_full_build = False
|
||||||
|
requires_nix = False
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
if path in GLOBAL_FILES or path.startswith(GLOBAL_PREFIXES):
|
||||||
|
global_change = True
|
||||||
|
requires_nix = True
|
||||||
|
if path.startswith(FULL_EVAL_PREFIXES) or path in GLOBAL_FILES:
|
||||||
|
requires_full_eval = True
|
||||||
|
if path.startswith(FULL_BUILD_PREFIXES) or path in {"flake.nix", "flake.lock"}:
|
||||||
|
requires_full_build = True
|
||||||
|
if path.endswith(".nix") or path == "flake.lock":
|
||||||
|
requires_nix = True
|
||||||
|
if path == "scripts/dotfiles-check.py":
|
||||||
|
requires_nix = requires_full_eval = requires_full_build = True
|
||||||
|
|
||||||
|
if path.startswith("hosts/") and path != "hosts/default.nix":
|
||||||
|
parts = PurePosixPath(path).parts
|
||||||
|
if len(parts) > 1 and parts[1] in hosts:
|
||||||
|
affected_hosts[hosts[parts[1]]["kind"]].add(parts[1])
|
||||||
|
requires_nix = True
|
||||||
|
|
||||||
|
owner = owner_for(path, units)
|
||||||
|
if owner:
|
||||||
|
classes = path_classes(path, units[owner])
|
||||||
|
for unit_id in users_of(owner, reverse):
|
||||||
|
affected_units.add(unit_id)
|
||||||
|
unit_classes[unit_id].update(classes)
|
||||||
|
requires_nix = requires_nix or not is_docs_only(path)
|
||||||
|
elif path.startswith("modules/") and not is_docs_only(path):
|
||||||
|
global_change = True
|
||||||
|
requires_nix = True
|
||||||
|
|
||||||
|
if global_change:
|
||||||
|
for name, host in hosts.items():
|
||||||
|
affected_hosts[host["kind"]].add(name)
|
||||||
|
else:
|
||||||
|
for name, host in hosts.items():
|
||||||
|
selected = set(host["selectedUnits"])
|
||||||
|
if any(unit_id in selected and host["kind"] in unit_classes[unit_id] for unit_id in affected_units):
|
||||||
|
affected_hosts[host["kind"]].add(name)
|
||||||
|
|
||||||
|
synthetic = {"nixos": set(), "darwin": set()}
|
||||||
|
for unit_id in affected_units:
|
||||||
|
for kind in unit_classes[unit_id]:
|
||||||
|
if not any(unit_id in hosts[name]["selectedUnits"] for name in affected_hosts[kind]):
|
||||||
|
synthetic[kind].add(unit_id)
|
||||||
|
|
||||||
|
systems = sorted({host["system"] for host in hosts.values()})
|
||||||
|
native_systems = {system: False for system in systems}
|
||||||
|
if requires_full_build:
|
||||||
|
native_systems = {system: requires_nix for system in systems}
|
||||||
|
else:
|
||||||
|
for kind, names in affected_hosts.items():
|
||||||
|
for name in names:
|
||||||
|
native_systems[hosts[name]["system"]] = True
|
||||||
|
for kind, unit_ids in synthetic.items():
|
||||||
|
if unit_ids:
|
||||||
|
candidates = sorted(host["system"] for host in hosts.values() if host["kind"] == kind)
|
||||||
|
if candidates:
|
||||||
|
native_systems[candidates[0]] = True
|
||||||
|
|
||||||
|
return {
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"paths": sorted(paths),
|
||||||
|
"affectedUnits": sorted(affected_units),
|
||||||
|
"affectedHosts": {kind: sorted(names) for kind, names in affected_hosts.items()},
|
||||||
|
"syntheticUnits": {kind: sorted(names) for kind, names in synthetic.items()},
|
||||||
|
"requiresNixValidation": requires_nix,
|
||||||
|
"requiresFullEvaluation": requires_full_eval,
|
||||||
|
"requiresFullNativeBuild": requires_full_build,
|
||||||
|
"nativeBuildSystems": native_systems,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_plan(value: dict[str, Any], reference: str) -> None:
|
||||||
|
print(f"flake: {reference}")
|
||||||
|
print("paths:", ", ".join(value["paths"]) or "(none)")
|
||||||
|
print("units:", ", ".join(value["affectedUnits"]) or "(none)")
|
||||||
|
for kind in ("nixos", "darwin"):
|
||||||
|
print(f"{kind} hosts:", ", ".join(value["affectedHosts"][kind]) or "(none)")
|
||||||
|
print(f"{kind} synthetic:", ", ".join(value["syntheticUnits"][kind]) or "(none)")
|
||||||
|
print("full evaluation:", value["requiresFullEvaluation"])
|
||||||
|
print("full native build:", value["requiresFullNativeBuild"])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_skill(path: Path) -> None:
|
||||||
|
text = path.read_text()
|
||||||
|
if not text.startswith("---\n"):
|
||||||
|
raise ValidationError(f"missing Skill frontmatter: {path}")
|
||||||
|
try:
|
||||||
|
frontmatter = text.split("---\n", 2)[1]
|
||||||
|
name = next(line.split(":", 1)[1].strip() for line in frontmatter.splitlines() if line.startswith("name:"))
|
||||||
|
except (IndexError, StopIteration) as error:
|
||||||
|
raise ValidationError(f"invalid Skill frontmatter: {path}") from error
|
||||||
|
if name != path.parent.name:
|
||||||
|
raise ValidationError(f"Skill name {name!r} does not match directory {path.parent.name!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def static_checks(root: Path, paths: Sequence[str], untracked: set[str]) -> None:
|
||||||
|
for relative in paths:
|
||||||
|
path = root / relative
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
if relative.endswith(".nix"):
|
||||||
|
run(["nix-instantiate", "--parse", str(path)], cwd=root, capture=True)
|
||||||
|
elif relative.endswith(".json"):
|
||||||
|
json.loads(path.read_text())
|
||||||
|
elif relative.endswith(".toml"):
|
||||||
|
tomllib.loads(path.read_text())
|
||||||
|
elif relative.endswith(".py"):
|
||||||
|
compile(path.read_text(), relative, "exec")
|
||||||
|
if path.name == "SKILL.md":
|
||||||
|
validate_skill(path)
|
||||||
|
if relative in untracked:
|
||||||
|
for number, line in enumerate(path.read_text(errors="replace").splitlines(), 1):
|
||||||
|
if line.rstrip() != line:
|
||||||
|
raise ValidationError(f"trailing whitespace: {relative}:{number}")
|
||||||
|
|
||||||
|
diff_paths = [path for path in paths if path not in untracked]
|
||||||
|
if diff_paths:
|
||||||
|
run(["git", "diff", "--check", "HEAD", "--", *diff_paths], cwd=root)
|
||||||
|
|
||||||
|
|
||||||
|
def run_fast(root: Path, reference: str, paths: Sequence[str], untracked: set[str], *, all_files: bool) -> None:
|
||||||
|
static_checks(root, paths, untracked)
|
||||||
|
command = ["nix", "develop", f"{reference}#validation", "--command", "pre-commit", "run"]
|
||||||
|
command += ["--all-files"] if all_files else (["--files", *paths] if paths else [])
|
||||||
|
if paths or all_files:
|
||||||
|
run(command, cwd=root)
|
||||||
|
|
||||||
|
|
||||||
|
def selection_module_expr(unit_id: str) -> str:
|
||||||
|
quoted = json.dumps(unit_id)
|
||||||
|
return f"(flake.lib.registry.mkSelectionModule [ {quoted} ])"
|
||||||
|
|
||||||
|
|
||||||
|
def synthetic_expr(reference: str, host_name: str, host: dict[str, Any], unit_id: str, *, drv_path: bool) -> str:
|
||||||
|
selection = selection_module_expr(unit_id)
|
||||||
|
modules = [selection]
|
||||||
|
if host.get("homeManager", True):
|
||||||
|
user = json.dumps(host["user"])
|
||||||
|
modules.append(f"{{ home-manager.users.{user}.imports = [ {selection} ]; }}")
|
||||||
|
base = f"flake.{('darwinConfigurations' if host['kind'] == 'darwin' else 'nixosConfigurations')}.{json.dumps(host_name)}"
|
||||||
|
output = "extended.system" if host["kind"] == "darwin" else "extended.config.system.build.toplevel"
|
||||||
|
if drv_path:
|
||||||
|
output += ".drvPath"
|
||||||
|
return f'''let
|
||||||
|
flake = builtins.getFlake {json.dumps(reference)};
|
||||||
|
base = {base};
|
||||||
|
extended = base.extendModules {{ modules = [ {' '.join(modules)} ]; }};
|
||||||
|
in {output}'''
|
||||||
|
|
||||||
|
|
||||||
|
def representative_host(metadata: dict[str, Any], kind: str, current_system: str | None = None) -> tuple[str, dict[str, Any]] | None:
|
||||||
|
candidates = [(name, host) for name, host in metadata["hosts"].items() if host["kind"] == kind]
|
||||||
|
if current_system:
|
||||||
|
candidates = [item for item in candidates if item[1]["system"] == current_system]
|
||||||
|
return sorted(candidates)[0] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
|
def host_drv_attr(reference: str, host: dict[str, Any]) -> str:
|
||||||
|
return f"{reference}#{host['buildAttr']}.drvPath"
|
||||||
|
|
||||||
|
|
||||||
|
def run_eval(
|
||||||
|
root: Path,
|
||||||
|
reference: str,
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
value: dict[str, Any],
|
||||||
|
*,
|
||||||
|
all_systems: bool,
|
||||||
|
) -> None:
|
||||||
|
if all_systems or value["requiresFullEvaluation"]:
|
||||||
|
run(["nix", "flake", "check", reference, "--no-build", "--all-systems", "--keep-going", "--show-trace"], cwd=root)
|
||||||
|
return
|
||||||
|
for kind in ("nixos", "darwin"):
|
||||||
|
for name in value["affectedHosts"][kind]:
|
||||||
|
run(["nix", "eval", "--raw", "--show-trace", host_drv_attr(reference, metadata["hosts"][name])], cwd=root)
|
||||||
|
representative = representative_host(metadata, kind)
|
||||||
|
if representative:
|
||||||
|
name, host = representative
|
||||||
|
for unit_id in value["syntheticUnits"][kind]:
|
||||||
|
run(["nix", "eval", "--raw", "--impure", "--show-trace", "--expr", synthetic_expr(reference, name, host, unit_id, drv_path=True)], cwd=root)
|
||||||
|
|
||||||
|
|
||||||
|
def current_system(root: Path) -> str:
|
||||||
|
return run(["nix", "eval", "--raw", "--impure", "--expr", "builtins.currentSystem"], cwd=root, capture=True).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def run_full_native(root: Path, reference: str, system: str) -> None:
|
||||||
|
run(
|
||||||
|
["nix", "run", f"{reference}#nix-fast-build", "--", "--flake", f"{reference}#checks.{system}", "--skip-cached", "--no-nom", "--no-link"],
|
||||||
|
cwd=root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run_build(root: Path, reference: str, metadata: dict[str, Any], value: dict[str, Any]) -> None:
|
||||||
|
system = current_system(root)
|
||||||
|
if value["requiresFullNativeBuild"]:
|
||||||
|
run_full_native(root, reference, system)
|
||||||
|
return
|
||||||
|
|
||||||
|
installables: list[str] = []
|
||||||
|
for kind in ("nixos", "darwin"):
|
||||||
|
for name in value["affectedHosts"][kind]:
|
||||||
|
host = metadata["hosts"][name]
|
||||||
|
if host["system"] == system:
|
||||||
|
installables.append(f"{reference}#{host['buildAttr']}")
|
||||||
|
else:
|
||||||
|
print(f"skip incompatible build: {name} ({host['system']})", file=sys.stderr)
|
||||||
|
for check in ("registry", "validation-tool"):
|
||||||
|
installables.append(f"{reference}#checks.{system}.{check}")
|
||||||
|
if installables:
|
||||||
|
run(["nix", "build", "--no-link", "--keep-going", "--print-build-logs", *sorted(set(installables))], cwd=root)
|
||||||
|
|
||||||
|
for kind in ("nixos", "darwin"):
|
||||||
|
representative = representative_host(metadata, kind, system)
|
||||||
|
if representative:
|
||||||
|
name, host = representative
|
||||||
|
for unit_id in value["syntheticUnits"][kind]:
|
||||||
|
run(["nix", "build", "--no-link", "--impure", "--expr", synthetic_expr(reference, name, host, unit_id, drv_path=False)], cwd=root)
|
||||||
|
|
||||||
|
|
||||||
|
def self_test() -> None:
|
||||||
|
metadata = {
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"units": {
|
||||||
|
"applications.foo": {"directory": "modules/applications/foo", "includes": [], "fragments": ["home", "homeNixos"]},
|
||||||
|
"applications.dormant": {"directory": "modules/applications/dormant", "includes": [], "fragments": ["home"]},
|
||||||
|
"profiles.workload.dev": {"directory": "modules/profiles/workload/dev", "includes": ["applications.foo"], "fragments": ["meta"]},
|
||||||
|
},
|
||||||
|
"hosts": {
|
||||||
|
"linux": {"kind": "nixos", "system": "x86_64-linux", "user": "test", "homeManager": True, "selectedUnits": ["profiles.workload.dev"], "buildAttr": "nixosConfigurations.linux.config.system.build.toplevel"},
|
||||||
|
"mac": {"kind": "darwin", "system": "aarch64-darwin", "user": "test", "homeManager": True, "selectedUnits": ["profiles.workload.dev"], "buildAttr": "darwinConfigurations.mac.system"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
nixos = plan(metadata, ["modules/applications/foo/home/nixos.nix"])
|
||||||
|
assert nixos["affectedHosts"] == {"nixos": ["linux"], "darwin": []}
|
||||||
|
assert nixos["affectedUnits"] == ["applications.foo", "profiles.workload.dev"]
|
||||||
|
common = plan(metadata, ["modules/applications/foo/home.nix"])
|
||||||
|
assert common["affectedHosts"] == {"nixos": ["linux"], "darwin": ["mac"]}
|
||||||
|
dormant = plan(metadata, ["modules/applications/dormant/home.nix"])
|
||||||
|
assert dormant["syntheticUnits"] == {"nixos": ["applications.dormant"], "darwin": ["applications.dormant"]}
|
||||||
|
global_value = plan(metadata, ["flake.nix"])
|
||||||
|
assert global_value["requiresFullEvaluation"] and global_value["requiresFullNativeBuild"]
|
||||||
|
docs = plan(metadata, ["modules/profiles/README.md"])
|
||||||
|
assert not docs["requiresNixValidation"]
|
||||||
|
print("dotfiles-check self-test passed")
|
||||||
|
|
||||||
|
|
||||||
|
def parser() -> argparse.ArgumentParser:
|
||||||
|
result = argparse.ArgumentParser(description=__doc__)
|
||||||
|
result.add_argument("command", choices=("plan", "fast", "eval", "build", "all", "full", "self-test"))
|
||||||
|
result.add_argument("--paths", nargs="+", help="Explicit task-owned repository paths")
|
||||||
|
result.add_argument("--base", help="Compare BASE...HEAD")
|
||||||
|
result.add_argument("--all-files", action="store_true", help="Check every tracked file")
|
||||||
|
result.add_argument("--all-hosts", action="store_true", help="Validate every registered host")
|
||||||
|
result.add_argument("--all-systems", action="store_true", help="Evaluate every flake system")
|
||||||
|
result.add_argument("--json", action="store_true", help="Emit the plan as JSON")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parser().parse_args()
|
||||||
|
if args.command == "self-test":
|
||||||
|
self_test()
|
||||||
|
return 0
|
||||||
|
selectors = sum(bool(value) for value in (args.paths, args.base, args.all_files))
|
||||||
|
if selectors > 1:
|
||||||
|
raise ValidationError("use only one of --paths, --base, or --all-files")
|
||||||
|
if args.command == "full":
|
||||||
|
if selectors or args.all_hosts or args.all_systems:
|
||||||
|
raise ValidationError("full is exhaustive and accepts no scope flags")
|
||||||
|
args.all_files = args.all_hosts = args.all_systems = True
|
||||||
|
|
||||||
|
root = repo_root()
|
||||||
|
paths, untracked = collect_paths(root, args)
|
||||||
|
with flake_reference(root, paths) as reference:
|
||||||
|
metadata = load_metadata(root, reference)
|
||||||
|
value = plan(metadata, paths, all_hosts=args.all_hosts)
|
||||||
|
if args.command == "plan":
|
||||||
|
print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True)) if args.json else render_plan(value, reference)
|
||||||
|
return 0
|
||||||
|
if args.command in {"fast", "all", "full"}:
|
||||||
|
run_fast(root, reference, paths, untracked, all_files=args.all_files)
|
||||||
|
if args.command in {"eval", "all", "full"} and value["requiresNixValidation"]:
|
||||||
|
run_eval(root, reference, metadata, value, all_systems=args.all_systems or args.command in {"all", "full"})
|
||||||
|
if args.command in {"build", "all"} and value["requiresNixValidation"]:
|
||||||
|
run_build(root, reference, metadata, value)
|
||||||
|
if args.command == "full":
|
||||||
|
run_full_native(root, reference, current_system(root))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(main())
|
||||||
|
except ValidationError as error:
|
||||||
|
print(f"error: {error}", file=sys.stderr)
|
||||||
|
raise SystemExit(2) from error
|
||||||
|
except subprocess.CalledProcessError as error:
|
||||||
|
if error.stdout:
|
||||||
|
print(error.stdout, file=sys.stderr, end="")
|
||||||
|
if error.stderr:
|
||||||
|
print(error.stderr, file=sys.stderr, end="")
|
||||||
|
raise SystemExit(error.returncode) from error
|
||||||
@@ -3,15 +3,22 @@ _: {
|
|||||||
{
|
{
|
||||||
pkgs,
|
pkgs,
|
||||||
config,
|
config,
|
||||||
|
lib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
devShells.dotnix = pkgs.mkShell {
|
devShells.dotnix = pkgs.mkShell {
|
||||||
packages = [
|
packages = [
|
||||||
config.treefmt.build.wrapper
|
config.treefmt.build.wrapper
|
||||||
|
config.packages.dotfiles-check
|
||||||
|
|
||||||
|
pkgs.actionlint
|
||||||
pkgs.git
|
pkgs.git
|
||||||
pkgs.gitleaks
|
pkgs.gitleaks
|
||||||
|
pkgs.mcp-nixos
|
||||||
|
pkgs.nix-fast-build
|
||||||
|
pkgs.nix-tree
|
||||||
|
pkgs.nixd
|
||||||
pkgs.pre-commit
|
pkgs.pre-commit
|
||||||
|
|
||||||
# sops-nix / age
|
# sops-nix / age
|
||||||
@@ -22,6 +29,8 @@ _: {
|
|||||||
# YubiKey for sops editing
|
# YubiKey for sops editing
|
||||||
pkgs.age-plugin-yubikey
|
pkgs.age-plugin-yubikey
|
||||||
pkgs.yubikey-manager
|
pkgs.yubikey-manager
|
||||||
|
]
|
||||||
|
++ lib.optionals pkgs.stdenv.isLinux [
|
||||||
pkgs.pcsc-tools
|
pkgs.pcsc-tools
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -95,26 +95,33 @@ points cannot express the requirement.
|
|||||||
|
|
||||||
### 4. Prove the change
|
### 4. Prove the change
|
||||||
|
|
||||||
Run the validation matrix in
|
Invoke the `validate-nix-change` skill and use the task-owned files as its
|
||||||
[references/review-checklist.md](references/review-checklist.md). At minimum:
|
explicit path set. At minimum:
|
||||||
|
|
||||||
1. Format the task-owned files with the repository formatter and run
|
1. Inspect `nix run .#check -- plan --paths <task-path>... --json` and confirm
|
||||||
`git diff --check`.
|
the reported units, host classes, and real hosts are correct.
|
||||||
2. Inspect the complete task diff for accidental files, duplication, leaked
|
2. Run `nix run .#check -- fast --paths <task-path>...` during the edit loop.
|
||||||
|
3. Inspect the complete task diff for accidental files, duplication, leaked
|
||||||
secrets, forced values, direct enable assignments, and unrelated rewrites.
|
secrets, forced values, direct enable assignments, and unrelated rewrites.
|
||||||
3. Run `nix flake check`.
|
4. Run `nix run .#check -- all --paths <task-path>...` after the structure is
|
||||||
4. Run `pre-commit run --all-files`.
|
complete. This evaluates every flake system and builds affected targets for
|
||||||
5. Evaluate every affected real host without switching it. For a
|
the current platform without activation.
|
||||||
cross-platform unit or profile, evaluate both NixOS and nix-darwin even if
|
5. Verify selection as well as syntax: confirm that the expected package,
|
||||||
only one class changed. Build an affected configuration with `--no-link`
|
|
||||||
when the current platform can build it.
|
|
||||||
6. Verify selection as well as syntax: confirm that the expected package,
|
|
||||||
program, service, group, cask, or external module appears in the resulting
|
program, service, group, cask, or external module appears in the resulting
|
||||||
configuration.
|
configuration.
|
||||||
|
6. Add a `pkgs.testers.runNixOSTest` check through the `test-nixos-service`
|
||||||
|
skill when service startup or another runtime contract cannot be proved by
|
||||||
|
evaluation and a system build.
|
||||||
|
|
||||||
|
Use `nix run .#check -- full` only for CI, scheduled maintenance, or an explicit
|
||||||
|
repository-wide audit. These commands never activate the live system. Do not
|
||||||
|
run `nh os switch`, `nixos-rebuild switch`, `darwin-rebuild switch`,
|
||||||
|
`home-manager switch`, or an equivalent activation command as validation.
|
||||||
|
|
||||||
If a command is unavailable, blocked by the environment, or fails for a
|
If a command is unavailable, blocked by the environment, or fails for a
|
||||||
pre-existing reason, diagnose it and report the exact gap. Never silently skip
|
pre-existing reason, invoke the `debug-nix-failure` skill, diagnose it, and
|
||||||
a required check or weaken the implementation to make a check pass.
|
report the exact gap. Never silently skip a required check or weaken the
|
||||||
|
implementation to make a check pass.
|
||||||
|
|
||||||
### 5. Audit before completion
|
### 5. Audit before completion
|
||||||
|
|
||||||
|
|||||||
@@ -72,56 +72,47 @@ user-owned mutable state unless the requested policy explicitly owns it.
|
|||||||
|
|
||||||
## Validation matrix
|
## Validation matrix
|
||||||
|
|
||||||
Run checks from the repository root and keep the exact results for the handoff.
|
Use the `validate-nix-change` skill and run checks from the repository root.
|
||||||
Do not switch or activate a live system merely to validate a change.
|
Keep exact results for the handoff. The validation app never switches or
|
||||||
|
activates a live system.
|
||||||
|
|
||||||
### Always
|
### Always
|
||||||
|
|
||||||
1. Format task-owned files. If the worktree contains unrelated user changes,
|
1. Run `nix run .#check -- plan --paths <task-path>... --json` and inspect the
|
||||||
pass only task-owned paths to the configured formatter when supported.
|
affected units and hosts.
|
||||||
2. Run `git diff --check`.
|
2. Run `nix run .#check -- fast --paths <task-path>...` during implementation.
|
||||||
3. Review `git status --short`, `git diff --stat`, and the complete `git diff`.
|
3. Review `git status --short`, `git diff --stat`, and the complete task diff.
|
||||||
4. Run `nix flake check`.
|
4. Run `nix run .#check -- all --paths <task-path>...` before handoff. It runs
|
||||||
5. Run `pre-commit run --all-files`.
|
all-system evaluation and compatible targeted builds without activation.
|
||||||
|
5. Reserve `nix run .#check -- full` for CI, scheduled maintenance, or an
|
||||||
|
explicit repository-wide audit.
|
||||||
|
|
||||||
|
Do not run `nh os switch`, `nixos-rebuild switch`, `darwin-rebuild switch`,
|
||||||
|
`home-manager switch`, or an equivalent activation command.
|
||||||
|
|
||||||
### NixOS or Home Manager on NixOS
|
### NixOS or Home Manager on NixOS
|
||||||
|
|
||||||
- Evaluate each affected host's system toplevel derivation.
|
- Confirm that the validation plan includes each affected real NixOS host.
|
||||||
- Build at least one affected NixOS configuration with `--no-link` when the
|
- Build affected NixOS configurations with `--no-link` through the validation
|
||||||
current system supports it.
|
app when the current system supports them.
|
||||||
- Inspect the resulting option that proves selection: for example
|
- Inspect the resulting option that proves selection: for example
|
||||||
`environment.systemPackages`, the user's `home.packages`,
|
`environment.systemPackages`, the user's `home.packages`,
|
||||||
`systemd.services`, `users.users.<name>.extraGroups`, or the upstream
|
`systemd.services`, `users.users.<name>.extraGroups`, or the upstream
|
||||||
`programs`/`services` option.
|
`programs`/`services` option.
|
||||||
|
|
||||||
Typical build shape:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
nix build .#nixosConfigurations.<host>.config.system.build.toplevel --no-link
|
|
||||||
```
|
|
||||||
|
|
||||||
### nix-darwin or Home Manager on Darwin
|
### nix-darwin or Home Manager on Darwin
|
||||||
|
|
||||||
- Evaluate every affected Darwin host even when running on Linux.
|
- Confirm that every affected Darwin host is evaluated even when running on
|
||||||
|
Linux.
|
||||||
- Inspect `homebrew.casks` or `homebrew.brews` for Homebrew-backed additions.
|
- Inspect `homebrew.casks` or `homebrew.brews` for Homebrew-backed additions.
|
||||||
- Evaluate the relevant Home Manager program or package option.
|
- Evaluate the relevant Home Manager program or package option.
|
||||||
- Build a Darwin configuration only on a compatible Darwin builder; otherwise
|
- Build a Darwin configuration only on a compatible Darwin runner or builder;
|
||||||
report that build as an explicit runtime-validation gap.
|
otherwise report the build as an explicit platform gap.
|
||||||
|
|
||||||
Typical evaluation shapes:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
nix eval --raw .#darwinConfigurations.<host>.system.drvPath
|
|
||||||
nix eval --json .#darwinConfigurations.<host>.config.homebrew.casks
|
|
||||||
```
|
|
||||||
|
|
||||||
Confirm the exact output attribute against the current flake before using a
|
|
||||||
command; do not paste these shapes blindly.
|
|
||||||
|
|
||||||
### Profiles and cross-platform changes
|
### Profiles and cross-platform changes
|
||||||
|
|
||||||
- Determine transitive selection through `meta.includes`, not only direct
|
- Confirm transitive selection through `meta.includes`, not only direct
|
||||||
mentions.
|
mentions. The validation plan computes reverse dependency closure.
|
||||||
- Evaluate every real host selecting the changed profile.
|
- Evaluate every real host selecting the changed profile.
|
||||||
- Evaluate both host classes for a cross-platform profile, even if only one
|
- Evaluate both host classes for a cross-platform profile, even if only one
|
||||||
current fragment changed.
|
current fragment changed.
|
||||||
@@ -134,6 +125,8 @@ command; do not paste these shapes blindly.
|
|||||||
### Runtime-dependent behavior
|
### Runtime-dependent behavior
|
||||||
|
|
||||||
Evaluation and builds cannot prove GUI appearance, credentials, network access,
|
Evaluation and builds cannot prove GUI appearance, credentials, network access,
|
||||||
hardware behavior, or successful daemon interaction. State the precise manual
|
hardware behavior, successful daemon interaction, or reboot state. For
|
||||||
post-activation check needed for those behaviors. Never describe evaluation as
|
reusable NixOS behavior, use the `test-nixos-service` skill and add a
|
||||||
|
`pkgs.testers.runNixOSTest` check. State the precise manual post-activation check
|
||||||
|
needed for physical hardware or external systems. Never describe evaluation as
|
||||||
a runtime test.
|
a runtime test.
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: debug-nix-failure
|
||||||
|
description: Diagnose failures from parsing, Nix module evaluation, derivation builds, flake checks, NixOS tests, or Home Manager activation logs without changing the live system. Use when `nix run .#check`, `nix flake check`, `nix build`, CI, or a user-provided activation log fails.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Debug a Nix Failure
|
||||||
|
|
||||||
|
Classify the failure before changing code. Preserve the original command,
|
||||||
|
complete error, first causal frame, and affected attribute. Never run a live
|
||||||
|
switch or activation to reproduce a validation failure.
|
||||||
|
|
||||||
|
## Identify the failing layer
|
||||||
|
|
||||||
|
- **Parse or format:** syntax location, malformed string, unmatched delimiter,
|
||||||
|
or formatter-owned rewrite.
|
||||||
|
- **Static analysis:** dead binding, suspicious expression, ShellCheck finding,
|
||||||
|
secret scan, or workflow lint.
|
||||||
|
- **Module evaluation:** missing option, wrong type, assertion, infinite
|
||||||
|
recursion, conflicting definitions, Registry selection, or unsupported host
|
||||||
|
class.
|
||||||
|
- **Derivation instantiation/build:** missing dependency, hash mismatch, patch
|
||||||
|
failure, compiler/test failure, sandbox violation, or unsupported platform.
|
||||||
|
- **NixOS test:** failed unit, timeout, command assertion, network readiness, or
|
||||||
|
reboot state.
|
||||||
|
- **Activation/runtime:** filesystem conflict, activation script, systemd unit,
|
||||||
|
hardware, credential, or external-service behavior. Diagnose only from logs
|
||||||
|
supplied by the user unless they explicitly request a non-switch inspection
|
||||||
|
command.
|
||||||
|
|
||||||
|
## Reproduce the narrowest failing operation
|
||||||
|
|
||||||
|
Start with the stage and paths reported by the validation app:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- plan --paths <task-path>... --json
|
||||||
|
nix run .#check -- fast --paths <task-path>...
|
||||||
|
nix run .#check -- eval --paths <task-path>...
|
||||||
|
nix run .#check -- build --paths <task-path>...
|
||||||
|
```
|
||||||
|
|
||||||
|
For a single attribute, use `nix eval --show-trace` on its `drvPath` before a
|
||||||
|
build. For a failed derivation, retain `--print-build-logs` and inspect
|
||||||
|
`nix log <drv-path>` when the summary omits the causal lines.
|
||||||
|
|
||||||
|
## Read traces selectively
|
||||||
|
|
||||||
|
1. Find the first repository-owned frame or option path.
|
||||||
|
2. Separate the immediate failure from wrapper frames in `modules.nix`,
|
||||||
|
`lib.evalModules`, or flake-parts.
|
||||||
|
3. Inspect the option declaration and every definition contributing to it.
|
||||||
|
4. Confirm package and option names against locked inputs, not memory.
|
||||||
|
5. Check whether the failure reproduces on the base revision before calling it
|
||||||
|
task-owned.
|
||||||
|
|
||||||
|
Do not respond to a type or ownership error with import-order changes,
|
||||||
|
`lib.mkForce`, global arguments, or an overlay unless repository evidence shows
|
||||||
|
that those mechanisms are the correct owner.
|
||||||
|
|
||||||
|
## Finish with a bounded diagnosis
|
||||||
|
|
||||||
|
Report the failing layer, root cause, minimal correction, rerun command, and any
|
||||||
|
remaining platform or runtime gap. Include enough of the error to identify it,
|
||||||
|
but do not paste large unrelated logs.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: test-nixos-service
|
||||||
|
description: Add or extend a non-activating NixOS VM or container test for service startup, sockets, timers, permissions, firewall behavior, reboot state, and inter-service dependencies. Use when evaluation and a system build cannot prove the requested runtime behavior.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Test NixOS Runtime Behavior
|
||||||
|
|
||||||
|
Prefer `pkgs.testers.runNixOSTest` for reusable NixOS behavior that can be
|
||||||
|
proved without the user's physical machine. Do not activate the host
|
||||||
|
configuration and do not substitute a live `nh os switch` for a deterministic
|
||||||
|
test.
|
||||||
|
|
||||||
|
## Define the observable contract
|
||||||
|
|
||||||
|
List the runtime facts that must hold, such as:
|
||||||
|
|
||||||
|
- a systemd unit reaches `active`;
|
||||||
|
- a socket or port is listening;
|
||||||
|
- a timer triggers its service;
|
||||||
|
- a user can or cannot read a file;
|
||||||
|
- a group membership grants access;
|
||||||
|
- a firewall permits one path and blocks another;
|
||||||
|
- state survives a reboot;
|
||||||
|
- one service waits for another dependency.
|
||||||
|
|
||||||
|
Exclude behavior that requires physical GPU, fingerprint, audio, display,
|
||||||
|
Secure Boot, TPM, private credentials, or an external provider unless the test
|
||||||
|
can model it explicitly.
|
||||||
|
|
||||||
|
## Implement the smallest useful machine
|
||||||
|
|
||||||
|
Create a test under `tests/` and expose it through `checks.<system>`. Import the
|
||||||
|
owning module or Registry selection instead of copying its implementation into
|
||||||
|
the test. Use only the packages, users, files, and network peers required by the
|
||||||
|
contract.
|
||||||
|
|
||||||
|
Typical shape:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
pkgs.testers.runNixOSTest {
|
||||||
|
name = "service-name";
|
||||||
|
|
||||||
|
nodes.machine = {
|
||||||
|
# Enable the owning unit or import the module under test.
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
machine.start()
|
||||||
|
machine.wait_for_unit("service-name.service")
|
||||||
|
machine.succeed("systemctl is-active service-name.service")
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `wait_for_unit`, `wait_for_open_port`, `succeed`, `fail`, and explicit
|
||||||
|
reboots to express outcomes. Avoid arbitrary sleeps when a readiness condition
|
||||||
|
exists.
|
||||||
|
|
||||||
|
## Validate and report
|
||||||
|
|
||||||
|
Run the targeted test through its flake check, then run the repository
|
||||||
|
validation app for the task paths. Report the test attribute and assertions
|
||||||
|
that passed. State clearly which hardware or external behavior remains outside
|
||||||
|
the VM/container model.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: update-flake-input
|
||||||
|
description: Update one or more pinned flake inputs with bounded lock-file changes and non-activating Linux and Darwin validation. Use for dependency refreshes, input-specific updates, automated lock-file pull requests, or diagnosing a regression introduced by flake.lock.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Update a Flake Input
|
||||||
|
|
||||||
|
Keep the update scope explicit and treat `flake.lock` as generated dependency
|
||||||
|
state. Never activate a host as part of this workflow; do not run `nh os switch`,
|
||||||
|
`nixos-rebuild switch`, `darwin-rebuild switch`, `home-manager switch`, or an
|
||||||
|
equivalent command.
|
||||||
|
|
||||||
|
## Bound the update
|
||||||
|
|
||||||
|
1. Read `AGENTS.md`, inspect `git status --short`, and preserve unrelated work.
|
||||||
|
2. Record the input names and the behavior or version change being requested.
|
||||||
|
3. Prefer an input-specific update:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix flake update <input-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use an unrestricted `nix flake update` only when the task explicitly requests a
|
||||||
|
full refresh. Do not hand-edit lock nodes.
|
||||||
|
|
||||||
|
## Audit the lock diff
|
||||||
|
|
||||||
|
Inspect the complete `flake.lock` diff. Confirm that changed nodes are the
|
||||||
|
requested inputs or unavoidable followers and that source owners, repositories,
|
||||||
|
reference types, and hashes remain expected. Investigate unexpected node
|
||||||
|
replacement, disappearing followers, or a large transitive graph rewrite before
|
||||||
|
validation.
|
||||||
|
|
||||||
|
## Validate without activation
|
||||||
|
|
||||||
|
Run the common validation workflow against the lock file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- plan --paths flake.lock --json
|
||||||
|
nix run .#check -- fast --paths flake.lock
|
||||||
|
nix run .#check -- eval --paths flake.lock
|
||||||
|
nix run .#check -- build --paths flake.lock
|
||||||
|
```
|
||||||
|
|
||||||
|
A lock-file change requires full evaluation and the complete native check set.
|
||||||
|
Linux and Darwin builds must run on compatible runners. The scheduled update
|
||||||
|
workflow uploads the candidate lock file, builds Linux and Darwin checks, and
|
||||||
|
creates a pull request only after both pass.
|
||||||
|
|
||||||
|
When a failure appears only after the update, invoke `debug-nix-failure`, compare
|
||||||
|
the failing derivation or option with the base lock, and narrow the responsible
|
||||||
|
input before adding an override or patch.
|
||||||
|
|
||||||
|
## Report the result
|
||||||
|
|
||||||
|
List requested and transitively changed inputs, validation commands and native
|
||||||
|
platform results, any package or option migration, and remaining manual runtime
|
||||||
|
checks. Evaluation or a native build is not activation.
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
name: validate-nix-change
|
||||||
|
description: Plan and run efficient, non-activating validation for edits to this NixOS, nix-darwin, and Home Manager flake. Use after changing Nix modules, hosts, profiles, overlays, flake outputs, tests, scripts, CI, or agent configuration; before handing off a task; or when deciding which real hosts must be evaluated or built.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Validate a Nix Change
|
||||||
|
|
||||||
|
Use the repository validation app as the source of truth for change impact and
|
||||||
|
validation commands. It derives affected hosts from Registry ownership,
|
||||||
|
`meta.includes`, host selections, fragment class, and host-local paths.
|
||||||
|
|
||||||
|
Never activate a live configuration as part of this workflow. Do not run
|
||||||
|
`nh os switch`, `nixos-rebuild switch`, `darwin-rebuild switch`,
|
||||||
|
`home-manager switch`, or an equivalent activation command. The user owns live
|
||||||
|
activation separately.
|
||||||
|
|
||||||
|
## Establish the validation scope
|
||||||
|
|
||||||
|
1. Read `AGENTS.md` and run `git status --short` before editing.
|
||||||
|
2. Preserve unrelated user changes. Track the paths owned by the current task,
|
||||||
|
including newly created untracked files.
|
||||||
|
3. Inspect the plan before expensive checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- plan --paths <task-path>... --json
|
||||||
|
```
|
||||||
|
|
||||||
|
When validating a committed pull-request range, use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- plan --base <base-sha> --json
|
||||||
|
```
|
||||||
|
|
||||||
|
The app automatically uses a `path:` flake reference when task paths are
|
||||||
|
untracked, so newly created Registry fragments are visible to Nix without
|
||||||
|
staging them.
|
||||||
|
|
||||||
|
## Run checks in increasing cost order
|
||||||
|
|
||||||
|
### Fast edit loop
|
||||||
|
|
||||||
|
After each coherent edit, parse Nix files, validate project JSON, TOML, and
|
||||||
|
skill frontmatter, check whitespace, and run the configured hooks only for
|
||||||
|
task-owned files:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- fast --paths <task-path>...
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not replace this with `pre-commit run --all-files` during the edit loop.
|
||||||
|
Unrelated repository files must not become part of the task merely because an
|
||||||
|
existing check fails elsewhere.
|
||||||
|
|
||||||
|
### Evaluation
|
||||||
|
|
||||||
|
After the implementation is structurally complete, evaluate every affected
|
||||||
|
NixOS and Darwin derivation plus the supporting checks without realizing or
|
||||||
|
activating them. Flake-wide paths additionally evaluate every flake system:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- eval --paths <task-path>...
|
||||||
|
```
|
||||||
|
|
||||||
|
This proves module evaluation, option types, assertions, Registry selection,
|
||||||
|
and derivation instantiation. It does not prove a successful build or runtime
|
||||||
|
behavior.
|
||||||
|
|
||||||
|
### Compatible builds
|
||||||
|
|
||||||
|
Build affected configurations for the current platform with no result link:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- build --paths <task-path>...
|
||||||
|
```
|
||||||
|
|
||||||
|
The app reports incompatible targets as evaluated but skipped for native build.
|
||||||
|
A Darwin target must be built by a compatible Darwin runner or builder; a Linux
|
||||||
|
evaluation is not a Darwin build.
|
||||||
|
|
||||||
|
### Final task validation
|
||||||
|
|
||||||
|
Before handoff, run the cumulative task check. It applies file checks only to
|
||||||
|
task-owned paths, evaluates every flake system, and builds affected native
|
||||||
|
targets:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- all --paths <task-path>...
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--all-hosts` only when a deliberate audit must report every registered
|
||||||
|
host as affected. Use the exhaustive command for CI, scheduled maintenance, or
|
||||||
|
an explicit repository-wide audit:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#check -- full
|
||||||
|
```
|
||||||
|
|
||||||
|
`full` runs hooks over every tracked file and builds every check for the current
|
||||||
|
platform through `nix-fast-build`.
|
||||||
|
|
||||||
|
## Add runtime tests when needed
|
||||||
|
|
||||||
|
Evaluation and builds do not prove service startup, socket behavior, firewall
|
||||||
|
rules, users and groups, permissions, reboot behavior, or network interaction.
|
||||||
|
For reusable NixOS behavior, invoke the `test-nixos-service` skill and add a
|
||||||
|
`pkgs.testers.runNixOSTest` check. Hardware, credentials, GUI appearance, and
|
||||||
|
external services may still require a precisely described manual check after
|
||||||
|
the user activates the configuration.
|
||||||
|
|
||||||
|
## Diagnose failures by layer
|
||||||
|
|
||||||
|
Invoke the `debug-nix-failure` skill when a stage fails. Fix the first failing
|
||||||
|
layer before running a more expensive one. Do not hide a pre-existing failure,
|
||||||
|
weaken an assertion, add `lib.mkForce`, or skip a required host merely to make
|
||||||
|
the task appear green.
|
||||||
|
|
||||||
|
## Report evidence precisely
|
||||||
|
|
||||||
|
Conclude with:
|
||||||
|
|
||||||
|
- task-owned paths;
|
||||||
|
- affected units and hosts reported by `plan`;
|
||||||
|
- each command run and its result;
|
||||||
|
- which targets were parsed, evaluated, built, or runtime-tested;
|
||||||
|
- any compatible-platform or manual-runtime gap.
|
||||||
|
|
||||||
|
Never describe evaluation as a build, a build as activation, or a VM test as
|
||||||
|
proof of hardware-specific behavior.
|
||||||
Reference in New Issue
Block a user