From 778b42ca3727145eccd4a9ce373222f9bbdf6521 Mon Sep 17 00:00:00 2001 From: Kris Williams <115474+kriswill@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:40:54 -0700 Subject: [PATCH 1/4] ci: build both deployed closures on PRs; weekly flake.lock bump PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ci.yml builds darwinConfigurations.k.system on the free arm64 macOS runner and nebula's NixOS toplevel on ubuntu (disk-reclaim first — gaming profile, nvidia, source-built hyprland). update-flake-lock.yml opens a weekly bump PR with a fine-grained PAT (FLAKE_UPDATE_PAT) since GITHUB_TOKEN-created events never trigger workflows. Load-bearing security property: builds never decrypt sops secrets, so CI's only credential is the read-only okflight deploy key (OKFLIGHT_DEPLOY_KEY, same secret pages.yml uses) — no signing key, no age key, ever. See knowledge/decisions/ci-github-actions.md. Claude-Session: https://claude.ai/code/session_01Si4x5wbNVNuHgh4TyU66D8 --- .github/workflows/ci.yml | 85 ++++++++++++++++++++++++ .github/workflows/update-flake-lock.yml | 40 +++++++++++ knowledge/decisions/ci-github-actions.md | 75 +++++++++++++++++++++ knowledge/decisions/index.md | 1 + knowledge/log.md | 11 +++ 5 files changed, 212 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/update-flake-lock.yml create mode 100644 knowledge/decisions/ci-github-actions.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..8cb3427e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +# Build gate: the two host closures this repo actually deploys — k +# (aarch64-darwin, on the arm64 macOS runner; free on public repos) and +# nebula's NixOS toplevel (x86_64-linux). Deliberately NOT `nix flake check`: +# the darwin checks would build all three darwin hosts; the gate is k only. +# +# Both jobs run ssh-agent because the flake has a private git+ssh input +# (okf → kriswill/okflight; read-only deploy key, the same OKFLIGHT_DEPLOY_KEY +# secret pages.yml uses). Fork PRs get no secrets, so the okf fetch — and the +# job — fails there; acceptable for a personal repo. +# +# NO signing key and NO age key in CI, ever: builds never decrypt sops +# secrets (decryption happens at activation on the host), so the only +# credential here is the read-only okflight deploy key. See +# knowledge/decisions/ci-github-actions.md. +# +# No hyprland.cachix.org substituter: hyprland/noctalia follow this flake's +# nixpkgs, so their upstream caches can never hit — nebula rebuilds them from +# source on bumps by design (the cost of the `follows` decision). +name: ci + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + darwin-k: + runs-on: macos-latest # arm64 == aarch64-darwin, matches host k + timeout-minutes: 120 + steps: + - uses: actions/checkout@v7 + + - uses: DeterminateSystems/determinate-nix-action@v3 + + # GHA-cache-backed store cache (~10 GiB/repo) — mainly helps the + # custom packages (ccglass, okf, tomato, nas-mount, …). + - uses: DeterminateSystems/magic-nix-cache-action@v14 + + - uses: webfactory/ssh-agent@v0.10.0 + with: + ssh-private-key: ${{ secrets.OKFLIGHT_DEPLOY_KEY }} + + - name: Ensure github.com in known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan github.com >> ~/.ssh/known_hosts + + - name: Build darwin system closure (k) + run: nix build .#darwinConfigurations.k.system -L + + nixos-nebula: + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + # nebula's closure (gaming profile, nvidia, source-built hyprland) can + # exceed the stock runner's free disk — reclaim preinstalled bloat + # first. Tune or drop once the first runs report disk high-water. + - uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: true + + - uses: actions/checkout@v7 + + - uses: DeterminateSystems/determinate-nix-action@v3 + + - uses: webfactory/ssh-agent@v0.10.0 + with: + ssh-private-key: ${{ secrets.OKFLIGHT_DEPLOY_KEY }} + + - name: Ensure github.com in known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan github.com >> ~/.ssh/known_hosts + + - name: Build NixOS toplevel (nebula) + run: nix build .#nixosConfigurations.nebula.config.system.build.toplevel -L diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml new file mode 100644 index 00000000..f9aa78c2 --- /dev/null +++ b/.github/workflows/update-flake-lock.yml @@ -0,0 +1,40 @@ +# Weekly flake.lock bump PR. Uses a fine-grained PAT (FLAKE_UPDATE_PAT — NOT +# the default GITHUB_TOKEN) so the resulting PR triggers ci.yml: GitHub never +# runs workflows on events created with GITHUB_TOKEN. PAT scope: this repo +# only, Contents R/W + Pull requests R/W. +# +# ssh-agent is REQUIRED here: `nix flake update` re-fetches every input, +# including the private git+ssh okf (kriswill/okflight, read-only deploy key). +name: update-flake-lock + +on: + schedule: + - cron: "0 10 * * 1" # Mondays 10:00 UTC + workflow_dispatch: + +permissions: + contents: read # the PAT, not this token, does the writing + +jobs: + update: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + + - uses: DeterminateSystems/determinate-nix-action@v3 + + - uses: webfactory/ssh-agent@v0.10.0 + with: + ssh-private-key: ${{ secrets.OKFLIGHT_DEPLOY_KEY }} + + - name: Ensure github.com in known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan github.com >> ~/.ssh/known_hosts + + - uses: DeterminateSystems/update-flake-lock@v28 + with: + token: ${{ secrets.FLAKE_UPDATE_PAT }} + pr-title: "flake.lock: weekly update" + pr-labels: dependencies diff --git a/knowledge/decisions/ci-github-actions.md b/knowledge/decisions/ci-github-actions.md new file mode 100644 index 00000000..f0282e0d --- /dev/null +++ b/knowledge/decisions/ci-github-actions.md @@ -0,0 +1,75 @@ +--- +type: Decision +title: CI Builds Both Host Closures — No Signing Key, No Age Key, Ever +description: 'GitHub Actions builds darwinConfigurations.k.system (free arm64 macOS runner, public repo) and nebula''s NixOS toplevel on every PR, plus a weekly update-flake-lock PR via a fine-grained PAT. The load-bearing security property: builds never decrypt sops secrets, so CI''s only credential is the read-only okflight deploy key — the Developer ID signing key never touches GitHub.' +tags: [ci, security, codesigning] +timestamp: '2026-07-10T21:30:00Z' +--- + +**Status:** active. **Where:** `.github/workflows/ci.yml`, +`.github/workflows/update-flake-lock.yml`; signing context: the +nas-mount codesigning record (PR #32; re-link once it merges). + +## Context + +Automating nas-mount's Developer ID signing (the nas-mount codesigning +record, PR #32, 2026-07-10 update) raised +the follow-on wish: auto-build the flake's deployed systems in CI so flake +input bumps arrive as reviewed, build-tested PRs. The original sketch had CI +holding the age private key as a GitHub secret "so it can sign during +builds" — investigated and rejected: sops-nix decrypts at *activation* on +the host, never at build, so a system build needs no secrets at all. That +observation removed the entire hard part. + +## Decision + +- **`ci.yml`** (pull_request + push to main): `darwin-k` builds + `.#darwinConfigurations.k.system` on `macos-latest` (arm64 — free and + unlimited on public repos; the ~10× private-repo minute multiplier does + not apply); `nixos-nebula` builds + `.#nixosConfigurations.nebula.config.system.build.toplevel` on + `ubuntu-latest` behind a `jlumbroso/free-disk-space` reclaim step + (nebula's closure: gaming profile, nvidia, source-built Hyprland). + Deliberately not `nix flake check` — that would build all three darwin + hosts; the gate is the two machines actually deployed. +- **CI's only credential is `OKFLIGHT_DEPLOY_KEY`** (pre-existing read-only + deploy key for the private `git+ssh` okf input, loaded via + `webfactory/ssh-agent`; same secret pages.yml uses). **No signing key and + no age key in CI ever**: a compromised workflow, malicious PR, or + exfiltrated secret store cannot leak what was never there. Fork PRs get + no secrets and simply fail on the okf fetch — acceptable for a personal + repo. +- **`update-flake-lock.yml`** (weekly cron + dispatch): + `DeterminateSystems/update-flake-lock@v28` opens the bump PR with a + fine-grained PAT (`FLAKE_UPDATE_PAT`, this repo only, Contents R/W + + Pull requests R/W) because events created with the default `GITHUB_TOKEN` + never trigger other workflows — the PAT is what makes ci.yml run on the + bump PR. Chosen over Dependabot's native nix support (April 2026) + because Dependabot cannot bump the private git+ssh okf input or the + FlakeHub `determinate` input. +- **Accepted cost:** hyprland/noctalia `follows` this flake's nixpkgs, so + their upstream caches can never hit — every bump PR rebuilds them from + source on the nebula job. `magic-nix-cache-action` (GHA cache, ~10 GiB) + is included on the darwin job for the custom packages. + +## Consequences + +- Flake bumps arrive as PRs whose CI proves both deployed systems still + build — the pre-`nrs` gate runs before anything lands on a machine. +- The signing story stays host-local: CI builds ship the store bundle with + only the build-time ad-hoc signature; the Developer ID signature is + applied at activation on `k` alone. +- Watch: nebula job wall-clock and disk high-water on the first runs (tune + or drop the disk-reclaim step); the PAT's expiry (~1 year) needs a + calendar note; `timeout-minutes` may need raising on uncached + hyprland-bump PRs. + +## Citations + +- [GitHub: workflows are not triggered by GITHUB_TOKEN events](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow) +- [DeterminateSystems/update-flake-lock](https://github.com/DeterminateSystems/update-flake-lock) +- [webfactory/ssh-agent](https://github.com/webfactory/ssh-agent) +- [GitHub-hosted runners: standard runners are free for public repositories](https://docs.github.com/en/actions/reference/runners/github-hosted-runners) +- Decision context: the nas-mount codesigning record and the + `docs/darwin-codesigning.md` manual — both land with PR #32; re-link + here once it merges. diff --git a/knowledge/decisions/index.md b/knowledge/decisions/index.md index 2330ea51..28368e14 100644 --- a/knowledge/decisions/index.md +++ b/knowledge/decisions/index.md @@ -7,6 +7,7 @@ choice is made (the commit body can then simply link here). ## Concepts * [Apple container — Repackage and Wrap, Don't Build](apple-container-wrapper.md) - Apple's container CLI is repackaged from the signed .pkg (never built from source) and wrapped so its install root resolves to the store path where the plugins actually live. +* [CI Builds Both Host Closures — No Signing Key, No Age Key, Ever](ci-github-actions.md) - GitHub Actions builds darwinConfigurations.k.system (free arm64 macOS runner, public repo) and nebula's NixOS toplevel on every PR, plus a weekly update-flake-lock PR via a fine-grained PAT. The load-bearing security property: builds never decrypt sops secrets, so CI's only credential is the read-only okflight deploy key — the Developer ID signing key never touches GitHub. * [Claude Profile Isolation Strategy](claude-profile-isolation.md) - The claude wrapper prefers each profile's own interactive login and uses the Keychain token only as a fallback; the desktop app is pinned via a launchd Aqua-domain setenv plus a shell scrub. * [codebase-memory-mcp via Nix-aware Fork](codebase-memory-fork.md) - The codebase-memory MCP server is consumed from the kriswill fork's nix branch (Nix symbols + flake topology, PR #19 upstream) with its index artifact kept out of git for now. * [Route All Linting and Formatting Through efm-langserver](efm-umbrella-formatting.md) - One umbrella LSP (efm) runs every CLI linter and formatter; format-on-save filters to efm only, so no two tools ever compete over a buffer. diff --git a/knowledge/log.md b/knowledge/log.md index b57a13eb..69c55c1c 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -58,6 +58,17 @@ ## 2026-07-10 +- **Creation** — [ci-github-actions](decisions/ci-github-actions.md) + + `.github/workflows/{ci,update-flake-lock}.yml`: GitHub Actions now builds + both deployed closures on every PR — `darwinConfigurations.k.system` on + the free arm64 macOS runner, nebula's toplevel on ubuntu behind a + disk-reclaim step — plus a weekly `update-flake-lock@v28` bump PR opened + with a fine-grained PAT (GITHUB_TOKEN-created events never trigger + workflows). Load-bearing property: builds never decrypt sops secrets, so + CI's only credential is the read-only okflight deploy key — no signing + key, no age key, ever. Chosen over Dependabot's native nix support + (April 2026) because Dependabot can't bump the private git+ssh okf input. + - **Update** — [claude-account-selector](modules/claude-account-selector.md) / [claude-profile-isolation](decisions/claude-profile-isolation.md): new `fallbackProfile` option symlinks `~/.claude` → `~/.claude-` at From 4311c906946882f9c5a2e5cf9fd248b0ace1f0e6 Mon Sep 17 00:00:00 2001 From: Kris Williams <115474+kriswill@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:32:18 -0700 Subject: [PATCH 2/4] =?UTF-8?q?ci(nebula):=20retry=20flake-input=20fetchin?= =?UTF-8?q?g=20=E2=80=94=20Codeberg=20serves=20intermittent=205xx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First runs: darwin-k green in 41m (and it does fetch okf, so ssh-agent is required); nixos-nebula died twice in eval on codeberg.org 503/504 fetching snowglobe-lib — reproduced from a residential IP, a real Codeberg outage. Only the nebula eval forces that input. Add a retried `nix flake archive` step (~10 min backoff) so fetch blips stop killing the build step; if Codeberg flakiness turns chronic, mirror snowglobe-lib to GitHub instead. Claude-Session: https://claude.ai/code/session_01Si4x5wbNVNuHgh4TyU66D8 --- .github/workflows/ci.yml | 15 +++++++++++++++ knowledge/decisions/ci-github-actions.md | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cb3427e..a38d4737 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,5 +81,20 @@ jobs: mkdir -p ~/.ssh ssh-keyscan github.com >> ~/.ssh/known_hosts + # snowglobe-lib lives on codeberg.org, which intermittently serves + # 503/504 (observed 2026-07-10, from residential IPs too — not just + # datacenter blocking). Fetch all inputs with backoff (~10 min grace) + # so the build step never dies on a fetch blip. If Codeberg flakiness + # becomes chronic, the escalation is mirroring snowglobe-lib to GitHub + # and swapping the input URL. + - name: Fetch flake inputs (retried) + run: | + for i in 1 2 3 4 5; do + nix flake archive && exit 0 + echo "input fetch attempt $i failed (Codeberg down?); retrying in $((i * 60))s" >&2 + sleep $((i * 60)) + done + exit 1 + - name: Build NixOS toplevel (nebula) run: nix build .#nixosConfigurations.nebula.config.system.build.toplevel -L diff --git a/knowledge/decisions/ci-github-actions.md b/knowledge/decisions/ci-github-actions.md index f0282e0d..166a409d 100644 --- a/knowledge/decisions/ci-github-actions.md +++ b/knowledge/decisions/ci-github-actions.md @@ -63,6 +63,14 @@ observation removed the entire hard part. or drop the disk-reclaim step); the PAT's expiry (~1 year) needs a calendar note; `timeout-minutes` may need raising on uncached hyprland-bump PRs. +- First-run data (2026-07-10): `darwin-k` green in 41m8s fully uncached, + and it DID fetch the private okf input (ssh-agent is required, not + precautionary). `nixos-nebula` failed twice on **Codeberg 503/504 + fetching snowglobe-lib** — reproduced from a residential IP, i.e. a real + Codeberg outage, and only the nebula eval forces that input (darwin + never touches Codeberg). Mitigation: a retried `nix flake archive` step + (~10 min backoff) before the build; escalation if chronic: mirror + snowglobe-lib to GitHub and swap the input URL. ## Citations From 7634cb301b22d6611984e2d6bd9859c1c1485455 Mon Sep 17 00:00:00 2001 From: Kris Williams <115474+kriswill@users.noreply.github.com> Date: Sat, 11 Jul 2026 20:41:58 -0700 Subject: [PATCH 3/4] ci: push both host closures to the private FlakeHub cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the darwin job's magic-nix-cache-action (GHA cache, ~10 GiB) for DeterminateSystems/flakehub-cache-action and add it to the nebula job, so every closure CI builds lands in the private FlakeHub cache (paid Determinate account). Auth is the workflow's OIDC JWT via 'id-token: write' — FlakeHub forbids ad-hoc push, so CI is the cache's only writer and no cache secret exists; OKFLIGHT_DEPLOY_KEY stays the sole CI credential. Hosts pull with a one-time 'determinate-nixd login' (done + verified on k; pending on mini, SOC-Kris-Williams, nebula) — Determinate Nix writes the substituter, netrc, and trusted keys itself, no nix config changes. --- .github/workflows/ci.yml | 15 +++++++++++--- knowledge/decisions/ci-github-actions.md | 26 ++++++++++++++++++++---- knowledge/decisions/index.md | 2 +- knowledge/log.md | 12 +++++++++++ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a38d4737..def96e41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,12 @@ # secret pages.yml uses). Fork PRs get no secrets, so the okf fetch — and the # job — fails there; acceptable for a personal repo. # +# Both jobs push what they build to FlakeHub Cache (private; paid +# Determinate account) via flakehub-cache-action — FlakeHub forbids ad-hoc +# push, CI OIDC is the only way in, so this workflow is what populates the +# cache the hosts pull from (`determinate-nixd login` per machine). Auth is +# the job's OIDC JWT (`id-token: write`) — no FlakeHub secret exists. +# # NO signing key and NO age key in CI, ever: builds never decrypt sops # secrets (decryption happens at activation on the host), so the only # credential here is the read-only okflight deploy key. See @@ -27,6 +33,7 @@ on: permissions: contents: read + id-token: write # OIDC JWT = the FlakeHub Cache credential (absent on fork PRs) concurrency: group: ci-${{ github.ref }} @@ -41,9 +48,9 @@ jobs: - uses: DeterminateSystems/determinate-nix-action@v3 - # GHA-cache-backed store cache (~10 GiB/repo) — mainly helps the - # custom packages (ccglass, okf, tomato, nas-mount, …). - - uses: DeterminateSystems/magic-nix-cache-action@v14 + # Pushes builds to (and pulls prior builds from) the private FlakeHub + # cache — replaces magic-nix-cache's ~10 GiB GHA-cache backend. + - uses: DeterminateSystems/flakehub-cache-action@main - uses: webfactory/ssh-agent@v0.10.0 with: @@ -72,6 +79,8 @@ jobs: - uses: DeterminateSystems/determinate-nix-action@v3 + - uses: DeterminateSystems/flakehub-cache-action@main + - uses: webfactory/ssh-agent@v0.10.0 with: ssh-private-key: ${{ secrets.OKFLIGHT_DEPLOY_KEY }} diff --git a/knowledge/decisions/ci-github-actions.md b/knowledge/decisions/ci-github-actions.md index 166a409d..1e94ade9 100644 --- a/knowledge/decisions/ci-github-actions.md +++ b/knowledge/decisions/ci-github-actions.md @@ -1,8 +1,8 @@ --- type: Decision title: CI Builds Both Host Closures — No Signing Key, No Age Key, Ever -description: 'GitHub Actions builds darwinConfigurations.k.system (free arm64 macOS runner, public repo) and nebula''s NixOS toplevel on every PR, plus a weekly update-flake-lock PR via a fine-grained PAT. The load-bearing security property: builds never decrypt sops secrets, so CI''s only credential is the read-only okflight deploy key — the Developer ID signing key never touches GitHub.' -tags: [ci, security, codesigning] +description: 'GitHub Actions builds darwinConfigurations.k.system (free arm64 macOS runner, public repo) and nebula''s NixOS toplevel on every PR, pushing both closures to the private FlakeHub Cache via OIDC, plus a weekly update-flake-lock PR via a fine-grained PAT. The load-bearing security property: builds never decrypt sops secrets, so CI''s only credential is the read-only okflight deploy key — the Developer ID signing key never touches GitHub, and the cache needs no key at all.' +tags: [ci, security, codesigning, cache] timestamp: '2026-07-10T21:30:00Z' --- @@ -47,10 +47,21 @@ observation removed the entire hard part. bump PR. Chosen over Dependabot's native nix support (April 2026) because Dependabot cannot bump the private git+ssh okf input or the FlakeHub `determinate` input. +- **FlakeHub Cache push (2026-07-11):** both jobs run + `DeterminateSystems/flakehub-cache-action`, pushing every closure they + build to the private FlakeHub cache (paid Determinate account) and + pulling prior CI builds back. It replaced the darwin job's + `magic-nix-cache-action` (~10 GiB GHA-cache backend). Auth is the job's + OIDC JWT (`permissions: id-token: write`) — FlakeHub forbids ad-hoc push + by design, so this workflow is the cache's only writer and there is no + cache secret to leak; the no-new-credentials property above holds. Hosts + consume with a one-time `determinate-nixd login` per machine (Determinate + Nix auto-configures substituter, netrc, and trusted keys; pull verified + on `k` 2026-07-11 via `nix store info --store https://cache.flakehub.com`). - **Accepted cost:** hyprland/noctalia `follows` this flake's nixpkgs, so their upstream caches can never hit — every bump PR rebuilds them from - source on the nebula job. `magic-nix-cache-action` (GHA cache, ~10 GiB) - is included on the darwin job for the custom packages. + source on the nebula job, once; the FlakeHub cache then serves that build + to re-runs and to nebula itself. ## Consequences @@ -63,6 +74,11 @@ observation removed the entire hard part. or drop the disk-reclaim step); the PAT's expiry (~1 year) needs a calendar note; `timeout-minutes` may need raising on uncached hyprland-bump PRs. +- The `follows` rebuild cost moves off the machines: after a merged bump + PR, nebula's `nrs` pulls source-built Hyprland/noctalia prebuilt from + the FlakeHub cache instead of compiling locally; same for the custom + packages on the darwin hosts (`k` — and `mini`/`SOC-Kris-Williams` for + the store paths their closures share with `k`'s). - First-run data (2026-07-10): `darwin-k` green in 41m8s fully uncached, and it DID fetch the private okf input (ssh-agent is required, not precautionary). `nixos-nebula` failed twice on **Codeberg 503/504 @@ -74,6 +90,8 @@ observation removed the entire hard part. ## Citations +- [FlakeHub Cache: CI-only push, JWT auth, `determinate-nixd login` to pull](https://docs.determinate.systems/flakehub/cache/) +- [DeterminateSystems/flakehub-cache-action](https://github.com/DeterminateSystems/flakehub-cache-action) - [GitHub: workflows are not triggered by GITHUB_TOKEN events](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#triggering-a-workflow-from-a-workflow) - [DeterminateSystems/update-flake-lock](https://github.com/DeterminateSystems/update-flake-lock) - [webfactory/ssh-agent](https://github.com/webfactory/ssh-agent) diff --git a/knowledge/decisions/index.md b/knowledge/decisions/index.md index 28368e14..239e92c6 100644 --- a/knowledge/decisions/index.md +++ b/knowledge/decisions/index.md @@ -7,7 +7,7 @@ choice is made (the commit body can then simply link here). ## Concepts * [Apple container — Repackage and Wrap, Don't Build](apple-container-wrapper.md) - Apple's container CLI is repackaged from the signed .pkg (never built from source) and wrapped so its install root resolves to the store path where the plugins actually live. -* [CI Builds Both Host Closures — No Signing Key, No Age Key, Ever](ci-github-actions.md) - GitHub Actions builds darwinConfigurations.k.system (free arm64 macOS runner, public repo) and nebula's NixOS toplevel on every PR, plus a weekly update-flake-lock PR via a fine-grained PAT. The load-bearing security property: builds never decrypt sops secrets, so CI's only credential is the read-only okflight deploy key — the Developer ID signing key never touches GitHub. +* [CI Builds Both Host Closures — No Signing Key, No Age Key, Ever](ci-github-actions.md) - GitHub Actions builds darwinConfigurations.k.system (free arm64 macOS runner, public repo) and nebula's NixOS toplevel on every PR, pushing both closures to the private FlakeHub Cache via OIDC, plus a weekly update-flake-lock PR via a fine-grained PAT. The load-bearing security property: builds never decrypt sops secrets, so CI's only credential is the read-only okflight deploy key — the Developer ID signing key never touches GitHub, and the cache needs no key at all. * [Claude Profile Isolation Strategy](claude-profile-isolation.md) - The claude wrapper prefers each profile's own interactive login and uses the Keychain token only as a fallback; the desktop app is pinned via a launchd Aqua-domain setenv plus a shell scrub. * [codebase-memory-mcp via Nix-aware Fork](codebase-memory-fork.md) - The codebase-memory MCP server is consumed from the kriswill fork's nix branch (Nix symbols + flake topology, PR #19 upstream) with its index artifact kept out of git for now. * [Route All Linting and Formatting Through efm-langserver](efm-umbrella-formatting.md) - One umbrella LSP (efm) runs every CLI linter and formatter; format-on-save filters to efm only, so no two tools ever compete over a buffer. diff --git a/knowledge/log.md b/knowledge/log.md index 69c55c1c..14a8c2db 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -2,6 +2,18 @@ ## 2026-07-11 +- **Update** — [ci-github-actions](decisions/ci-github-actions.md) / + `.github/workflows/ci.yml`: both CI jobs now push the closures they build + to the private FlakeHub Cache via + `DeterminateSystems/flakehub-cache-action` (replaces the darwin job's + `magic-nix-cache-action` GHA backend). Auth is the workflow's OIDC JWT + (`id-token: write`) — FlakeHub forbids ad-hoc push, so CI is the cache's + only writer and no cache credential exists; `OKFLIGHT_DEPLOY_KEY` remains + CI's sole secret. Hosts pull with a one-time `determinate-nixd login` + (already done + verified on `k`; pending on `mini`, `SOC-Kris-Williams`, + `nebula` — Determinate Nix writes substituter/netrc/keys itself, zero nix + config changes). + - **Update** — [gh-config](packages/gh-config.md): `capture`/`diff` now normalize the YAML through yq-go (2-space indent, comments/quoting kept) instead of copying/comparing verbatim. gh versions disagree on mapping From 6b35fa0edca09d551ec2539df241ff792b0dbe01 Mon Sep 17 00:00:00 2001 From: Kris Williams <115474+kriswill@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:16:58 -0700 Subject: [PATCH 4/4] =?UTF-8?q?ci:=20drop=20the=20okflight=20deploy=20key?= =?UTF-8?q?=20everywhere=20=E2=80=94=20okf=20is=20public;=20add=20reusable?= =?UTF-8?q?=20cached-CI=20workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit okf went public with the okflight rebrand, so the flake input becomes a plain github: fetch (same pinned rev, relocked) and the deploy-key machinery (OKFLIGHT_DEPLOY_KEY, webfactory/ssh-agent, known_hosts) leaves ci.yml, update-flake-lock.yml, and pages.yml. CI now holds zero build credentials; FLAKE_UPDATE_PAT (bump PRs) is the sole repo secret. Retire the OKFLIGHT_DEPLOY_KEY secret and okflight's deploy key after merge. New .github/workflows/nix-build-cache.yml (workflow_call): any kriswill/* repo gets Determinate Nix + FlakeHub-cache CI from a one-job caller that grants id-token: write — the cache is account-scoped, so there is no per-repo registration or secret. --- .github/workflows/ci.yml | 33 ++++--------------- .github/workflows/nix-build-cache.yml | 41 ++++++++++++++++++++++++ .github/workflows/pages.yml | 5 ++- .github/workflows/update-flake-lock.yml | 13 ++------ AGENTS.md | 2 +- flake.lock | 12 +++---- flake.nix | 8 ++--- knowledge/decisions/ci-github-actions.md | 25 ++++++++++----- knowledge/log.md | 11 +++++++ 9 files changed, 90 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/nix-build-cache.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index def96e41..40f08b3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,20 +3,17 @@ # nebula's NixOS toplevel (x86_64-linux). Deliberately NOT `nix flake check`: # the darwin checks would build all three darwin hosts; the gate is k only. # -# Both jobs run ssh-agent because the flake has a private git+ssh input -# (okf → kriswill/okflight; read-only deploy key, the same OKFLIGHT_DEPLOY_KEY -# secret pages.yml uses). Fork PRs get no secrets, so the okf fetch — and the -# job — fails there; acceptable for a personal repo. -# # Both jobs push what they build to FlakeHub Cache (private; paid # Determinate account) via flakehub-cache-action — FlakeHub forbids ad-hoc # push, CI OIDC is the only way in, so this workflow is what populates the # cache the hosts pull from (`determinate-nixd login` per machine). Auth is -# the job's OIDC JWT (`id-token: write`) — no FlakeHub secret exists. +# the job's OIDC JWT (`id-token: write`) — no FlakeHub secret exists. Fork +# PRs get no id-token, so they build without cache push (warns, still green). # -# NO signing key and NO age key in CI, ever: builds never decrypt sops -# secrets (decryption happens at activation on the host), so the only -# credential here is the read-only okflight deploy key. See +# CI holds ZERO build credentials: every flake input is a public fetch (okf +# went public 2026-07, dropping the old git+ssh deploy key), and builds +# never decrypt sops secrets (decryption happens at activation on the +# host). NO signing key and NO age key in CI, ever. See # knowledge/decisions/ci-github-actions.md. # # No hyprland.cachix.org substituter: hyprland/noctalia follow this flake's @@ -52,15 +49,6 @@ jobs: # cache — replaces magic-nix-cache's ~10 GiB GHA-cache backend. - uses: DeterminateSystems/flakehub-cache-action@main - - uses: webfactory/ssh-agent@v0.10.0 - with: - ssh-private-key: ${{ secrets.OKFLIGHT_DEPLOY_KEY }} - - - name: Ensure github.com in known_hosts - run: | - mkdir -p ~/.ssh - ssh-keyscan github.com >> ~/.ssh/known_hosts - - name: Build darwin system closure (k) run: nix build .#darwinConfigurations.k.system -L @@ -81,15 +69,6 @@ jobs: - uses: DeterminateSystems/flakehub-cache-action@main - - uses: webfactory/ssh-agent@v0.10.0 - with: - ssh-private-key: ${{ secrets.OKFLIGHT_DEPLOY_KEY }} - - - name: Ensure github.com in known_hosts - run: | - mkdir -p ~/.ssh - ssh-keyscan github.com >> ~/.ssh/known_hosts - # snowglobe-lib lives on codeberg.org, which intermittently serves # 503/504 (observed 2026-07-10, from residential IPs too — not just # datacenter blocking). Fetch all inputs with backoff (~10 min grace) diff --git a/.github/workflows/nix-build-cache.yml b/.github/workflows/nix-build-cache.yml new file mode 100644 index 00000000..c5bbbf51 --- /dev/null +++ b/.github/workflows/nix-build-cache.yml @@ -0,0 +1,41 @@ +# Reusable account-wide Nix CI: build with Determinate Nix, push everything +# built to the private FlakeHub cache. Any kriswill/* repo gets cached Nix CI +# with a single job: +# +# jobs: +# nix: +# uses: kriswill/dotfiles/.github/workflows/nix-build-cache.yml@main +# permissions: { id-token: write, contents: read } +# # optionally: +# # with: { command: "nix build .#foo -L", runner: macos-latest } +# +# The caller MUST grant `id-token: write` — that OIDC JWT is the FlakeHub +# credential (account-scoped cache; no secrets exist). Fork PRs get no +# id-token and build without cache push. dotfiles' own ci.yml does not use +# this (its jobs carry host-specific extras); it exists for the other repos. +name: nix-build-cache + +on: + workflow_call: + inputs: + command: + description: Nix command(s) to run + type: string + default: nix flake check -L + runner: + description: Runner image (ubuntu-latest, macos-latest, ...) + type: string + default: ubuntu-latest + +jobs: + build: + runs-on: ${{ inputs.runner }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v7 + + - uses: DeterminateSystems/determinate-nix-action@v3 + + - uses: DeterminateSystems/flakehub-cache-action@main + + - run: ${{ inputs.command }} diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6e304f4b..56bbc8d8 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -33,8 +33,8 @@ jobs: with: fetch-depth: 0 # gitISO stamps each embedded file with its last-commit date - # okf lives in the private kriswill/okflight repo; check it out at the - # exact rev the flake.lock pins (read-only deploy key: OKFLIGHT_DEPLOY_KEY). + # okf lives in the (public) kriswill/okflight repo; check it out at the + # exact rev the flake.lock pins. - name: Read pinned okf rev id: okf run: echo "rev=$(jq -r '.nodes.okf.locked.rev' flake.lock)" >> "$GITHUB_OUTPUT" @@ -43,7 +43,6 @@ jobs: with: repository: kriswill/okflight ref: ${{ steps.okf.outputs.rev }} - ssh-key: ${{ secrets.OKFLIGHT_DEPLOY_KEY }} path: okflight - uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml index f9aa78c2..dca6d462 100644 --- a/.github/workflows/update-flake-lock.yml +++ b/.github/workflows/update-flake-lock.yml @@ -3,8 +3,8 @@ # runs workflows on events created with GITHUB_TOKEN. PAT scope: this repo # only, Contents R/W + Pull requests R/W. # -# ssh-agent is REQUIRED here: `nix flake update` re-fetches every input, -# including the private git+ssh okf (kriswill/okflight, read-only deploy key). +# `nix flake update` re-fetches every input; all of them are public fetches +# (okf went public 2026-07), so no ssh key or other credential is needed. name: update-flake-lock on: @@ -24,15 +24,6 @@ jobs: - uses: DeterminateSystems/determinate-nix-action@v3 - - uses: webfactory/ssh-agent@v0.10.0 - with: - ssh-private-key: ${{ secrets.OKFLIGHT_DEPLOY_KEY }} - - - name: Ensure github.com in known_hosts - run: | - mkdir -p ~/.ssh - ssh-keyscan github.com >> ~/.ssh/known_hosts - - uses: DeterminateSystems/update-flake-lock@v28 with: token: ${{ secrets.FLAKE_UPDATE_PAT }} diff --git a/AGENTS.md b/AGENTS.md index aadb9c6b..08340e91 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -256,4 +256,4 @@ rather than appending contradictions. `knowledge/` is the repo's authored knowledge layer — an [OKF v0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) bundle of architecture patterns, decision records, playbooks, and a scaffolded catalog of every module/package/host/nvim-plugin, cross-linked into a graph. Conventions: `knowledge/okf-profile.md`. Start reading at `knowledge/index.md` (each directory's `index.md` discloses one level at a time). -**Keep it current as part of any change** (the `knowledge-bundle` skill has the full procedure): after adding a module/package/host/nvim-plugin run `okf scaffold` + `okf index`; record non-obvious decisions in `knowledge/decisions/`; append `knowledge/log.md`; `okf validate` must exit 0 before committing. `okf viz` renders an interactive graph at `knowledge/viz.html` (gitignored). The `okf` command is on the dev-shell PATH (`modules/dev.nix`), nix-built from okf's own repo (`kriswill/okflight`, private — fetched over git+ssh, so evaluation needs a GitHub-authorized SSH key on the agent; here that's the 1Password agent); outside the dev shell use `nix run .#okf -- `. +**Keep it current as part of any change** (the `knowledge-bundle` skill has the full procedure): after adding a module/package/host/nvim-plugin run `okf scaffold` + `okf index`; record non-obvious decisions in `knowledge/decisions/`; append `knowledge/log.md`; `okf validate` must exit 0 before committing. `okf viz` renders an interactive graph at `knowledge/viz.html` (gitignored). The `okf` command is on the dev-shell PATH (`modules/dev.nix`), nix-built from okf's own repo (`kriswill/okflight`, public — a plain `github:` input, no SSH key needed); outside the dev shell use `nix run .#okf -- `. diff --git a/flake.lock b/flake.lock index 43866700..19c2685b 100644 --- a/flake.lock +++ b/flake.lock @@ -1022,15 +1022,15 @@ "locked": { "lastModified": 1783296211, "narHash": "sha256-/wMDd8apCYg4txBcN9PLGNCZ0yLOPgo2Phv3Zuvgj0g=", - "ref": "refs/heads/main", + "owner": "kriswill", + "repo": "okflight", "rev": "f318be0ba0e8345536907941938b8bca04fbdd0b", - "revCount": 38, - "type": "git", - "url": "ssh://git@github.com/kriswill/okflight.git" + "type": "github" }, "original": { - "type": "git", - "url": "ssh://git@github.com/kriswill/okflight.git" + "owner": "kriswill", + "repo": "okflight", + "type": "github" } }, "pre-commit-hooks": { diff --git a/flake.nix b/flake.nix index 9ac0de6b..3abf9220 100644 --- a/flake.nix +++ b/flake.nix @@ -43,11 +43,11 @@ inputs.nixpkgs.follows = "nixpkgs"; inputs.flake-parts.follows = "flake-parts"; }; - # okf lives in its own (private, for now) repo, fetched over git+ssh so - # auth rides the SSH agent (here: 1Password, enclave-gated per use — no - # token at rest). Evaluation must run as the key-holding user (nh does). + # okf lives in its own (public since 2026-07) repo; plain github: fetch — + # no SSH agent or deploy key needed anywhere (machines or CI). If it ever + # goes private again: git+ssh + deploy key, or a private FlakeHub flake. okf = { - url = "git+ssh://git@github.com/kriswill/okflight.git"; + url = "github:kriswill/okflight"; inputs.nixpkgs.follows = "nixpkgs"; inputs.flake-parts.follows = "flake-parts"; }; diff --git a/knowledge/decisions/ci-github-actions.md b/knowledge/decisions/ci-github-actions.md index 1e94ade9..6b5cf85f 100644 --- a/knowledge/decisions/ci-github-actions.md +++ b/knowledge/decisions/ci-github-actions.md @@ -1,7 +1,7 @@ --- type: Decision title: CI Builds Both Host Closures — No Signing Key, No Age Key, Ever -description: 'GitHub Actions builds darwinConfigurations.k.system (free arm64 macOS runner, public repo) and nebula''s NixOS toplevel on every PR, pushing both closures to the private FlakeHub Cache via OIDC, plus a weekly update-flake-lock PR via a fine-grained PAT. The load-bearing security property: builds never decrypt sops secrets, so CI''s only credential is the read-only okflight deploy key — the Developer ID signing key never touches GitHub, and the cache needs no key at all.' +description: 'GitHub Actions builds darwinConfigurations.k.system (free arm64 macOS runner, public repo) and nebula''s NixOS toplevel on every PR, pushing both closures to the private FlakeHub Cache via OIDC, plus a weekly update-flake-lock PR via a fine-grained PAT. The load-bearing security property: builds never decrypt sops secrets and every flake input is a public fetch (okf went public 2026-07), so CI holds zero build credentials — the Developer ID signing key never touches GitHub, and the cache needs no key at all.' tags: [ci, security, codesigning, cache] timestamp: '2026-07-10T21:30:00Z' --- @@ -32,13 +32,22 @@ observation removed the entire hard part. (nebula's closure: gaming profile, nvidia, source-built Hyprland). Deliberately not `nix flake check` — that would build all three darwin hosts; the gate is the two machines actually deployed. -- **CI's only credential is `OKFLIGHT_DEPLOY_KEY`** (pre-existing read-only - deploy key for the private `git+ssh` okf input, loaded via - `webfactory/ssh-agent`; same secret pages.yml uses). **No signing key and - no age key in CI ever**: a compromised workflow, malicious PR, or - exfiltrated secret store cannot leak what was never there. Fork PRs get - no secrets and simply fail on the okf fetch — acceptable for a personal - repo. +- **CI holds zero build credentials (since 2026-07-11):** okf went public, + so its input became `github:kriswill/okflight` and the read-only deploy + key (`OKFLIGHT_DEPLOY_KEY` + `webfactory/ssh-agent` + known_hosts steps) + was dropped from all three workflows (ci, update-flake-lock, pages — + retire the secret and the okflight deploy key once this merges). **No + signing key and no age key in CI ever**: a compromised workflow, + malicious PR, or exfiltrated secret store cannot leak what was never + there. Fork PRs now build fine; lacking `id-token`, they only lose the + cache push. `FLAKE_UPDATE_PAT` (bump PRs) is the sole remaining secret. +- **Account-wide caching via a reusable workflow** + (`.github/workflows/nix-build-cache.yml`, `workflow_call`): any + kriswill/* repo gets Determinate Nix + FlakeHub cache CI with a one-job + caller granting `id-token: write` — the cache is account-scoped, so no + per-repo registration or secret exists. flake-explorer and okflight + wired 2026-07-11 (okflight builds on both ubuntu and arm64 macOS because + this flake consumes okf on x86_64-linux and aarch64-darwin). - **`update-flake-lock.yml`** (weekly cron + dispatch): `DeterminateSystems/update-flake-lock@v28` opens the bump PR with a fine-grained PAT (`FLAKE_UPDATE_PAT`, this repo only, Contents R/W + diff --git a/knowledge/log.md b/knowledge/log.md index 14a8c2db..1e63907a 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -2,6 +2,17 @@ ## 2026-07-11 +- **Update** — [ci-github-actions](decisions/ci-github-actions.md) / + `flake.nix` / all three workflows: okf turned out to be PUBLIC (flipped + with the okflight rebrand), so its input became `github:kriswill/okflight` + and the deploy-key machinery (`OKFLIGHT_DEPLOY_KEY`, ssh-agent, + known_hosts) left ci.yml, update-flake-lock.yml, and pages.yml — CI now + holds zero build credentials (FLAKE_UPDATE_PAT for bump PRs is the sole + secret; retire the deploy key + secret after merge). Added the reusable + `nix-build-cache.yml` (`workflow_call`): one-job callers give any + kriswill/* repo Determinate Nix + FlakeHub-cached CI (account-scoped + OIDC, no per-repo setup); flake-explorer and okflight wired the same day. + - **Update** — [ci-github-actions](decisions/ci-github-actions.md) / `.github/workflows/ci.yml`: both CI jobs now push the closures they build to the private FlakeHub Cache via