diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..40f08b3a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +# 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 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. Fork +# PRs get no id-token, so they build without cache push (warns, still green). +# +# 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 +# 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 + id-token: write # OIDC JWT = the FlakeHub Cache credential (absent on fork PRs) + +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 + + # 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 + + - 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: DeterminateSystems/flakehub-cache-action@main + + # 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/.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 new file mode 100644 index 00000000..dca6d462 --- /dev/null +++ b/.github/workflows/update-flake-lock.yml @@ -0,0 +1,31 @@ +# 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. +# +# `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: + 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: DeterminateSystems/update-flake-lock@v28 + with: + token: ${{ secrets.FLAKE_UPDATE_PAT }} + pr-title: "flake.lock: weekly update" + pr-labels: dependencies 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 new file mode 100644 index 00000000..6b5cf85f --- /dev/null +++ b/knowledge/decisions/ci-github-actions.md @@ -0,0 +1,110 @@ +--- +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 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' +--- + +**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 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 + + 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. +- **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, once; the FlakeHub cache then serves that build + to re-runs and to nebula itself. + +## 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. +- 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 + 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 + +- [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) +- [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..239e92c6 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, 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 b57a13eb..1e63907a 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -2,6 +2,29 @@ ## 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 + `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 @@ -58,6 +81,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