Rewrites the CI workflow to use the house pattern — the one that already works on this cluster — instead of the bespoke mechanism I had built.
I should have started by reading armistace/resbuilder_ai and armistace/wedding-photos (and the gitea-forgejo skill's homelab-ci-workflow-template.md) rather than inventing a parallel one. This corrects that.
What was wrong with my version, and what it is now
Mine
House pattern
Why it mattered
Three jobs: test → build → deploy
One job
The buildx kubernetes driver needs the kubeconfig to create its builder pod. I put the kubeconfig in a job that ran after the build, so the driver could never use it.
driver: remote + endpoint: tcp://buildkit...
driver: kubernetes + namespace=gitea-runner
I switched to the remote driver to dodge an x509 error. The real cause was that the job container lacked the k3s CA — and the working workflows solve that the same way I now do: the kubeconfig carries the CA. The remote driver was a workaround for a problem the house pattern does not have.
scripts/kube-ca-refresh.sh + a pinned CA fingerprint
deleted
Both existed only to work around that same x509 error. The house pattern does not need them.
A SET_BY_CI image-tag sentinel + kubectl set image
:latest + rollout restart
The sentinel was my own invention and does not fit the pattern.
A separate verification job with a digest comparison
A verification step in the deploy job
The house skill's rule is "pods Running ≠ app wired" — exercise the real endpoints.
The deploy step
Installs kubectl, applies kube/*.yaml, creates the image-pull secret idempotently, then does a rollout restart (the image tag in the manifest does not change, so apply alone does not roll the pods).
The namespace is not deleted — that would destroy the live beer-db and beer-app Secrets, which are provisioned by hand.
The verification step
Exercises /healthz and /readyz from inside the pod, using the pod's own python. The house skill is explicit that curl is not in the slim images and that kubectl exec against the app's own pod is the reliable way to probe a deployed app.
The Deployment manifest
Also corrected against the house skill's notes:
runAsUser: 999 at the pod level, matching the beer user in the image. kubelet can only verify a numeric UID, so runAsNonRoot on the container fails with a named user.
allowPrivilegeEscalation and capabilities are on the container, not the pod — the skill records that putting them on the pod makes kubectl applysilently reject the manifest.
Verified against the live cluster
All three manifests were validated with kubectl apply --dry-run=client first (which is what catches the schema error above), then applied for real:
/healthz and /readyz also both return 200 through the NodePort.
Test plan
The workflow YAML parses
All three manifests pass kubectl apply --dry-run=client
The manifests apply cleanly and the rollout completes
/healthz and /readyz return 200 from inside the pod, using the same command the CI step uses
The workflow has not yet run on CI. It runs on merge to master — that is the real proof, and I will report the outcome.
## Summary
Rewrites the CI workflow to use the **house pattern** — the one that already works on this cluster — instead of the bespoke mechanism I had built.
I should have started by reading `armistace/resbuilder_ai` and `armistace/wedding-photos` (and the `gitea-forgejo` skill's `homelab-ci-workflow-template.md`) rather than inventing a parallel one. This corrects that.
### What was wrong with my version, and what it is now
| Mine | House pattern | Why it mattered |
|---|---|---|
| Three jobs: `test` → `build` → `deploy` | One job | The buildx `kubernetes` driver needs the kubeconfig to create its builder pod. I put the kubeconfig in a job that ran *after* the build, so the driver could never use it. |
| `driver: remote` + `endpoint: tcp://buildkit...` | `driver: kubernetes` + `namespace=gitea-runner` | I switched to the remote driver to dodge an x509 error. The real cause was that the job container lacked the k3s CA — and the working workflows solve that the same way I now do: the kubeconfig carries the CA. The remote driver was a workaround for a problem the house pattern does not have. |
| `scripts/kube-ca-refresh.sh` + a pinned CA fingerprint | *deleted* | Both existed only to work around that same x509 error. The house pattern does not need them. |
| A `SET_BY_CI` image-tag sentinel + `kubectl set image` | `:latest` + `rollout restart` | The sentinel was my own invention and does not fit the pattern. |
| A separate verification job with a digest comparison | A verification step in the deploy job | The house skill's rule is *"pods Running ≠ app wired"* — exercise the real endpoints. |
### The deploy step
Installs kubectl, applies `kube/*.yaml`, creates the image-pull secret idempotently, then does a `rollout restart` (the image tag in the manifest does not change, so `apply` alone does not roll the pods).
The namespace is **not** deleted — that would destroy the live `beer-db` and `beer-app` Secrets, which are provisioned by hand.
### The verification step
Exercises `/healthz` and `/readyz` from inside the pod, using the pod's own python. The house skill is explicit that `curl` is not in the slim images and that `kubectl exec` against the app's own pod is the reliable way to probe a deployed app.
### The Deployment manifest
Also corrected against the house skill's notes:
- `runAsUser: 999` at the **pod** level, matching the `beer` user in the image. kubelet can only verify a numeric UID, so `runAsNonRoot` on the container fails with a named user.
- `allowPrivilegeEscalation` and `capabilities` are on the **container**, not the pod — the skill records that putting them on the pod makes `kubectl apply` *silently* reject the manifest.
### Verified against the live cluster
All three manifests were validated with `kubectl apply --dry-run=client` first (which is what catches the schema error above), then applied for real:
```
namespace/beer configured
deployment.apps/beer-deployment configured
service/beer-service unchanged
deployment "beer-deployment" successfully rolled out
```
And from inside the pod, the same way the CI step does it:
```
$ kubectl -n beer exec deploy/beer-deployment -- python -c "...urlopen('http://127.0.0.1:5000/healthz')..."
healthz: {"status":"ok"}
rc=0
```
`/healthz` and `/readyz` also both return 200 through the NodePort.
## Test plan
- [x] The workflow YAML parses
- [x] All three manifests pass `kubectl apply --dry-run=client`
- [x] The manifests apply cleanly and the rollout completes
- [x] `/healthz` and `/readyz` return 200 from inside the pod, using the same command the CI step uses
- [ ] **The workflow has not yet run on CI.** It runs on merge to `master` — that is the real proof, and I will report the outcome.
The cluster-count guard I added used `grep -c '"name"'`, which counts
LINES. The jsonpath output is a single line, so the count was 1 for a
kubeconfig with clusters and 1 for one without - which made the guard
reject every kubeconfig:
ERROR: the kubeconfig has no cluster entries.
The count is now done with `grep -o . | wc -l`, which counts occurrences
rather than lines. Tested against both cases:
- a real kubeconfig with a stale CA -> the CA is refreshed and kubectl works
- a kubeconfig with no clusters -> refuses with a clear message
I had built a parallel mechanism for this repo rather than copying the
workflow that is already known to work on this cluster. The working patterns
are `armistace/resbuilder_ai` and `armistace/wedding-photos`, and the house
skill (`gitea-forgejo/references/homelab-ci-workflow-template.md`) documents
them. This rewrites the workflow to match them.
What changed, and why each piece was wrong:
- ONE job, not three. The kubeconfig is written at the TOP, before buildx,
because the buildx `kubernetes` driver needs it to create its builder pod.
My split into test/build/deploy jobs put the kubeconfig in a job that ran
AFTER the build, which the kubernetes driver cannot use.
- `driver: kubernetes` with `namespace=gitea-runner`, not the `remote`
driver. I switched to the remote driver to dodge an x509 error, but the
real cause was that the job container lacked the k3s CA - and the working
workflows solve that the same way: the kubeconfig carries the CA. The
remote driver was a workaround for a problem the house pattern does not
have.
- The CA-refresh script and the pinned-fingerprint file are deleted. They
existed to work around the same x509 error, and the house pattern does not
need them.
- The deploy step installs kubectl and applies `kube/*.yaml`, creating the
image-pull secret idempotently, then does a `rollout restart` (the image
tag in the manifest does not change, so `apply` alone does not roll the
pods).
- The verification step exercises `/healthz` and `/readyz` from inside the
pod, using the pod's own python - the pattern the house skill documents
for probing a deployed app. There is no `curl` in the slim image.
The Deployment manifest also changes:
- `runAsUser: 999` at the POD level, matching the `beer` user in the image.
kubelet can only verify a numeric UID, so `runAsNonRoot` on the container
fails with a named user.
- `allowPrivilegeEscalation` and `capabilities` are on the CONTAINER, not
the pod - the house skill notes that putting them on the pod makes
`kubectl apply` silently reject the manifest.
- The image is `:latest` again, with the rollout restart doing the work -
the `SET_BY_CI` sentinel was my own invention and does not fit the pattern.
All three manifests were validated with `kubectl apply --dry-run=client`
and then applied for real: the rollout completed and `/healthz` and
`/readyz` both return 200 from inside the pod.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Rewrites the CI workflow to use the house pattern — the one that already works on this cluster — instead of the bespoke mechanism I had built.
I should have started by reading
armistace/resbuilder_aiandarmistace/wedding-photos(and thegitea-forgejoskill'shomelab-ci-workflow-template.md) rather than inventing a parallel one. This corrects that.What was wrong with my version, and what it is now
test→build→deploykubernetesdriver needs the kubeconfig to create its builder pod. I put the kubeconfig in a job that ran after the build, so the driver could never use it.driver: remote+endpoint: tcp://buildkit...driver: kubernetes+namespace=gitea-runnerscripts/kube-ca-refresh.sh+ a pinned CA fingerprintSET_BY_CIimage-tag sentinel +kubectl set image:latest+rollout restartThe deploy step
Installs kubectl, applies
kube/*.yaml, creates the image-pull secret idempotently, then does arollout restart(the image tag in the manifest does not change, soapplyalone does not roll the pods).The namespace is not deleted — that would destroy the live
beer-dbandbeer-appSecrets, which are provisioned by hand.The verification step
Exercises
/healthzand/readyzfrom inside the pod, using the pod's own python. The house skill is explicit thatcurlis not in the slim images and thatkubectl execagainst the app's own pod is the reliable way to probe a deployed app.The Deployment manifest
Also corrected against the house skill's notes:
runAsUser: 999at the pod level, matching thebeeruser in the image. kubelet can only verify a numeric UID, sorunAsNonRooton the container fails with a named user.allowPrivilegeEscalationandcapabilitiesare on the container, not the pod — the skill records that putting them on the pod makeskubectl applysilently reject the manifest.Verified against the live cluster
All three manifests were validated with
kubectl apply --dry-run=clientfirst (which is what catches the schema error above), then applied for real:And from inside the pod, the same way the CI step does it:
/healthzand/readyzalso both return 200 through the NodePort.Test plan
kubectl apply --dry-run=client/healthzand/readyzreturn 200 from inside the pod, using the same command the CI step usesmaster— that is the real proof, and I will report the outcome.PR received — starting review, sit tight 🫡
The cluster-count guard I added used `grep -c '"name"'`, which counts LINES. The jsonpath output is a single line, so the count was 1 for a kubeconfig with clusters and 1 for one without - which made the guard reject every kubeconfig: ERROR: the kubeconfig has no cluster entries. The count is now done with `grep -o . | wc -l`, which counts occurrences rather than lines. Tested against both cases: - a real kubeconfig with a stale CA -> the CA is refreshed and kubectl works - a kubeconfig with no clusters -> refuses with a clear message6538eb3258to563e22fb00