chore(release): cut and promote v26.15.0 on 736649b3b (main head could not build an image) #900

Closed
opened 2026-08-30 17:29:13 +02:00 by timothy · 0 comments
Owner

Done

What was done: Cut and promoted v26.15.0 (~125 commits since v26.14.0). Tag v26.15.0736649b3b; release-notes PR #898 merged; DeployStack jazz-media promoted prod from v26.14.0 to v26.15.0.

The non-obvious decision — the tag is NOT on main's head, deliberately.

main could not build an image and had not been able to for 8 commits. e8f80c42c (#883) added web/src/api/completeAnnotations.guard.test.ts, a third importer of virtual:etv-tracked-source-files, without adding it to the hand-maintained --exclude list in the Dockerfile's web-build stage — a stage that has no git index by construction. The guard is behaving correctly; it refuses to fall back to a filesystem walk (testing.guard-derives-population-from-source). That is #887, which was live-claimed by a parallel session and was NOT touched here.

A v* tag runs the same Dockerfile, so a tag on main's head would have produced no release image at all. 736649b3b — the commit immediately before the break — is the newest commit on main that can produce one.

Three independent confirmations, because one would not have been enough:

Line of evidence Result
Measured run 2459 @ 736649b3b image job success in 6m45s; run 2515 @ cf5f42edf failure in 86s, dying in web-build
Structural at 736649b3b the only test importing the virtual module is pageSizeCallSites.guard.test.ts, already in the exclude list; completeAnnotations.guard.test.ts does not exist there
End-to-end the registry held ersatztv:736649b3 and none of the 8 shas after it

Also verified e8f80c42c's sole parent is 736649b3b, and docker/Dockerfile was untouched between there and main's head — so no later commit could have quietly fixed it.

Cost, stated rather than buried: #880 (scheduling recurrence — an absent array means unrestricted) merged after the break and therefore ships in the next release, not this one. The other seven excluded commits are hooks/scripts/CI/docs and do not ship in the image.

Verification

Gate Result
Release build (run 2519) all jobs green; Build & test (.NET) 9.3 min (real suite, not a docs fast path)
Local gate on 736649b3b 4556 passed / 7 skipped / 0 failed; SPA 1272/1272; 0 vulnerable packages across 17 projects
Decisions sweep validator OK, catalog no drift, 0 legacy-unmigrated
Version stamp image carries 26.15.0, zero 0.0.0- stamps (#836 failure class ruled out)
semgrep SAST 1 finding — test-only helper, all 5 callers pass hardcoded literals, ErsatzTV.Tests never published → not blocking
ZAP DAST vs immutable :26.15.0 0 FAIL / 1 WARN / 118 PASS → WARN-only, not release-blocking
Pre-deploy safeties backup → ersatztv-backups/20260830T152422Z; migration smoke PASSED on a 322 MB prod copy
Prod after image c724d0ef4a1c, healthy, 0 restarts; running assembly reports 26.15.0
Live smoke M3U 200 / 43 channels · XMLTV 200 / 4.9 MB / 114 programmes · SPA 200

The single ZAP WARN is text/html at / and two unmatched random paths — the documented SPA catch-all (any unmatched non-/api path → /app). No /api/v1 path was involved.

Files changed: docs/ci-cd.md (the v26.15.0 release-table row) — that is the whole diff. The release itself is a tag, not a code change.

Side effect worth recording: DeployStack jazz-media deploys the whole compose project, so dispatcharr was also recreated (it pulled a fresh :latest) alongside ErsatzTV. It came up healthy; Jellyfin was untouched. Promoting ErsatzTV via the stack is not surgical — expect siblings in that project to be recreated too.

Deferred:

  • #887 remains open and owned by another session — this release routed around it, it did not fix it. Until it lands, main still cannot publish an image.
  • .claude/skills/ersatztv/SKILL.md:60 reads "Prod runs …:prod (v26.3.1)". It is dated to the 2026-06-27 cutover but scans as a current-state claim; prod now runs v26.15.0. Not folded into #898 because that PR was already reviewed and green.
  • The decisions validator reports the 60-line per-record ceiling has drifted below the tail boundary it was derived from (p90=104, p95=142; 45/218 over). Still inside the blocking 0.02–0.25 band, so not urgent — but it asks for re-derivation.

Docs updated: docs/ci-cd.md release table (PR #898). No route, endpoint, SPA convention or decision changed, so no other doc was in scope per the CLAUDE.md docs-update table.

Reusable knowledge:

  • A docs-only push to main produces a GREEN run with the image job skipped, not run. After #898 merged, main reads green while #887 is still open. Do not read that green as the break being fixed.
  • The registry is a harder oracle than CI status for "did anything actually publish": GET /api/v1/packages/timothy?type=container&q=ersatztv. A skipped job cannot fake a published tag. Duration is the secondary tell — ~6–7 min when the image job really runs, ~86 s when it dies in web-build.
  • H11 no longer blocks a tag-only push (exemption shipped in #719/#749, with a vacuous-truth guard requiring ≥1 parsed ref line). The old detach-at-origin/main workaround is obsolete; confirmed by pushing this tag with hooks armed.
  • Resolve the prod stack by the container's com.docker.compose.project.config_files label, not by name. Confirmed /etc/komodo/stacks/jazz-media/.... The dead media-servers stack the docs warn about is no longer registered in Komodo at all.

Refs #887, #880, #883.

## Done **What was done**: Cut and promoted **v26.15.0** (~125 commits since v26.14.0). Tag `v26.15.0` → `736649b3b`; release-notes PR #898 merged; `DeployStack jazz-media` promoted prod from v26.14.0 to v26.15.0. **The non-obvious decision — the tag is NOT on `main`'s head, deliberately.** `main` could not build an image and had not been able to for 8 commits. `e8f80c42c` (#883) added `web/src/api/completeAnnotations.guard.test.ts`, a third importer of `virtual:etv-tracked-source-files`, without adding it to the hand-maintained `--exclude` list in the Dockerfile's `web-build` stage — a stage that has no git index by construction. The guard is behaving correctly; it refuses to fall back to a filesystem walk (`testing.guard-derives-population-from-source`). That is **#887**, which was live-claimed by a parallel session and was NOT touched here. A `v*` tag runs the same Dockerfile, so a tag on `main`'s head would have produced **no release image at all**. `736649b3b` — the commit immediately before the break — is the newest commit on `main` that can produce one. **Three independent confirmations**, because one would not have been enough: | Line of evidence | Result | |---|---| | Measured | run 2459 @ `736649b3b` image job **success in 6m45s**; run 2515 @ `cf5f42edf` **failure in 86s**, dying in `web-build` | | Structural | at `736649b3b` the only *test* importing the virtual module is `pageSizeCallSites.guard.test.ts`, already in the exclude list; `completeAnnotations.guard.test.ts` does not exist there | | End-to-end | the registry held `ersatztv:736649b3` and **none** of the 8 shas after it | Also verified `e8f80c42c`'s sole parent *is* `736649b3b`, and `docker/Dockerfile` was untouched between there and `main`'s head — so no later commit could have quietly fixed it. **Cost, stated rather than buried**: **#880** (scheduling recurrence — an absent array means unrestricted) merged after the break and therefore ships in the *next* release, not this one. The other seven excluded commits are hooks/scripts/CI/docs and do not ship in the image. **Verification** | Gate | Result | |---|---| | Release build (run 2519) | all jobs green; `Build & test (.NET)` **9.3 min** (real suite, not a docs fast path) | | Local gate on `736649b3b` | 4556 passed / 7 skipped / 0 failed; SPA 1272/1272; **0 vulnerable packages** across 17 projects | | Decisions sweep | validator OK, catalog no drift, **0** legacy-unmigrated | | Version stamp | image carries `26.15.0`, **zero** `0.0.0-` stamps (#836 failure class ruled out) | | semgrep SAST | 1 finding — test-only helper, all 5 callers pass hardcoded literals, `ErsatzTV.Tests` never published → not blocking | | ZAP DAST vs immutable `:26.15.0` | **0 FAIL / 1 WARN / 118 PASS** → WARN-only, not release-blocking | | Pre-deploy safeties | backup → `ersatztv-backups/20260830T152422Z`; migration smoke **PASSED** on a 322 MB prod copy | | Prod after | image `c724d0ef4a1c`, healthy, 0 restarts; running assembly reports `26.15.0` | | Live smoke | M3U 200 / 43 channels · XMLTV 200 / 4.9 MB / 114 programmes · SPA 200 | The single ZAP WARN is `text/html` at `/` and two unmatched random paths — the documented SPA catch-all (any unmatched non-`/api` path → `/app`). No `/api/v1` path was involved. **Files changed**: `docs/ci-cd.md` (the `v26.15.0` release-table row) — that is the whole diff. The release itself is a tag, not a code change. **Side effect worth recording**: `DeployStack jazz-media` deploys the whole compose project, so **`dispatcharr` was also recreated** (it pulled a fresh `:latest`) alongside ErsatzTV. It came up healthy; Jellyfin was untouched. Promoting ErsatzTV via the stack is not surgical — expect siblings in that project to be recreated too. **Deferred**: - **#887** remains open and owned by another session — this release routed around it, it did not fix it. Until it lands, `main` still cannot publish an image. - `.claude/skills/ersatztv/SKILL.md:60` reads "Prod runs `…:prod` (v26.3.1)". It is dated to the 2026-06-27 cutover but scans as a current-state claim; prod now runs v26.15.0. Not folded into #898 because that PR was already reviewed and green. - The decisions validator reports the 60-line per-record ceiling has drifted below the tail boundary it was derived from (p90=104, p95=142; 45/218 over). Still inside the blocking 0.02–0.25 band, so not urgent — but it asks for re-derivation. **Docs updated**: `docs/ci-cd.md` release table (PR #898). No route, endpoint, SPA convention or decision changed, so no other doc was in scope per the CLAUDE.md docs-update table. **Reusable knowledge**: - A **docs-only push to `main` produces a GREEN run with the image job `skipped`, not run.** After #898 merged, `main` reads green while #887 is still open. Do not read that green as the break being fixed. - The **registry is a harder oracle than CI status** for "did anything actually publish": `GET /api/v1/packages/timothy?type=container&q=ersatztv`. A skipped job cannot fake a published tag. Duration is the secondary tell — ~6–7 min when the image job really runs, ~86 s when it dies in `web-build`. - **H11 no longer blocks a tag-only push** (exemption shipped in #719/#749, with a vacuous-truth guard requiring ≥1 parsed ref line). The old detach-at-`origin/main` workaround is obsolete; confirmed by pushing this tag with hooks armed. - Resolve the prod stack by the container's `com.docker.compose.project.config_files` label, not by name. Confirmed `/etc/komodo/stacks/jazz-media/...`. The dead `media-servers` stack the docs warn about is **no longer registered in Komodo at all**. Refs #887, #880, #883.
timothy added the ci-cdpriority: lowad-hoc labels 2026-08-30 17:29:13 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: timothy/ersatztv#900