Commit graph

8 commits

Author SHA1 Message Date
Emi Griffith
8432144bb3 ci: gate the prod path, fold the repo-wide checks into gate, run the daemon tests
All checks were successful
PR build (required check) / changes (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 6s
PR build (required check) / validate-observability (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 16s
PR build (required check) / lint-shell (pull_request) Successful in 23s
PR build (required check) / guard-secrets (pull_request) Successful in 26s
PR build (required check) / build-frontend (pull_request) Successful in 1m41s
PR build (required check) / build-backend (pull_request) Successful in 2m13s
PR build (required check) / gate (pull_request) Successful in 2s
Four holes, all of which let untested or unchecked code reach an environment.

- PRs into `release` ran no build and no test. release is the branch that deploys
  to prod, so the intended promotion path was the least-checked path in the repo:
  the last seven release PRs ran shellcheck and the secrets check, nothing else.
  pr-build now triggers on it.

- shell-lint and secrets-guard reported as standalone statuses outside `gate`,
  and pr-build's own setup notes tell the operator to require only `gate`. Taken
  literally that means a commit adding a plaintext SOPS file was mergeable. Both
  are now called from pr-build and reduced into gate. They are deliberately not
  domain-gated and not `needs: changes`: they are repo-wide invariants that must
  hold on every PR, including one touching no app domain -- exactly the case
  where every domain job skips and gate used to pass vacuously. For the same
  reason `skipped` counts as a failure for these two, while it stays a pass for
  the domain jobs it legitimately describes.

  They keep their standalone pull_request trigger, so they will run twice on a
  PR until branch protection is confirmed to require only `gate`. Cheap, and it
  avoids breaking a rule that may still require them by name.

- build-push.yml built and pushed with no test step, and deploy.yml consumes a
  TAG rather than a commit status -- so an image reaching the registry by any
  route other than a dev/main PR (a direct push, a dispatch, a v*.*.* tag) was
  never tested by CI, and beta/prod then rolled it. The test now sits between
  build and push, so the artifact that deploys is the artifact that was tested.
  Gating the artifact is what makes this work; gating the branch would not.

- backend/Dockerfile ran `go build` on daemon/ but never its tests. All 48 of
  them (gateway, cron, apiclient, config) ran nowhere: `grep -rn "go test"
  .forgejo/workflows/` found nothing, while the daemon owns the prod Discord
  gateway websocket and every recurring-job timer. It now runs gofmt + vet +
  test before the build, which is the pattern frontend/Dockerfile already uses --
  and being inside `docker build` it gates build-push too. Verified clean: gofmt
  reports nothing, vet passes, all four packages pass.
2026-07-25 01:36:31 -07:00
emi
0f1b3a168c Lake /query reads era5_daily through Iceberg metadata (#44)
All checks were successful
shell-lint / shellcheck (push) Successful in 7s
secrets-guard / encrypted (push) Successful in 9s
PR build (required check) / changes (pull_request) Successful in 14s
secrets-guard / encrypted (pull_request) Successful in 12s
shell-lint / shellcheck (pull_request) Successful in 13s
PR build (required check) / validate-observability (pull_request) Has been skipped
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m27s
Deploy backend to LAN dev server / build (push) Successful in 1m43s
PR build (required check) / build-frontend (pull_request) Successful in 1m23s
PR build (required check) / build-backend (pull_request) Successful in 1m31s
PR build (required check) / gate (pull_request) Successful in 3s
Deploy backend to LAN dev server / deploy (push) Successful in 25s
2026-07-24 19:09:22 +00:00
emi
eaaa4f1ad9 Fix lake extension bake user; disable the daemon healthcheck in the stack (#30)
Some checks failed
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 18s
PR build (required check) / changes (pull_request) Successful in 9s
secrets-guard / encrypted (pull_request) Successful in 10s
shell-lint / shellcheck (pull_request) Successful in 8s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m12s
Deploy backend to LAN dev server / build (push) Successful in 1m27s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-backend (pull_request) Successful in 1m17s
PR build (required check) / build-frontend (pull_request) Successful in 1m8s
PR build (required check) / gate (pull_request) Successful in 2s
Deploy backend to LAN dev server / deploy (push) Failing after 1m55s
2026-07-24 03:47:39 +00:00
Emi Griffith
30b0c70bbb Reconcile: merge main (shellcheck guard, Go daemon) into dev
All checks were successful
secrets-guard / encrypted (pull_request) Successful in 6s
PR build (required check) / changes (pull_request) Successful in 8s
shell-lint / shellcheck (pull_request) Successful in 9s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 1m9s
PR build (required check) / build-backend (pull_request) Successful in 1m44s
PR build (required check) / gate (pull_request) Successful in 2s
main took two direct PRs (#19 shell-lint, #21 the thermograph-daemon Go
service) while dev accumulated the ERA5 lake stack; both sides added compose/
stack services and touched the same seams. Resolutions keep both worlds:

- compose + Swarm stack: lake AND daemon are sibling services; backend env
  carries THERMOGRAPH_LAKE_URL and THERMOGRAPH_INTERNAL_TOKEN.
- deploy.sh: backend deploys roll (backend lake daemon); all adds frontend.
- requirements.txt: websockets/apscheduler stay removed (moved to the Go
  daemon), duckdb stays (the lake role's engine).

Merged tree: backend suite green, both compose configs validate.
2026-07-23 17:06:52 -07:00
emi
6396d553d2 Own /state in the image; lake cache degrades instead of failing (#25)
All checks were successful
secrets-guard / encrypted (push) Successful in 12s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 57s
Deploy backend to LAN dev server / build (push) Successful in 1m13s
Deploy backend to LAN dev server / deploy (push) Successful in 18s
2026-07-23 22:55:41 +00:00
emi
2d3f37c474 daemon: move the Discord gateway and scheduler out of the web process into Go (#21)
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 8s
Sync infra to hosts / sync-prod (push) Successful in 7s
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 10s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m14s
Deploy backend to beta VPS / deploy (push) Successful in 2m4s
The gateway bot and APScheduler were long-lived stateful I/O loops running
inside the async web app under a leader election. They move into a single Go
binary that owns ONLY that I/O -- websocket, RESUME, heartbeat, backoff, timers.

It owns no grading logic. Anything needing data calls back over a new
internal-only surface (/internal/discord/grade, /internal/jobs/*). Grading
depends on polars and the parquet cache; reimplementing it in Go would let the
bot's grades drift from the API's. The grade route returns gateway-ready JSON
and Go relays the bytes verbatim.

The binary ships in the backend image and runs as a second compose service off
the same tag, so the two ends of the /internal/* contract can never skew.
deploy.sh rolls daemon alongside backend -- without that the service would never
be created, since a single-service deploy uses --no-deps. It also probes the
image first and skips the daemon when rolling a tag that predates the binary:
infra tracks main while image tags are env-staged, so a host can legitimately be
asked to roll an older backend image, and creating the service anyway would
leave a container crash-looping on a missing binary.

replicas: 1 with order: stop-first replaces the leader election -- Discord
permits one gateway connection per bot token.

THERMOGRAPH_INTERNAL_TOKEN is optional: both ends derive it from
THERMOGRAPH_AUTH_SECRET via HMAC under a domain-separation label, so this needs
no new vault entry. The derivation is pinned to a shared cross-language test
vector asserted on both sides, so drift fails CI instead of 401ing every call.
Fail closed when neither secret is set.

Improvements over the Python: a close intended for RESUME uses 4000 rather than
1000 (Discord invalidates a session closed 1000, so the old default defeated its
own resume); MESSAGE_CREATE runs on a bounded worker pool; and a malformed HELLO
returns an error rather than a clean reconnect, which would otherwise reset
backoff and hot-loop against the gateway.

365 Python tests pass; Go build/vet/test -race clean; shellcheck 0 findings.
2026-07-23 22:49:54 +00:00
emi
370a4ffdc1 ERA5 lake: bucket-hosted history primary + SQL indexer service (#15)
All checks were successful
secrets-guard / encrypted (push) Successful in 10s
Deploy backend to LAN dev server / build (push) Successful in 1m13s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m39s
Deploy backend to LAN dev server / deploy (push) Successful in 34s
2026-07-23 21:20:35 +00:00
Emi Griffith
a4be7066e5 Subtree-merge thermograph-backend (origin/main) into backend/
git-subtree-dir: backend
git-subtree-mainline: 6723fc0326
git-subtree-split: 83c2e05b96
2026-07-22 22:01:11 -07:00