--- id: IRP-WP-0001 type: workplan title: "ihp-railiance-probe — Full Pipeline Validation" domain: stack repo: ihp-railiance-probe status: done owner: tegwick created: "2026-05-02" updated: "2026-05-07" state_hub_workstream_id: "dce43c1c-1a4c-4b8d-aeb1-7755c9243a38" --- # ihp-railiance-probe — Full Pipeline Validation ## Goal Stand up a minimal IHP application that successfully traverses the complete build-to-production cycle: `nix build` on haskelseed → OCI push to Gitea registry → Helm deploy to Railiance01 → live HTTP response. The probe carries one Hspec integration test to prove the test-first loop is closed. ## Background `inter-hub` exposed two GHC 9.10.3 production-build bugs and an unvalidated deployment pipeline. This probe exercises the same stack on a trivially small codebase (one schema table, one controller, one test) so failures are cheap to diagnose. See `INTENT.md` and `DeploymentBlueprint.md` for full context. **Key hard-won knowledge going in:** - `flake.nix` must carry the ActualTypes.hs export-list rewrite overlay to prevent `.hi` overflow (Bug 1). - `libHSghc-9.10.3-5702.a` on haskelseed may need a one-time patch if the full 289 MB archive isn't already in place (Bug 2); check before build. - `GHCRTS=-A32m -M2g` and `-j1` are mandatory on the 2-CPU/3.8 GB host. --- ## Tasks ### T01 — Adopt flake.nix from inter-hub baseline ```task id: IRP-WP-0001-T01 status: done priority: high state_hub_task_id: "dfaeada0-97fd-454a-99c6-98186419cbc9" ``` Copy `flake.nix` from `inter-hub` as the starting point and strip it down to the probe's minimal package set: 1. Copy `inter-hub/flake.nix` → `ihp-railiance-probe/flake.nix` 2. Change `appName` to `"ihp-railiance-probe"` 3. Remove packages not needed: `http-conduit`, `aeson`, `string-conversions`, `cryptohash-sha256`, `base16-bytestring`, `random-bytestring`, `yaml`, `network-uri` (add back only as features require them) 4. Keep the inter-hub-models `configureFlags` and `postUnpack` overlay verbatim — these fix GHC 9.10.3 Bug 1 and are needed regardless of module count 5. Remove the `inter-hub-lib` overlay (it was a workaround that was superseded; confirm it is absent from inter-hub's current flake before copying) 6. Commit the flake **Exit criteria:** `nix flake check` passes (or `--no-build` if check is slow). --- ### T02 — Minimal IHP project scaffold ```task id: IRP-WP-0001-T02 status: done priority: high state_hub_task_id: "d982d0a1-ba48-481f-bb2e-dfc0126d36d9" ``` Bootstrap the IHP project skeleton inside the repo: 1. Verify Determinate Nix + `ihp-new` are available on the workstation 2. If the repo is empty (only README/LICENSE), run: ```bash cd /home/worsch/ihp-railiance-probe ihp-new . --name ihp-railiance-probe # or copy scaffold from inter-hub ``` Alternatively: copy the IHP scaffold from inter-hub and strip everything down to bare bones (single-table schema, no domain modules). 3. Confirm `devenv up` starts: app on `:8000`, Postgres managed by Nix 4. Commit baseline scaffold **Exit criteria:** `devenv up` succeeds; `http://localhost:8000` returns IHP welcome page or a minimal home view. --- ### T03 — Minimal schema: `probes` table ```task id: IRP-WP-0001-T03 status: done priority: high state_hub_task_id: "675210b9-0e4e-4c08-882a-09ccc19c1854" ``` Define one schema table in `Application/Schema.sql`: ```sql CREATE TABLE probes ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL, name TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL ); ``` Steps: 1. Add table definition to `Application/Schema.sql` 2. Run `migrate` inside `devenv shell` 3. Trigger IHP code generation (IHP IDE at `:8001` → Schema tab → regenerate, or `build-generated-code` inside devenv) 4. Commit migration + generated code **Exit criteria:** `probes` table exists; generated `Generated/Types.hs` and `Generated/ActualTypes.hs` are present in `build/`; `devenv up` still compiles. --- ### T04 — Health endpoint controller ```task id: IRP-WP-0001-T04 status: done priority: high state_hub_task_id: "4fba285f-ca9c-4f53-8474-d418ab8b4e1b" ``` Add a minimal `/healthz` route that returns `200 OK` with body `"ok"`: 1. Add route in `Web/Routes.hs`: ```haskell instance CanRoute HealthController where parseRoute' = do pathPrefix "/healthz" pure HealthAction ``` 2. Add `HealthController` to `Web/Types.hs` 3. Implement controller in `Web/Controller/Health.hs`: ```haskell action HealthAction = renderPlain "ok" ``` 4. Wire into `Web/FrontController.hs` 5. Verify `curl http://localhost:8000/healthz` → `ok` 6. Commit **Exit criteria:** `/healthz` returns `200 ok` in devenv. --- ### T05 — First Hspec integration test ```task id: IRP-WP-0001-T05 status: done priority: high state_hub_task_id: "0555d8ec-3179-4f49-9e80-c1a43d37c7fa" ``` Write the test *before* adding any Probes CRUD (test-first proof): 1. Add `Test/ProbeControllerSpec.hs`: ```haskell module Test.ProbeControllerSpec where import Test.Hspec import IHP.HSpec spec :: Spec spec = describe "ProbeController" $ do it "GET /probes returns 200" $ do response <- get "/probes" response `shouldRespondWith` 200 ``` 2. Wire into `Test/Main.hs` 3. Run `test` in devenv — test should **fail** (no `/probes` route yet) 4. Implement minimal `ProbesController` with `index` action returning an empty list 5. Run `test` again — should pass 6. Commit both the test and the controller together **Exit criteria:** `test` exits 0; test report shows ProbeController spec green. --- ### T06 — Production build on haskelseed ```task id: IRP-WP-0001-T06 status: done priority: high state_hub_task_id: "6007fdde-3b03-4c7a-ae4e-2106cadd6a56" ``` First `nix build .#docker` on haskelseed for the probe: **Pre-build checklist:** ```bash # 1. Verify libHSghc-9.10.3-5702.a is full (should be 289,295,782 bytes) wc -c /nix/store/ffg3yf2ypnbz3hc31y7nglrkihz0if01-ghc-9.10.3/lib/ghc-9.10.3/lib/x86_64-linux-ghc-9.10.3/ghc-9.10.3-5702/libHSghc-9.10.3-5702.a # If ~287 MB, apply archive patch before proceeding (see HaskellVibePrimer.md §Bug 2) # 2. Ensure source is on haskelseed scp flake.nix + source tree → root@192.168.178.135:/root/ihp-railiance-probe/ # or: git push + git pull on haskelseed ``` Build steps: ```bash sshpass -p 'hcs26!x' ssh root@192.168.178.135 \ 'cd /root/ihp-railiance-probe && nix build .#docker --log-format raw \ > /tmp/probe-build01.log 2>&1 &' ``` Monitor with tail; expect 30-50 min on first build (no cache). **Exit criteria:** `result` symlink present on haskelseed; `nix log` shows no errors. --- ### T07 — Push OCI image to Gitea registry ```task id: IRP-WP-0001-T07 status: done priority: medium state_hub_task_id: "24b892fa-2a81-4606-b7a8-20e493c89441" ``` Push the built image to the Gitea container registry. **Note:** Gitea's registry token realm is misconfigured — it points to `gitea.coulomb.social:80` but Gitea runs on port 32166. Pre-fetch the token manually and pass it with `--dest-registry-token` to bypass the broken token dance (no `iptables` on haskelseed's Alpine to redirect ports): ```bash sshpass -p 'hcs26!x' ssh root@192.168.178.135 bash <<'EOF' cd /root/ihp-railiance-probe SHA=$(git rev-parse --short HEAD) SKOPEO=/nix/store/fwdagky9lfsyrgzxiq14zijcziazfdsn-skopeo-1.22.2/bin/skopeo TOKEN=$(curl -s \ "http://92.205.130.254:32166/v2/token?service=container_registry&scope=repository:coulomb/ihp-railiance-probe:push,pull" \ -u 'tegwick:' | awk -F'"' '/token/{print $4}') $SKOPEO copy --insecure-policy --dest-tls-verify=false \ --dest-registry-token "$TOKEN" \ docker-archive:result \ docker://92.205.130.254:32166/coulomb/ihp-railiance-probe:$SHA EOF ``` Verify via the registry API: ```bash TOKEN=$(curl -s "http://92.205.130.254:32166/v2/token?service=container_registry&scope=repository:coulomb/ihp-railiance-probe:pull" \ -u 'tegwick:' | awk -F'"' '/token/{print $4}') curl -s -H "Authorization: Bearer $TOKEN" \ "http://92.205.130.254:32166/v2/coulomb/ihp-railiance-probe/tags/list" ``` **Exit criteria:** `skopeo inspect` succeeds; image visible in Gitea Packages UI. --- ### T08 — Helm chart ```task id: IRP-WP-0001-T08 status: done priority: medium state_hub_task_id: "06eb278b-8ff6-4123-a2e3-856dc44ed275" ``` Create a minimal Helm chart in `chart/`: ``` chart/ Chart.yaml # name: ihp-railiance-probe, version: 0.1.0 values.yaml # image.repository, image.tag, env vars templates/ deployment.yaml # single replica, port 8000, envFrom secretRef service.yaml # ClusterIP, port 80 → 8000 ingress.yaml # Traefik IngressRoute or standard Ingress secret.yaml # IHP_SESSION_SECRET, DATABASE_URL, IHP_BASEURL ``` Key `deployment.yaml` notes: - Image: `{{ .Values.image.repository }}:{{ .Values.image.tag }}` - Repository default: `92.205.130.254:32166/coulomb/ihp-railiance-probe` - `imagePullPolicy: Always` - Resource limits: `memory: 256Mi`, `cpu: 200m` (probe is small) - Liveness probe: `GET /healthz` after 30s initialDelay Commit the chart. **Exit criteria:** `helm lint chart/` passes. --- ### T09 — k3s registry configuration on Railiance01 ```task id: IRP-WP-0001-T09 status: done priority: medium state_hub_task_id: "97f50f12-3511-408f-a049-9300241344b8" ``` Configure k3s to pull from the HTTP (non-TLS) Gitea registry: ```bash ssh railiance01 sudo cat /etc/rancher/k3s/registries.yaml # If not present or missing the mirror entry, add: ``` ```yaml mirrors: "92.205.130.254:32166": endpoint: - "http://92.205.130.254:32166" ``` ```bash sudo systemctl restart k3s ``` Verify: `sudo k3s crictl pull 92.205.130.254:32166/coulomb/ihp-railiance-probe:` **Exit criteria:** image pulls successfully on Railiance01. --- ### T10 — Deploy to Railiance01 ```task id: IRP-WP-0001-T10 status: done priority: medium state_hub_task_id: "7c79c843-5497-4d88-985d-a915145691cd" ``` Deploy the probe to the `coulomb` namespace: ```bash # Create namespace if not present kubectl --context railiance01 create namespace coulomb --dry-run=client -o yaml | kubectl apply -f - # Create/update secret kubectl --context railiance01 -n coulomb create secret generic ihp-railiance-probe-env \ --from-literal=IHP_SESSION_SECRET="$(openssl rand -base64 32)" \ --from-literal=DATABASE_URL="postgresql://..." \ --from-literal=IHP_BASEURL="https://probe.coulomb.example" \ --dry-run=client -o yaml | kubectl apply -f - # Deploy helm --kube-context railiance01 upgrade --install ihp-railiance-probe ./chart \ --namespace coulomb \ --set image.tag= ``` **Exit criteria:** ```bash kubectl -n coulomb get pods | grep ihp-railiance-probe # Running kubectl -n coulomb logs deploy/ihp-railiance-probe | tail -5 # IHP startup curl http:///healthz # ok ``` --- ### T11 — End-to-end smoke test ```task id: IRP-WP-0001-T11 status: done priority: medium state_hub_task_id: "cb2b1ee7-6c54-48b5-8cd8-b4fcb8fa07ab" ``` Verify the full pipeline produced a live application: 1. `GET /healthz` → `200 ok` from outside the cluster (via Ingress or NodePort) 2. `GET /probes` → `200` (empty list, no crash) 3. No panic/crash in pod logs within 60 seconds of startup 4. Document the verified SHA and timestamp in a `PIPELINE_LOG.md` entry: ``` | 2026-05-02 | | Build: haskelseed | Push: 92.205.130.254:32166 | Deploy: Railiance01 | Smoke: PASS | ``` **Exit criteria:** All three HTTP checks pass; log entry committed. --- ### T12 — Gitea Actions CI (optional, Phase 2) ```task id: IRP-WP-0001-T12 status: blocked priority: low state_hub_task_id: "0bf9c616-54e6-48f3-ae6d-d91a9f7517c9" ``` Automate the build → push → deploy pipeline via Gitea Actions: 1. Register haskelseed as a Gitea Actions runner: ```bash # On haskelseed: act_runner register --instance http://92.205.130.254:32166 --token --name haskelseed act_runner daemon & ``` 2. Create `.gitea/workflows/build-and-deploy.yml`: ```yaml on: [push] jobs: build: runs-on: haskelseed steps: - uses: actions/checkout@v3 - run: nix build .#docker --log-format raw - run: | SHA=$(git rev-parse --short HEAD) skopeo copy docker-archive:result \ docker://92.205.130.254:32166/coulomb/ihp-railiance-probe:$SHA - run: | SHA=$(git rev-parse --short HEAD) helm upgrade --install ihp-railiance-probe ./chart \ --namespace coulomb --set image.tag=$SHA ``` 3. Trigger a push; verify pipeline runs end-to-end **Exit criteria:** CI pipeline runs without manual intervention on each push to `main`. --- ## Exit Criteria Summary | Task | Check | Status | |------|-------|--------| | T01 | flake.nix with overlay from inter-hub | done | | T02 | `devenv up` → IHP welcome page | done | | T03 | `probes` table in DB; code-gen passes | done | | T04 | `/healthz` returns `200 ok` | done | | T05 | Hspec `test` exits 0 | done | | T06 | `nix build .#docker` on haskelseed succeeds | done | | T07 | Image visible in Gitea registry | done | | T08 | `helm lint chart/` passes | done | | T09 | k3s can pull from HTTP registry | done | | T10 | Pod Running on Railiance01 | done | | T11 | Smoke tests pass; log entry committed | done | | T12 | CI pipeline automated (optional) | todo |