From fef0e82d268162e330f96fb5d0c1fc3b4a468eba Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Tue, 2 Jun 2026 07:14:15 -0700 Subject: [PATCH] ci: run build entirely through Dagger; drop prek from CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Forgejo k8s job image is a thin Alpine + Dagger orchestrator (no native Rust/Neovim toolchain, no prek) with a DinD sidecar — so CI was failing on step 1 (`prek: command not found`) on every run, and a native cargo hook wouldn't work there either. - build.yaml now runs `dagger call check` (cargo fmt/clippy/test on rust:1-bookworm) + `dagger call test-nvim` (build hephd + headless e2e). - New Dagger `check` function; CARGO_BUILD_JOBS capped on both functions so parallel rustc on heavy crates doesn't OOM the build engine; cargo registry + target caches shared across runs. - prek is intentionally not run in CI — it runs locally via git hooks. Co-Authored-By: Claude Opus 4.8 (1M context) --- .dagger/src/hephaestus_ci/main.py | 38 +++++++++++++++++++++++++++++++ .forgejo/workflows/build.yaml | 36 ++++++++++------------------- docs/reference/tech-spec.md | 2 +- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/.dagger/src/hephaestus_ci/main.py b/.dagger/src/hephaestus_ci/main.py index 1833f05..091a1e7 100644 --- a/.dagger/src/hephaestus_ci/main.py +++ b/.dagger/src/hephaestus_ci/main.py @@ -9,6 +9,43 @@ NVIM_VERSION = "v0.11.2" @object_type class HephaestusCi: + @function + async def check(self, src: dagger.Directory) -> str: + """Rust workspace checks: rustfmt, clippy (-D warnings), and the full + test suite (tech-spec §9). Runs on `rust:1-bookworm` (glibc + a C + toolchain for rusqlite's bundled SQLite); the Alpine job image only + orchestrates `dagger call`. Shares cargo caches with `test_nvim`. + """ + return await ( + dag.container() + .from_("rust:1-bookworm") + .with_exec(["rustup", "component", "add", "clippy", "rustfmt"]) + # Cap parallel rustc — unbounded (= ncpu) spikes memory on heavy + # crates and OOMs the build engine on a many-core runner. + .with_env_variable("CARGO_BUILD_JOBS", "4") + .with_mounted_cache( + "/usr/local/cargo/registry", + dag.cache_volume("heph-cargo-registry"), + ) + .with_directory("/workspace", src) + .with_workdir("/workspace") + .with_mounted_cache("/workspace/target", dag.cache_volume("heph-target")) + .with_exec(["cargo", "fmt", "--all", "--check"]) + .with_exec( + [ + "cargo", + "clippy", + "--all-targets", + "--all-features", + "--", + "-D", + "warnings", + ] + ) + .with_exec(["cargo", "test", "--all"]) + .stdout() + ) + @function async def test_nvim(self, src: dagger.Directory) -> str: """Run the heph.nvim headless e2e suite (tech-spec §9). @@ -46,6 +83,7 @@ class HephaestusCi: ] ) .with_env_variable("PATH", "/opt/nvim/bin:$PATH", expand=True) + .with_env_variable("CARGO_BUILD_JOBS", "4") .with_directory("/workspace", src) .with_workdir("/workspace") .with_mounted_cache("/workspace/target", dag.cache_volume("heph-target")) diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index 3ff12f4..ae265e7 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -1,19 +1,15 @@ # Build Workflow # -# Generic CI validation for template-based repositories. -# By default this runs the repo's prek checks, which already cover: -# - workflow linting -# - docs integrity checks -# - formatting/linting hooks configured by the project +# CI validation, run entirely through Dagger. The Forgejo job image is a thin +# Alpine + Dagger orchestrator (no Rust/Neovim toolchain), so all build and test +# work happens in Dagger containers (.dagger/src/hephaestus_ci/main.py): # -# Projects can extend this with an optional executable hook at: -# .forgejo/scripts/build +# - check — cargo fmt --check, clippy -D warnings, cargo test --all +# - test-nvim — build hephd + run the headless heph.nvim e2e suite # -# The optional hook is the place for project-specific validation such as: -# - unit/integration tests -# - package builds -# - schema validation -# - language-specific linters +# prek is intentionally not run here — it runs locally via git hooks +# (`prek install`). Dagger uses the DinD sidecar; both functions share cargo +# cache volumes across runs. name: Build @@ -30,20 +26,12 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Run repository checks + - name: Rust checks (Dagger) run: | - prek run --all-files + echo "Running cargo fmt/clippy/test via Dagger..." + dagger call check --src=. - - name: Run project-specific build hook - run: | - if [ -x .forgejo/scripts/build ]; then - echo "Running project-specific build hook..." - ./.forgejo/scripts/build - else - echo "No .forgejo/scripts/build hook found; template validation complete." - fi - - - name: Run heph.nvim e2e (Dagger) + - name: heph.nvim e2e (Dagger) run: | echo "Running headless heph.nvim e2e suite via Dagger..." dagger call test-nvim --src=. diff --git a/docs/reference/tech-spec.md b/docs/reference/tech-spec.md index 4f80f89..4f1536a 100644 --- a/docs/reference/tech-spec.md +++ b/docs/reference/tech-spec.md @@ -344,7 +344,7 @@ See [[design]] §5–§7 for the constraints later phases impose on present choi - ✅ **Hub auth — verification side (§13, slice 10a):** the hub validates an **OIDC bearer token** (`jsonwebtoken`, RS256-pinned, exact iss+aud, exp/nbf, required `sub`; JWKS discovered + cached, refetched on unknown `kid`) on `/sync/*` + `/rpc`. A [`TokenVerifier`] trait seam keeps it mockable; **single-tenant** owner gate (`authorize_owner_sub`: claim-on-first, then require-match → 403 for any other identity). `--oidc-issuer`/`--oidc-audience` enable it (open when unset, for local dev). Tested fully offline: stub-verifier middleware tests + an adversarial battery against an in-process mock IdP (expired/wrong-iss/wrong-aud/unknown-kid/tampered/alg-confusion/missing-sub all rejected). - ✅ **Auth — client side (§13, slice 10b):** OAuth2 **device-code flow** (`hephd::oauth::DeviceFlow`: discover → start → poll handling `authorization_pending`/`slow_down`, + refresh). `TokenStore` (OS keyring via `keyring`, in-memory for tests); `current_bearer` refreshes on expiry. `heph auth login` runs the flow + caches the token; spokes (`sync_once`) and `client` mode (`RemoteStore`) attach the bearer, refreshing as needed (`--oidc-issuer`/`--oidc-client-id`). **All auth/proxy HTTP uses `ureq`** (runtime-free blocking) — `reqwest::blocking` panics inside the daemon's `spawn_blocking`; async `reqwest` remains only for `sync_once`. Tested offline against a mock OAuth server (device flow, refresh, store) + a full spoke⇄authed-hub loop. - ✅ **CLI (§1):** `heph` next/task/doc/get/export/search/journal/**auth login·logout**. -- ✅ **CI (§9):** `.forgejo/scripts/build` runs fmt/clippy/test (self-bootstrapping rustup). +- ✅ **CI (§9):** Forgejo `build.yaml` runs **entirely through Dagger** (the k8s job image is a thin Alpine + Dagger orchestrator with a DinD sidecar — no native Rust/nvim toolchain): `dagger call check` (cargo fmt/clippy/test on `rust:1-bookworm`) + `dagger call test-nvim` (build hephd + headless e2e). Cargo parallelism capped (`CARGO_BUILD_JOBS`) to avoid OOMing the build engine; cargo caches shared across runs. `prek` runs locally via git hooks, not in CI. - ✅ **`heph.nvim` slice 11a (§8) — the primary surface begins:** the Lua plugin (`heph.nvim/`) as a thin client of the `hephd` unix socket. **RPC client** over a `vim.uv` pipe (blocking `call` via `vim.wait`; id-demuxed; partial-line buffered; `luanil` so JSON `null`→`nil`; isolated `Session`s for tests). **Buffer-backed nodes** — `heph://node/` buffers (`buftype=acwrite`), `BufReadCmd`→`node.get` / `BufWriteCmd`→`node.update` (whole-buffer body, CRDT-diffed; exact round-trip). **`[[wiki-link]]` follow** on `` via a new **`node.resolve {title}`** RPC (exact alias-then-title match, the same mapping that materializes `wiki` links — never fuzzy `search`; unresolved links allowed). **Daily journal** (`:Heph today`/`journal `, idempotent). `:Heph` command surface + completion. **Headless e2e (§9):** drives the plugin in `nvim --headless` against a real daemon over a temp socket via a **self-contained busted-style runner** (`tests/e2e/runner.lua` — no external plugins/network, deterministic exit codes); specs cover journal round-trip, follow-link (+ unresolved no-op), and link-two-docs/backlink. `mise run test-nvim` builds the daemon and runs the suite (dev: system nvim/rustc; CI: a Dagger container provides them — slice 11c). - ✅ **`heph.nvim` slice 11b (§8) — task views:** **`list` enriched** to titled [`RankedTask`] rows (title + `canonical_context_id`, shared `ranked_from_row` with `next`) so the Organizational view needs no N+1 `node.get`. Plugin: **Tactical `next`** + **Organizational `list`** views (rendered scratch buffers, `` opens a row's canonical-context doc — the node autocmd narrowed to `heph://node/*` so view buffers don't trip it); **task capture**, **set-attention**, **done/drop**, **skip**, **per-task `log` append** — all resolving "the current task" from the buffer (a `task` node, or a context doc via its `canonical-context` backlink); **`vim.ui.select` picker** (`picker.lua`) with Telescope auto-upgrade; `:Heph next/list/capture/attention/done/drop/skip/log/search` subcommands. e2e specs: **capture→next→open context→add/check checklist→done**, and **recurring fresh-checklist** (complete rolls forward in place; the next occurrence is all-unchecked — the §4.4 hard requirement). - ✅ **`heph.nvim` slice 11c (§8) — promotion + Dagger CI:** backend **`task.promote {container_id, item_ref, attention?, project?}`** — mints a committed task from the `item_ref`-th `- [ ]` context item (1-based, document order via a new `extract::context_item_lines`) and rewrites that source line into a `[[link]]` to it. **Wiki-link resolution now excludes canonical-context docs** (`resolve_id`), so `[[Task Title]]` deterministically resolves to the task, not its identically-titled context doc — a general fix surfaced by promotion. Plugin: `:Heph promote`, `promote_under_cursor` (save-if-dirty → `util.context_item_index_at_cursor` mirrors extract's fence rules → `task.promote` → reload). e2e spec (f). **CI via Dagger:** a `test_nvim` function in `.dagger/` bakes a **pinned, arch-detected Neovim** (`v0.11.2`; Debian's is too old for `vim.uv`) onto `rust:1-bookworm`, builds `hephd`, and runs the shim suite (cargo + cargo-target cache volumes); `build.yaml` calls `dagger call test-nvim`. `run.lua` fails on zero-specs (no false-green) — validated end-to-end (failing spec → Dagger exit 1).