docs(guide): CI builds images, not you — correct deployment + release + ci-cd
CD - Develop (guide → bcos.dev) / build-and-deploy (push) Failing after 32m40s

platform.deployment and processes.release both walked the reader through ./build.sh, never
mentioning that cd-develop/cd-main already build and scan. Following either one produces an
UNSCANNED image that races CI for the mutable :dev tag. That is what happened on 2026-07-13:
a full day of hand-built images reached bcos.dev without passing Trivy.

Verified against the workflow YAML and both runners on 2026-07-14:

- cd-develop does NOT deploy for 6 of 8 repos. Only incident and devices deploy to bcos.dev;
  everywhere else CI stops at push. The old 'a green run redeploys the service' claim was wrong.
- hiveops-fleet CI builds the FRONTEND ONLY — the fleet backend is built by neither cd-develop
  nor cd-main. hiveops-devices cd-main builds only the frontend too. Flagged as defects.
- runner-02 IS active (incident built on it today) — the 'not the active worker' note was wrong.
- CI now pushes an immutable :<sha8> alongside :dev/:latest, so deploys can pin a scanned image.

Also records the runner failure signatures ('lease does not exist', 'network ... not found'),
the daemon-restart + prune runbook, the standing warning never to garbage-collect or delete
/opt/docker-registry, and the fact that merging a batch of PRs at once overwhelms a runner's
4 shared job slots and breaks its Docker daemon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 10:08:29 -04:00
parent 1727f3309b
commit 8d7450d747
3 changed files with 143 additions and 58 deletions
@@ -6,79 +6,141 @@ order: 35
audience: dev
---
> **DRAFT · internal.** Written 2026-07-01 from the runner + workflow files. CI **is enabled and
> runs on every push** — the older "CI is disabled, builds are manual" note is stale. Verify
> specifics against the workflow YAML before relying on them.
> **CI IS ENABLED and runs on every push.** Any note claiming "CI is disabled, builds are manual"
> is **stale — do not believe it.** Verified against the workflow YAML and the runners on
> **2026-07-14**.
## How CI is wired
## Do not hand-build images
Each `hiveops-*` repo has `.gitea/workflows/`. GitHub-Actions-compatible syntax, run on
self-hosted Gitea Action runners. The common set:
**Merge → let CI build → deploy the image CI produced.** Do **not** run `./build.sh` yourself.
A hand-built image **skips the security gate entirely** (npm audit + Trivy) and **races CI for the
mutable `:dev` tag**, so what ends up in the registry is nondeterministic. On 2026-07-13 a full
day's worth of images were hand-built on the false belief that CI was off; every one of them
reached bcos.dev **unscanned**.
The narrow exception — **CI is genuinely broken AND the user explicitly asks.** Confirm it is
broken with the actions API first, not from memory:
`GET /repos/hiveiq-src/<repo>/actions/tasks?limit=3`.
## What the workflows actually do
| Workflow | Trigger | Does |
|----------|---------|------|
| `cd-develop.yml` | push to `develop` | build → **Trivy** scan → **OWASP** dep-check → build+push Docker image(s) → **deploy to bcos.dev** |
| `cd-main.yml` | push to `main` | same, targeting production registry/host |
| `sonar-pr.yml` / `sonar-nightly.yml` | PR / schedule | SonarQube scan (`192.168.200.230:9000`) |
| `cd-develop.yml` | push to `develop` | `npm audit --audit-level=critical` → **Trivy** (`--exit-code 1 --severity HIGH,CRITICAL`) → `docker build` → push `:dev` **and `:<sha8>`** |
| `cd-main.yml` | push to `main` | same, against the production registry |
| `sonar-pr.yml` / `sonar-nightly.yml` | PR / schedule | SonarQube (`192.168.200.230:9000`) |
| `version-check.yml` | PR | guards the version bump |
| `ci.yml` / `build.yml` | varies | build-only (a few repos) |
`cd-develop` is **build-and-deploy** — a green run redeploys the service to bcos.dev. Java build
is validated in-pipeline and is rarely the failure point; the network-dependent steps (Trivy DB,
Sonar host, registries, the deploy SSH) are where breakage shows.
**Immutable tags (added 2026-07-14):** every build now pushes `:<sha8>` alongside `:dev`, so a
deploy can pin a **scanned** artifact instead of chasing a mutable tag. Pin `:<sha8>` in the
instance `.env` — never deploy from `:dev` or `:latest`.
## The runner — `dlx-hiveiq-runner-01`
### ⚠️ `cd-develop` does NOT deploy for most repos
- **Host:** `192.168.200.227`, SSH `dlxadmin` (key `~/.ssh/id_ed25519_dlx`). A **Proxmox VM on
`dlx-proxmox-03`** (DLX network) — the same node that gets maintained, which matters (see below).
- **Service:** `act_runner.service` (root, `WorkingDirectory=/opt/act_runner_hiveiq`,
config `config.yaml`, registration `.runner` id=18). Labels `ubuntu-latest`, `sonar`. 4 concurrent jobs.
- **Runs ALL hiveops-src jobs.** runner-02 (`.228`) is not the active worker.
- **The API `online` field reads `false` even when active** — a Gitea display quirk, ignore it;
check `journalctl` instead.
The old claim that "a green `cd-develop` run redeploys the service to bcos.dev" is **wrong for 6 of
8 repos.** Verified 2026-07-14:
### No internet — everything is pre-baked or mirrored
| Repo | `cd-develop` | Images CI builds |
|------|--------------|------------------|
| `hiveops-incident` | **builds + DEPLOYS to bcos.dev** | backend + frontend |
| `hiveops-devices` | **builds + DEPLOYS to bcos.dev** | backend + frontend |
| `hiveops-recon` | build + push only | backend + frontend |
| `hiveops-reports` | build + push only | backend + frontend |
| `hiveops-journal` | build + push only | backend |
| `hiveops-auth` | build + push only | backend |
| `hiveops-dashboard` | build + push only | frontend |
| `hiveops-fleet` | build + push only | **frontend ONLY** |
The runner VM has **no internet access**. Nothing is pulled from the public net at job time:
For every repo except incident and devices, a green run means **the image exists in the registry —
nothing has been deployed.** Someone still has to pull and restart on `.251`.
- **Job image** `192.168.200.227:5000/ubuntu-latest:latest` — Java 21 (Temurin), Maven 3.8.7,
**Trivy** (DB baked in), Docker, openssh-client all pre-installed.
- **OWASP data** at `/opt/owasp-data` (mounted read-only into jobs). The `cd-develop` OWASP step
**skips with a warning** if that dir is absent — it never hard-fails on it. NVD refresh:
`sudo /opt/owasp-data/update.sh` (key in `/etc/owasp-nvd-api-key`).
- **Maven cache** at `/opt/maven-cache`, mounted into jobs.
- **Actions mirrored on Gitea** — `actions/checkout`, `actions/cache` live under the Gitea
`actions` org; `DEFAULT_ACTIONS_URL = self` so the runner never reaches out to github.com.
- **Local registry** `192.168.200.227:5000`.
### 🕳️ Known gap — some backends are never built by CI
To update the job image: build on `dlx-proxmox-03` (has internet) → push to `227:5000`.
- **`hiveops-fleet`: the backend (`hiveiq-fleet`) is built by neither `cd-develop` nor `cd-main`.**
Only the frontend is. Any fleet backend image therefore *has* to be hand-built, which means it
never passes Trivy.
- **`hiveops-devices`: `cd-main` builds only the frontend** — the production devices backend image
is not produced by CI either.
## Access & diagnosis
Treat these as **defects, not as licence to hand-build**. Fix the workflow.
## The runners
Both are **Proxmox VMs on `dlx-proxmox-03`** (DLX network, no internet). Both are active — the
older "runner-02 is not the active worker" note is wrong: `hiveops-incident` built on **runner-02**
on 2026-07-14.
| Runner | IP | Notes |
|--------|-----|-------|
| `dlx-hiveiq-runner-01` | `192.168.200.227` | 4 concurrent jobs. Also hosts the local registry on `:5000`. |
| `dlx-hiveiq-runner-02` | `192.168.200.228` | 4 concurrent jobs. |
```bash
ssh -i ~/.ssh/id_ed25519_dlx -o IdentitiesOnly=yes dlxadmin@192.168.200.227
sudo journalctl -u act_runner -n 40 --no-pager # runner activity / task fetch
sudo docker ps # running job containers (none = idle)
sudo systemctl restart act_runner # if it's wedged
sudo journalctl -u act_runner -n 40 --no-pager # runner activity / task fetch
sudo docker ps # job containers (none = idle)
```
## Failure signatures (read this before blaming the build)
The Gitea API `online` field reads `false` even when a runner is active — a display quirk, ignore it.
- **`failed to fetch task … lookup hiveiq-gitea.directlx.dev on 127.0.0.53:53: server misbehaving`**
→ **the runner's DNS is down**, not a build break. The runner's only resolver is
`127.0.0.53` → AdGuard (`192.168.200.100`) with **no fallback**, so a DLX/AdGuard outage
(e.g. Proxmox maintenance) makes CI go red across *every* repo at once. This is a known
single-point-of-failure → **hiveiq-devops#11** and **platform.gotchas**. Confirm the build is
actually fine by running `mvn clean package` locally on the same commit.
- **Run shows "running" forever in Gitea but `docker ps` on the runner has no job container**
→ an **orphaned/zombie run** — the runner lost its Gitea connection mid-job (DNS blip) and
couldn't report status. It's not really running; a fresh push on stable DNS supersedes it.
- **Sonar job red, build green** → the Sonar host (`192.168.200.230:9000`, DLX side) was
unreachable — again infra, not code.
### ⛔ The registry on runner-01 — never garbage-collect, never delete
`192.168.200.227:5000` stores its data on a **bind mount at `/opt/docker-registry`** (~1.2G). It
holds `ubuntu-latest`, the job base image **every CI job depends on**. `docker volume prune` cannot
touch it (it is not a volume), but:
- **Never** run `registry garbage-collect` against a live registry. On 2026-07-13 that reaped ~86GB
from the *dev* registry (88GB → 2.3GB); images not still in a local Docker cache were **lost
permanently**.
- **Never** `rm -rf /opt/docker-registry`.
### No internet — everything is pre-baked or mirrored
- **Job image** `192.168.200.227:5000/ubuntu-latest:latest` — Java 21 (Temurin), Maven 3.8.7,
**Trivy** (DB baked in), Docker, openssh-client.
- **OWASP data** `/opt/owasp-data` (ro mount). The step **skips with a warning** if absent.
Refresh: `sudo /opt/owasp-data/update.sh` (key in `/etc/owasp-nvd-api-key`).
- **Maven cache** `/opt/maven-cache`.
- **Actions mirrored on Gitea** — `actions/checkout`, `actions/cache` under the `actions` org;
`DEFAULT_ACTIONS_URL = self`, so the runner never reaches github.com.
## Failure signatures — read before blaming the build
- **`network GITEA-ACTIONS-TASK-… not found`** / **`NotFound: lease does not exist`** mid-`docker
build` / `session healthcheck failed fatally`
→ **the runner's Docker daemon is degraded, not your code.** Jobs share one
`/var/run/docker.sock`, so concurrent builds on a long-lived daemon race each other.
**Fix:** `sudo systemctl restart docker && sudo systemctl restart act_runner`, then reclaim the
stale state — orphaned `GITEA-ACTIONS-TASK-*` volumes, build cache, dangling images:
```bash
sudo docker volume ls --format '{{.Name}}' | grep '^GITEA-ACTIONS-TASK-' | xargs -r sudo docker volume rm
sudo docker builder prune -af
sudo docker image prune -f # dangling ONLY — never -a (it evicts ubuntu-latest)
```
Keep the `act-toolcache` volume. The registry container is `restart=always` and comes back on its
own. (2026-07-14: runner-01 had 34 days of uptime, 497 orphaned volumes and 9.7GB of build cache;
four builds died on it.)
- **`failed to fetch task … lookup hiveiq-gitea.directlx.dev … server misbehaving`**
→ **the runner's DNS is down**, not a build break. Its only resolver is AdGuard
(`192.168.200.100`) with **no fallback**, so DLX maintenance turns *every* repo red at once.
Tracked in **hiveiq-devops#11**.
- **Run stuck "running" forever but `docker ps` shows no job container** → an orphaned run; the
runner lost Gitea mid-job and could not report status. A fresh push supersedes it.
- **Sonar red, build green** → the Sonar host (`192.168.200.230:9000`) was unreachable. Infra, not code.
## ⚠️ Do not merge a batch of PRs at once
Each runner has **4 concurrent job slots against a single shared Docker daemon.** Merging six PRs
together on 2026-07-13 fired six workflows simultaneously and **broke runner-01's daemon** — four
builds failed with the lease/network errors above. **Stagger merges**, or accept that you will be
repairing the runner afterwards.
## Root fragility
The runner is a VM on the cluster it depends on, with a single non-HA resolver — so cluster
maintenance removes the DNS the runner needs. Fix tracked in **hiveiq-devops#11** (secondary
resolver on the runner + AdGuard HA). Same DNS single-point-of-failure that hits the app boxes →
**platform.gotchas**.
The runners are VMs on the cluster they depend on, with a single non-HA resolver — so cluster
maintenance removes the DNS the runner needs. Fix tracked in **hiveiq-devops#11**. Same DNS
single-point-of-failure that hits the app boxes → **platform.gotchas**.
@@ -24,14 +24,30 @@ audience: dev
- Live VM edits must be **committed back to `hiveiq-openmetal-dev`** or they drift.
## Deploy a service to bcos.dev
> ⛔ **CI builds the image. You do not.** Merging to `develop` runs `cd-develop.yml`
> (npm audit → **Trivy** → build → push `:dev` **and `:<sha8>`**). **Do NOT run `./build.sh`** —
> a hand-built image skips the security gate and races CI for the `:dev` tag. Full detail, the
> per-repo table of what CI actually builds, and the runner runbook are in **[[platform.ci-cd]]**.
**1. Merge to `develop` and let CI build.** Confirm the image exists before deploying:
```bash
curl -sk -u hiveiq:<pw> https://registry.bcos.dev/v2/hiveiq-<svc>/tags/list # expect your :<sha8>
```
**2. Deploy — but check whether CI already did it.** `cd-develop` **only deploys for
`hiveops-incident` and `hiveops-devices`.** For every other repo CI stops at push, and you deploy:
```bash
cd /source/hiveops-src/hiveops-<svc>
./build.sh --registry registry.bcos.dev --push # tag :dev
ssh -i ~/.ssh/id_ed25519_hiveiq bcosadmin@173.231.195.251
cd ~/hiveiq/hiveops-openmetal/hiveops/instances/dev
# pin the immutable, scanned tag in .env — never deploy from :dev or :latest
docker compose pull hiveiq-<svc> && docker compose up -d hiveiq-<svc>
```
**Then smoke-test it** (see the bottom of this page). A green container is not a deploy.
## Production blue/green cutover (services VM)
Six prod services run blue/green (auth, incident, devices, fleet, transactions, agent-proxy). The
routing mechanics — the `00-backends.conf` map, the targeted-flip gotcha, and how **internal
@@ -15,9 +15,16 @@ Milestones section in [[processes.gitea-issues]]. Roll up everything targeted at
## Microservice release
1. Merge to `main` via the [[processes.main]] gate.
2. Build + push: `./build.sh --registry registry.bcos.cloud --push` (image `hiveiq-<svc>`).
3. Deploy on the production services VM (`173.231.195.250`): `docker compose pull hiveiq-<svc>`
→ `docker compose up -d hiveiq-<svc>` — blue/green rules apply (see [[processes.main]]).
2. **CI builds it — you do not.** `cd-main.yml` fires on the merge: npm audit → **Trivy** →
build → push `:latest` **and `:<sha8>`** to `registry.bcos.cloud`. **Never run `./build.sh`
yourself** — a hand-built image skips the security gate. See **[[platform.ci-cd]]**, which also
lists the repos whose backend CI does *not* build (a defect to fix, not a licence to hand-build).
3. **Confirm the scanned image exists**, then deploy on the production services VM
(`173.231.195.250`) by **pinning `:<sha8>`** — never `:latest`, which is mutable.
Blue/green rules apply: deploy the **STANDBY** colour, then flip the nginx map
(`scripts/bluegreen-deploy.sh`). **Never `docker compose up -d` the ACTIVE colour** — that
recreates the live container and is an outage, not a cutover (see [[processes.main]] and
[[platform.deployment]]).
4. Update **VERSIONS.md** and add a **What's New** entry (below).
## Agent release (`hiveops-agent`)