# CI subproject handoff — 2026-06-15 (ci-01)

First handover for the **CI / release subproject** (the `ci` branch). Kept in
`docs/ci/handoff/`, a namespace separate from the project's global
`docs/handoff/` session sequence so it can't collide with `main`'s numbering —
the same split as `docs/ci/adr/`, and needed for the same reason: `main`
independently wrote its own **handoff-70** this same day (just as it took
**ADR-0049**), which would have collided.

A dedicated infrastructure session that built the project's **entire CI/CD
pipeline** on the self-hosted Gitea Actions runner — from nothing to a live
gate plus a six-target cross-platform release. Net: the **CI** /
`requirements.md` **TT5** item and **D1**/**D2** are now done; **D3** and a
couple of TT5 tails remain. Decisions are recorded in the sibling ADR namespace
**`docs/ci/adr/`** (ADR-ci-001/002/003).

## §1. State at handoff

**Branch:** `ci` (worktree). **`main` has been merged into `ci`** (commit
`138e766`, clean — `ci` and `main` touched disjoint files) so the gate runs
against current `main` before CI lands there. Working tree clean except the
in-progress doc updates from this handoff. Pushes/promotion are the user's
step.

**Gate verified locally on the merged code:** `clippy -D warnings` clean;
**`cargo test` 2488 passing / 0 failing / 1 ignored** (the long-standing
`friendly` doctest). main's features came in with their tests (2424 → 2488).

**Pipeline (`.gitea/workflows/`):**

- `build-ci-image.yaml` — builds + pushes the CI image (`node:22-bookworm-slim`
  + single-user nix + the flake's devShell pre-warmed) to the Gitea registry.
  Triggers only on image-input changes (Dockerfile / flake / toolchain).
- `ci.yaml` — the gate: `clippy -D warnings` + `cargo test`, branch pushes + PRs
  (docs-only changes skipped).
- `release.yaml` — on a `v*` tag: `test` → `build` matrix over the **four
  non-macOS** targets via `cargo-zigbuild`, upload to the Gitea release.
- `release-macos.yaml` — **workflow_dispatch** (tag input) on the Tart
  Apple-Silicon runner (`runs-on: macos`): test → build both `*-apple-darwin`
  → de-nix `libiconv` + ad-hoc re-sign → upload.

**Verified live this session:** the 4-target release published **8 assets**
(binary + `.sha256` each) for tag `v.0.0.0-citest3`; the macOS build was proven
portable (system-only deps) + signed + launches on the runner.

## §2. What was built (and the non-obvious bits)

- **Nix flake** (ADR-ci-002, relocated from a would-be `main` ADR-0049): one
  pinned toolchain (`1.95.0`) for dev *and* CI; `cargo-zigbuild` + `zig` (Linux
  only) for the cross targets; `apple-sdk` on darwin.
- **Runner facts** (ADR-ci-001): jobs run *inside* a container (`ci-public` →
  `catthehacker/ubuntu`), so host nix is unreachable — hence the baked image.
  The Mac runner is **host execution**; its label is `macos` (`:host` in the
  registration is the act_runner backend, not part of the label).
- **Cross-compile** (ADR-ci-003): `cargo-zigbuild` for the 4 non-macOS targets.
  Windows needs an **empty `libsynchronization.a` stub** (`ci/winstub/`, wired
  via `.cargo/config.toml`) — std links `-lsynchronization`, absent from
  rust-overlay's toolchain + zig's mingw, but forwarded by `kernel32`.
- **macOS** (ADR-ci-003 amendment): built on **real Apple hardware** (Tart), so
  the SDK is fully licensed — no osxcross grey area. The darwin stdenv bakes a
  `/nix/store` `libiconv` path into the binary; the build rewrites it to
  `/usr/lib/libiconv.2.dylib` (`install_name_tool`) and re-signs ad-hoc
  (`codesign -f -s -`; `install_name_tool` invalidates the signature, arm64
  refuses unsigned). A guard fails the build on any remaining `/nix/store` dep.
- **Cache hygiene (Mac):** the runner wipes the workspace each run, so cargo
  `target/` never accumulates; the persistent nix store is bounded by
  **generation** (record the devShell in a persistent profile, keep the 2
  newest via `nix-env --delete-generations +2`, GC the rest). First sweep
  reclaimed a ~3.8 GB one-time backlog of build scaffolding (source + build-only
  deps, *not* re-installed toolchains).

## §3. Immediate next steps (user)

1. **Push `ci`** → the gate re-runs in CI (should be green; no image rebuild —
   the merge didn't touch the flake/Dockerfile).
2. **Promote:** `git checkout main && git merge ci` — a **fast-forward** (`ci`
   already contains `main`) — then push `main`. CI goes live; `release-macos`
   becomes dispatchable (workflow_dispatch needs the default branch).
3. **First real release:** tag `v0.1.0` (auto-builds the 4 Linux/Windows
   assets), then **dispatch `release-macos` for `v0.1.0`** with the Mac up (adds
   the 2 macOS assets) → a full 6-binary release.
4. **Cleanup:** delete the `v.0.0.0-citest*` test tags + their releases.
5. **Runner-side:** add `min-free`/`max-free` to the Mac's `/etc/nix/nix.conf`
   as a hands-off nix-store backstop.

## §4. Known gaps / follow-ups

- **Versioning is not wired into the binary** (flagged by the user). The release
  **git tag is nowhere in the produced binary** — there is no `--version` flag,
  no `CARGO_PKG_VERSION` use anywhere in `src/`, and the release workflows use
  the tag only for the *release name* + *asset filenames*
  (`rdbms-playground-<tag>-<target>`). `Cargo.toml` is a static `version =
  "0.1.0"`, decoupled from the tag. So a `v0.5.0` tag yields a `…-v0.5.0-…`
  asset whose binary knows nothing of "0.5.0". To fix later: add a `--version`
  flag, and inject the tag at build time (e.g. a `build.rs` reading a
  CI-provided env, or bumping `Cargo.toml` as part of tagging) so the binary and
  the release agree.
- **D3 packaging** — Homebrew / Scoop / winget / `cargo binstall` manifests
  (asset naming is already binstall-friendly).
- **TT5 tails** — Windows is build-only (no execution runner); Tier-4 PTY (TT4)
  is unwired in CI.
- **`fmt` gate** — deliberately off (tree isn't stock-`rustfmt`-clean); revisit
  on `main`.
- **Website → Cloudflare** deploy — the separate, simpler workflow, still to do.
