Merge main back into dev to reconcile the squash-promotion divergence #91

Merged
admin_emi merged 23 commits from sync/main-into-dev into dev 2026-07-25 08:45:52 +00:00
Owner

Prerequisite for the beta promotion. Merge this first, then #90 becomes mergeable.

Why the promotion was blocked

promote opened #90 and reported mergeable: falsedev is 5 ahead but 22 behind main, with a certain modify/delete conflict.

The cause: recent promotions were merged with squash rather than "Create merge commit". A squash replays dev's commits onto main under new SHAs, so the branches share content but not history. main then looks like it independently edited the same files, and every later promotion inherits the divergence. promote's own docs warn about this — "a squash would rewrite shared history".

Real conflicts, confirmed with git merge-tree --write-tree:

  • CLAUDE.md, README.md — content conflicts
  • the eight workflow files #87 deleted — modify/delete

(Correction to something I said earlier: I previously reported this merge as conflict-free. That check used git merge-tree's legacy three-arg form, which doesn't surface conflicts in the format I was grepping for. It was a false negative.)

Resolution

Both conflicts take dev's side — not as a default, but because main's side describes workflows that no longer exist (*-deploy-dev.yml, backend-build-push, and friends). Keeping it would reintroduce exactly the staleness the root CLAUDE.md now forbids: statements naming files that aren't there.

The eight deleted workflows stay deleted. The merge result carries the nine #87 left behind.

Verification

  • No conflict markers anywhere; no unmerged paths.
  • No document naming a deleted workflow.
  • All nine workflow files parse.
  • shellcheck -x clean across all 34 scripts (CI-equivalent invocation, pinned v0.11.0).

After this lands

#90 (devmain) should go green and mergeable. Merge that one with "Create merge commit", not squash — otherwise this divergence recurs immediately and the next promotion needs the same repair.

