hermes 6538eb3258 fix(ci): use the house CI pattern instead of a bespoke one
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.
2026-11-09 12:48:19 +10:00
2024-09-07 23:24:32 +10:00
2024-09-07 23:24:32 +10:00
2024-09-07 23:24:32 +10:00
2024-09-07 23:24:32 +10:00
2024-09-07 23:24:32 +10:00

Brew Log

Stores specific-gravity data for home-brewed beer runs in Mongo and turns it into useful figures — alcohol by volume, days in the fermenter, gravity trends.

Flask app, Mongo backend, no build step for the front end.

Running it locally

python -m venv .venv
. .venv/bin/activate
pip install -r requirements-dev.txt

export MONGO_HOST=192.168.178.229
export MONGO_USER=root
export MONGO_PASS=...
export SECRET_KEY=anything-for-local-use

cd src/flask
python -m beer_data            # dev server on :5000
# or, production-style:
python -m waitress --listen=0.0.0.0:5000 --call beer_data:create_app

Then open http://localhost:5000/ and sign in.

Tests

. .venv/bin/activate
python -m pytest tests/ -q

The tests use mongomock, so they need no database and no network.

Layout

src/flask/
  beer_data.py       entry point; exposes `app` and `create_app`
  app_factory.py     application factory, request-scoped Mongo repository
  brewing.py         pure domain logic — gravity parsing, ABV, run aggregation
  views/             blueprints: auth, brews, (later) analysis
  mongo/repository.py Mongo access for the beer_data and beer_runs collections
  templates/         Jinja templates
  static/            CSS, JS, fonts, logo (all served locally — no CDN)
tests/               pytest suite

brewing.py holds every piece of arithmetic and has no Flask or Mongo dependency, so the maths is unit-testable on its own.

Data model

beer_data — one document per gravity reading

Field Type Notes
_id string 24 random characters
beer_run_id string The run this reading belongs to, e.g. "20"
beer_type string Beer/cider name. Free text, per reading — it drifts across a run
sg string Specific gravity in points: 1039 means 1.039
date string YYYY-MM-DD
final_reading string "True" / "False" — marks the last reading of a run
comment string Free text

The unique key is (date, beer_run_id): re-submitting a reading for the same run and day updates it in place rather than creating a duplicate.

A run is active while it has no reading with final_reading == "True".

beer_runs — one document per run (metadata)

Added alongside the readings to hold what cannot be derived from them: the canonical beer name, the tap number, the recorded original gravity, and brew notes. This collection is only ever inserted into, never written over by the backfill.

Alcohol by volume

Standard homebrew formula:

ABV% = (OG - FG) × 131.25

With gravity stored in points, the drop is already in points, so ABV% = points_drop × 0.13125.

The previous version of this app used (max - min) / 7.36 + 0.5, which is not a recognised brewing formula and overstated every result. The old figure is still computed by brewing.legacy_abv_percent() so the history page can show the difference between the two.

For a run still in the fermenter the figure is an estimate: it uses the run's first reading as the original gravity. A run with a single reading and no recorded OG has no reliable estimate, and the UI shows a dash rather than a misleading number.

Deployment

The image is built and pushed by .gitea/workflows/build_push.yml on push to master. See RUNBOOK.md for the cluster side.

S
Description
No description provided
Readme
1.2 MiB
Languages
Python 64.3%
HTML 14.5%
JavaScript 11.4%
CSS 9%
Dockerfile 0.7%