ci: gate image build on migrations + #1 handoff #33

Merged
timothy merged 1 commits from foundations/promote-checks into main 2026-06-27 12:29:34 +02:00
2 changed files with 80 additions and 1 deletions
+1 -1
View File
@@ -131,7 +131,7 @@ jobs:
build:
name: Build & push image (amd64)
runs-on: ubuntu-latest
needs: test
needs: [test, migrations]
if: github.event_name != 'pull_request'
steps:
- name: Checkout
+79
View File
@@ -0,0 +1,79 @@
# Handoff — ersatztv#1: M3U `tvg-logo` (and channel-logo) URLs hardcoding localhost
**How to use:** start a fresh session and say something like
*"Read `docs/handoffs/m3u-tvg-logo-fix.md` and execute ersatztv#1"* (optionally *"with ultracode"* /
*"use a workflow"*). Read **ersatztv#1** in full (Gitea, `timothy/ersatztv`) before acting.
This is the first **feature/bug** task after the engineering-foundations batch (all 8 done: CPM, vuln
deps, analyzers, arch tests, golden tests, migration checks, IPTV E2E, contributors guide). #1 is
`priority: high` (Jellyfin channel logos break after every guide refresh) and was deliberately
**unblocked + protected** by that work.
## ⚠️ Investigation first — the issue's stated root cause is partly a RED HERRING
A quick trace (do not skip — it'll save you from the wrong rabbit hole):
- `ChannelLogoGenerator.GenerateChannelLogoUrl()` (`ErsatzTV.Core/Images/ChannelLogoGenerator.cs:81`)
**does** hardcode `http://localhost:{StreamingPort}…`. **But its only callers are in
`ErsatzTV.Core/FFmpeg/WatermarkSelector.cs`** (4×) — i.e. it's the URL **ffmpeg** fetches to draw the
channel logo as a *watermark*, and ffmpeg runs **inside the ErsatzTV container**, where
`localhost:8409` correctly reaches ErsatzTV. So this `localhost` is **not** the M3U bug — leave it
(or confirm before touching).
- The **M3U** `tvg-logo` is built in `ChannelPlaylist.ToM3U()`
(`ErsatzTV.Core/Iptv/ChannelPlaylist.cs:65,70`) from `{_scheme}://{_host}{_baseUrl}` — i.e. the
**request** host. That's already "correct" given a correct request host.
- The **XMLTV** guide (`GetChannelGuideHandler`) and artwork (`RefreshChannelDataHandler:~1114`) use a
**`{RequestBase}` placeholder** substituted at request time with `{Scheme}://{Host}{BaseUrl}` — also
request-host based.
- `Startup` **does** configure `ForwardedHeaders.All` (`Startup.cs:126`) + `app.UseForwardedHeaders()`
(`:534`), and `ETV_BASE_URL` → `UsePathBase` (`:505`).
**So: where does `localhost` actually reach Jellyfin?** Most likely one of:
1. **Request flow / proxy**: Jellyfin (or a reverse proxy) fetches the M3U via a host that resolves to
`localhost` from ErsatzTV's perspective, **or** `UseForwardedHeaders()` runs too late / the proxy
isn't sending `X-Forwarded-Host`, so `Request.Host` ends up `localhost:8409`. Check middleware
**ordering** (forwarded-headers must run before anything reading `Request.Host`) and what host
ErsatzTV actually sees.
2. **A cached value**: `channels.xml` / per-channel fragments are generated by `RefreshChannelList`/
`RefreshChannelData` (background, **no** request) — confirm every host-bearing field there is a
`{RequestBase}` placeholder, not a baked absolute URL. A baked `localhost` there would leak into the
guide regardless of the request.
3. **Genuine misconfiguration** (the issue flags this): the Jellyfin-side M3U URL points at `localhost`.
**Reproduce first.** You can run the real image on bumblebee (Docker host, `ssh timothy@192.168.1.99`,
registry `192.168.1.95:3000/timothy/ersatztv:latest`) and curl `/iptv/channels.m3u` +
`/iptv/xmltv.xml` with different `Host`/`X-Forwarded-*` headers to see exactly when `localhost`
appears. (This is how #16's E2E was validated.)
## Likely fix shapes (decide after repro)
- If it's forwarded-headers ordering/config → fix middleware order / `KnownProxies`/`KnownNetworks`,
or honor an explicit configured public base URL.
- If a background-generated cache bakes a host → switch it to the `{RequestBase}` placeholder pattern
(copy `GetChannelGuideHandler.cs:51` / `RefreshChannelDataHandler.cs:~1114`).
- If purely Jellyfin/proxy config → document it (and this may belong in server-management, not here).
## Regression net (already in place — use it)
- **M3U golden tests**: `ErsatzTV.Core.Tests/Iptv/ChannelPlaylistGoldenTests.cs`. If your fix changes
`ToM3U` output, the goldens **will** fail — review the diff, then regenerate intentionally with
`ETV_UPDATE_GOLDENS=1` (or the `[Explicit] Regenerate_goldens` test) and commit. (ersatztv#11)
- **Container E2E**: the smoke step asserts `/iptv/channels.m3u`→`#EXTM3U` and `/iptv/xmltv.xml`→`<tv`
on the built image (main/tag builds). (ersatztv#16)
- The **XMLTV icon URLs are not golden-tested yet** (that's ersatztv#28). If #1 touches the guide's
logo/icon URLs, consider adding an XMLTV golden as part of this work (or at least a focused test),
since the M3U goldens won't cover it.
## Process (non-negotiable — see CLAUDE.md + `docs/contributing.md`)
- **Root cause**: document WHY (this is a bug fix). If it's config not code, say so and close with that.
- Follow established patterns (`docs/contributing.md`); diverge only with a stated reason.
- **One branch = one PR.** PR runs `test` + `migrations` (both now **required** to merge); merge to
`main` runs `test`+`migrations`+`build`+smoke/E2E. Verify green before closing.
- Adversarial self-review of the diff before closing. Task Completion Protocol / `/done 1`.
- CI poll: `curl -u timothy:ded89Lm4 …/api/v1/repos/timothy/ersatztv/actions/tasks` (jobs by name).
## Repo state at handoff
- All foundations merged to `main`; `main` requires `Build & test (.NET)` **and** `EF migration
integrity (SQLite + MySql)` to merge a PR. Image `build` now `needs: [test, migrations]`.
- Versioning is CalVer `vYY.N.P`; a bug-fix release for #1 would be `v26.4.0` (first release with app
changes) — but only tag when the user asks.
- `ssh-mcp`/`docker-mcp` target `192.168.1.99` (bumblebee); if they fail "auth", their `.mcp.json`
needs `--key=~/.ssh/id_rsa` + a client restart. Plain Bash `ssh timothy@192.168.1.99` works.