generated from coulomb/repo-seed
All checks were successful
Build and Deploy / build-push-deploy (push) Successful in 38s
Gitea Actions build-and-deploy workflow ran successfully without manual intervention: checkout → nix build → skopeo push → helm deploy all passed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
454 lines
13 KiB
Markdown
454 lines
13 KiB
Markdown
---
|
|
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:<GITEA_API_KEY>' | 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:<GITEA_API_KEY>' | 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:<SHA>`
|
|
|
|
**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=<SHA>
|
|
```
|
|
|
|
**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://<cluster-ip>/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 | <SHA> | 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: done
|
|
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 <runner-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) | done |
|