Prerequisite for the beta promotion. **Merge this first, then #90 becomes mergeable.** ## Why the promotion was blocked `promote` opened [#90](https://git.thermograph.org/emi/thermograph/pulls/90) and reported `mergeable: false` — `dev` is 5 ahead but **22 behind** `main`, with a certain modify/delete conflict. The cause: recent promotions were merged with **squash** rather than "Create merge commit". A squash replays dev's commits onto `main` under new SHAs, so the branches share content but not history. `main` then looks like it independently edited the same files, and every later promotion inherits the divergence. `promote`'s own docs warn about this — "a squash would rewrite shared history". Real conflicts, confirmed with `git merge-tree --write-tree`: - `CLAUDE.md`, `README.md` — content conflicts - the eight workflow files #87 deleted — modify/delete *(Correction to something I said earlier: I previously reported this merge as conflict-free. That check used `git merge-tree`'s legacy three-arg form, which doesn't surface conflicts in the format I was grepping for. It was a false negative.)* ## Resolution Both conflicts take **dev's** side — not as a default, but because `main`'s side describes workflows that no longer exist (`*-deploy-dev.yml`, `backend-build-push`, and friends). Keeping it would reintroduce exactly the staleness the root `CLAUDE.md` now forbids: statements naming files that aren't there. The eight deleted workflows stay deleted. The merge result carries the nine #87 left behind. ## Verification - No conflict markers anywhere; no unmerged paths. - No document naming a deleted workflow. - All nine workflow files parse. - `shellcheck -x` clean across all 34 scripts (CI-equivalent invocation, pinned v0.11.0). ## After this lands #90 (`dev` → `main`) should go green and mergeable. **Merge that one with "Create merge commit", not squash** — otherwise this divergence recurs immediately and the next promotion needs the same repair.
admin_emi added 23 commits 2026-07-25 08:45:14 +00:00
Merge pull request 'Promote dev to main (deploy beta): ERA5 lake stack' (#26) from dev into main
All checks were successful
Build + push backend image (Forgejo registry) / build-push (push) Successful in 28s
Sync infra to hosts / sync-beta (push) Successful in 9s
Sync infra to hosts / sync-prod (push) Successful in 7s
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 10s
Deploy backend to beta VPS / deploy (push) Successful in 50s
755886e244
frontend: rewrite the SSR content service in Go (#28)
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 13s
Sync infra to hosts / sync-prod (push) Successful in 12s
secrets-guard / encrypted (push) Successful in 7s
shell-lint / shellcheck (push) Successful in 8s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 53s
Deploy frontend to beta VPS / deploy (push) Successful in 1m16s
secrets-guard / encrypted (pull_request) Successful in 5s
shell-lint / shellcheck (pull_request) Successful in 6s
92e74c585a
Ports frontend/ (Jinja2/FastAPI, ~1180 LOC) to Go with html/template. No
climate math, no DB, no auth -- every route fetches from the backend's
/content/* API.

Verified with a golden-HTML diff, not just unit tests: both the Python
original and the Go rewrite were run against the same committed fixtures
and every route compared byte-for-byte, confirmed programmatically. That
process caught defects unit tests alone missed, since map[string]any has
no compile-time field check:

- Render-context keys were snake_case throughout while the templates read
  PascalCase fields. A missing map key doesn't error, it silently renders
  empty -- title, meta description, canonical URL, OpenGraph tags, and the
  homepage's entire ranked list were blank on every page despite every
  route returning 200. Fixed by renaming every key to match each
  template's own documented field contract, and passing API structs
  straight through wherever their fields already matched (removes a whole
  layer of future drift risk).
- Three pages 500'd: ToolHref needed a composed href, not a bare
  "lat,lon" fragment; the records table needed the raw API struct.
- JSON-LD was double-encoded: <script type="application/ld+json"> is
  JAVASCRIPT context to html/template's escaper regardless of the
  script's type attribute, so template.HTML gets re-escaped as a quoted
  JS string. Needed template.JS. The glossary term page's JSON-LD was
  never built at all -- added.
- html/template silently strips literal HTML and JS comments from parsed
  output (verified in isolation) -- both need a FuncMap function
  returning template.HTML/template.JS to survive.

Packaging: 187MB -> 22.6MB. Two defects caught before reaching a host: the
Swarm stack's entrypoint override with no explicit command drops the
image's CMD entirely (every deploy would have exited 127), and
COPY --chown by name fails under the classic Docker builder on Alpine.
Both fixed.

go build/vet/test -race clean; docker build passes its embedded test step
under both BuildKit and the classic builder; shellcheck 0 findings.
Merge pull request 'Promote dev to main: lake extension bake + daemon healthcheck fixes' (#31) from dev into main
All checks were successful
Deploy backend to beta VPS / deploy (push) Successful in 42s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 44s
Sync infra to hosts / sync-beta (push) Successful in 5s
Sync infra to hosts / sync-prod (push) Successful in 6s
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 8s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 9s
b83bfdbd67
deploy.sh: fix the daemon-binary probe, which always dropped daemon (#33)
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 5s
Sync infra to hosts / sync-prod (push) Successful in 4s
secrets-guard / encrypted (push) Successful in 5s
shell-lint / shellcheck (push) Successful in 7s
979653f407
docker compose config --images daemon does not filter to the named service
on this host's Compose v5.3.1 -- it prints every service's image, one per
line, in file order, so `| head -1` was silently grabbing db's image
(timescaledb) instead of daemon's. The probe then always found no
/usr/local/bin/thermograph-daemon in a Postgres image and dropped daemon
from every backend deploy, regardless of what the real backend image
contained.

Fixed by building the image reference directly from the same vars
docker-compose.yml's daemon.image: already interpolates, instead of going
through docker compose config at all.

Reproduced against beta directly: the old sequence selected the wrong
image; the new construction resolves correctly and the binary probe
passes.
frontend: fetch City() concurrently with the page's primary API call (#37)
All checks were successful
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 14s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 42s
Deploy frontend to beta VPS / deploy (push) Successful in 1m5s
da82abda27
MonthPage and RecordsPage each made two backend calls sequentially
(CityMonth/CityRecords, then City) where the second never depended on the
first's result. fetchWithCity launches both concurrently via goroutines
and a WaitGroup.

Verified live against a stub with an injected 400ms delay on both
endpoints: city page (1 call) and month/records pages (2 calls each) all
cost ~0.404s now, not double for the two-call pages.

Error priority preserved exactly: primary's error wins even when City
also fails, matching the old sequential code. One trade-off: City() is
now always launched even on a request about to 404 from primary, costing
one extra cheap lookup on that rare path.

Tests include a deterministic concurrency proof via rendezvous channels
(the old sequential code would deadlock this test, not just run it
slower). Full suite green under -race -count=2.
infra/forgejo: add resource limits and a leaner CI job image (#38)
Some checks failed
Sync infra to hosts / sync-beta (push) Successful in 7s
Sync infra to hosts / sync-prod (push) Failing after 8s
secrets-guard / encrypted (push) Successful in 7s
shell-lint / shellcheck (push) Successful in 8s
dcf15ea572
Merge pull request 'Promote dev to main: daemon roll + Iceberg-backed lake query' (#41) from dev into main
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 6s
Sync infra to hosts / sync-prod (push) Successful in 11s
secrets-guard / encrypted (push) Successful in 9s
Validate observability stack / validate (push) Successful in 14s
shell-lint / shellcheck (push) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 8s
88f5d0cce0
infra/forgejo: raise LAN runner concurrency to 8 (#43)
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 8s
Sync infra to hosts / sync-prod (push) Successful in 10s
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 8s
81c579ddcb
Merge pull request 'Promote dev to main: Iceberg-backed lake query' (#45) from dev into main
All checks were successful
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 8s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 10s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 41s
Deploy backend to beta VPS / deploy (push) Successful in 47s
6f5682b7d7
Merge pull request 'Promote dev to main: geocode P0 fix + ops tooling + privacy logging' (#52) from dev into main
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 8s
secrets-guard / encrypted (push) Successful in 12s
shell-lint / shellcheck (push) Successful in 10s
Sync infra to hosts / sync-prod (push) Successful in 16s
Deploy backend to beta VPS / deploy (push) Successful in 38s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 52s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 10s
0935eea394
Merge pull request 'Promote dev to main: off-box encrypted backups' (#57) from dev into main
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 6s
secrets-guard / encrypted (push) Successful in 5s
Sync infra to hosts / sync-prod (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 7s
60d5af41dd
Merge pull request 'Promote dev to main: first alerting + Postfix supervision' (#60) from dev into main
Some checks failed
Sync infra to hosts / sync-prod (push) Failing after 7s
Sync infra to hosts / sync-beta (push) Successful in 13s
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 13s
Validate observability stack / validate (push) Successful in 17s
secrets-guard / encrypted (pull_request) Successful in 6s
shell-lint / shellcheck (pull_request) Successful in 6s
633c6a5ace
Merge pull request 'Promote dev to main: MET forecast coverage gate' (#64) from dev into main
All checks were successful
secrets-guard / encrypted (push) Successful in 9s
shell-lint / shellcheck (push) Successful in 16s
secrets-guard / encrypted (pull_request) Successful in 11s
shell-lint / shellcheck (pull_request) Successful in 13s
Deploy backend to beta VPS / deploy (push) Successful in 51s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m22s
150029075e
Promote dev → main (frontend QA batch → beta) (#71)
Some checks failed
Deploy frontend to beta VPS / deploy (push) Failing after 8s
Sync infra to hosts / sync-beta (push) Successful in 7s
Deploy backend to beta VPS / deploy (push) Failing after 13s
secrets-guard / encrypted (push) Successful in 12s
Sync infra to hosts / sync-prod (push) Successful in 14s
shell-lint / shellcheck (push) Successful in 12s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 57s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 1m2s
ca84e0ce95
Merge pull request 'Promote dev to main: NASA retirement' (#73) from dev into main
All checks were successful
secrets-guard / encrypted (push) Successful in 12s
shell-lint / shellcheck (push) Successful in 11s
secrets-guard / encrypted (pull_request) Successful in 12s
shell-lint / shellcheck (pull_request) Successful in 10s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m54s
Deploy backend to beta VPS / deploy (push) Successful in 2m24s
f596fa87d8
Merge pull request 'Promote dev to main: vault consolidation + renderer fail-closed' (#76) from dev into main
All checks were successful
Sync infra to hosts / sync-beta (push) Successful in 7s
Sync infra to hosts / sync-prod (push) Successful in 6s
secrets-guard / encrypted (push) Successful in 6s
shell-lint / shellcheck (push) Successful in 7s
9350045866
Merge pull request 'Promote dev to main: dev + Centralis vaults' (#78) from dev into main
All checks were successful
Sync infra to hosts / sync-prod (push) Successful in 5s
secrets-guard / encrypted (push) Successful in 7s
shell-lint / shellcheck (push) Successful in 9s
Sync infra to hosts / sync-beta (push) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 6s
shell-lint / shellcheck (pull_request) Successful in 8s
6c6842ab9e
Merge pull request 'promote: dev → main' (#83) from dev into main
All checks were successful
secrets-guard / encrypted (push) Successful in 8s
shell-lint / shellcheck (push) Successful in 7s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 44s
Deploy backend to beta VPS / deploy (push) Successful in 48s
be88987d95
infra: mirror LAN dev secrets under $HOME for snap-confined Docker (#85)
Some checks failed
Sync infra to hosts / sync-beta (push) Failing after 13s
Sync infra to hosts / sync-prod (push) Successful in 25s
secrets-guard / encrypted (push) Successful in 22s
Validate observability stack / validate (push) Successful in 27s
shell-lint / shellcheck (push) Successful in 21s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 1m24s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 1m21s
Deploy backend to beta VPS / deploy (push) Successful in 2m4s
Deploy frontend to beta VPS / deploy (push) Successful in 2m12s
dde2cdd6e2
Merge main back into dev to reconcile the squash-promotion divergence
All checks were successful
secrets-guard / encrypted (pull_request) Successful in 5s
PR build (required check) / changes (pull_request) Successful in 11s
shell-lint / shellcheck (pull_request) Successful in 7s
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / gate (pull_request) Successful in 1s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
be4aa940d9
The dev → main promotion could not merge: `main` sat 22 commits ahead of `dev`
with content dev had never seen, and git found genuine conflicts in CLAUDE.md
and README.md plus modify/delete on the eight workflow files #87 removed.

The cause is that recent promotions were merged with squash rather than "Create
merge commit". A squash replays dev's commits onto main under new SHAs, so the
two branches share content but not history — main appears to have independently
edited the same files, and every subsequent promotion inherits the divergence.
`promote` warns about exactly this.

Resolution: both conflicts take dev's side, not as a default but because main's
side describes workflows that no longer exist (`*-deploy-dev.yml`,
`backend-build-push` and friends). Keeping it would reintroduce the class of
staleness the root CLAUDE.md now forbids — statements naming files that are not
there. The eight deleted workflows stay deleted; the merge result carries the
nine that #87 left behind.

Verified after resolution: no conflict markers anywhere, no unmerged paths, no
document naming a deleted workflow, all nine workflow files parse, and
shellcheck is clean across the tree.

Promotions from here should use "Create merge commit" so this does not recur.
admin_emi merged commit 52cfe5b77a into dev 2026-07-25 08:45:52 +00:00
admin_emi deleted branch sync/main-into-dev 2026-07-25 08:45:53 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Jinemi/thermograph#91
No description provided.