Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions .github/workflows/nix-build-cache.yml
Original file line number Diff line number Diff line change
@@ -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 }}
5 changes: 2 additions & 3 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/update-flake-lock.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`, privatefetched 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 -- <cmd>`.
**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`, publica plain `github:` input, no SSH key needed); outside the dev shell use `nix run .#okf -- <cmd>`.
12 changes: 6 additions & 6 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
};
Expand Down
110 changes: 110 additions & 0 deletions knowledge/decisions/ci-github-actions.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions knowledge/decisions/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading