Document upstream's vYY.<release-seq>.<patch> scheme (year, sequential release-in-year, patch) in docs/ci-cd.md + CLAUDE.md so we follow it going forward: <release-seq> is NOT the calendar month (v25.2.0 shipped in June, v26.3.0 in Feb), it's a per-year counter that resets each January. v26.3.1 = our infra-only rebuild of upstream 26.3.0; v26.4.0 reserved for the first release with app changes. Also notes the [skip ci]-on-a-tagged-commit gotcha. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.1 KiB
CI/CD for the ErsatzTV Fork
The fork builds its own Docker image via Gitea Actions on the homelab and pushes to the Gitea container registry. Runner + registry were provisioned in server-management#172; the build pipeline is ersatztv#4; test/prod containers are server-management#481.
Versioning & releases
The fork inherits upstream ErsatzTV's scheme: vYY.<release-seq>.<patch> (lightweight, v-prefixed git tags).
YY— two-digit year.<release-seq>— a sequential release counter within the year, reset at each year boundary. It is not the calendar month. (Evidence:v25.2.0shipped in June 2025,v25.5.0in Sep,v26.3.0in Feb 2026 — minors don't track months; andv25.9.0→v26.1.0shows the year-reset.)<patch>— a small follow-up/hotfix on the same release line (e.g.v26.1.0→v26.1.1, days later).
Upstream's final release was v26.3.0 (archived). Our line continues from there:
| Tag | Meaning |
|---|---|
v26.3.1 |
Upstream 26.3.0 rebuilt on our infra (Gitea CI/registry, fork ffmpeg base) — no application changes. A patch bump, because nothing functional changed. |
v26.4.0 |
Reserved for our first release that carries actual app changes (e.g. the #1 M3U fix). Later 2026 releases: 26.5.0, 26.6.0, …; a new year resets to 27.1.0. |
Cutting a release: push a vYY.N.P tag on main → CI builds :prod + :<version> + :<sha>; server-management does the prod switch (server-management#481).
Gotcha: never put a [skip ci] token in a commit you intend to tag — Gitea reads skip-ci from the tagged commit and will suppress the release build. (Also, workflow_dispatch on a tag ref isn't supported on this Gitea version, so the tag push must do the triggering.) Release commits, and anything you'll tag, must not contain skip-ci.
The workflow: .gitea/workflows/docker-build.yml
Single workflow, two jobs (test → build).
Triggers & tags
| Trigger | test job |
build job |
Image tags pushed |
|---|---|---|---|
pull_request |
✅ | — (skipped) | none |
push to main |
✅ | ✅ | :latest + :<short-sha> |
push tag v* |
✅ | ✅ | :prod + :<version> + :<short-sha> |
workflow_dispatch |
✅ | ✅ | only if ref is main/v*, else build-only (no push) |
:latest is the test/dev channel (every main commit). Prod pins :prod,
never :latest — enforced in the prod compose (server-management#481). :prod is
only produced by pushing a v* tag.
concurrency: { group: ersatztv-build, cancel-in-progress: false } serializes all
runs — the single runner on jazz can't safely run the push-main-then-push-v*
release flow in parallel (shared :buildcache tag, shared smoke container).
test job
dotnet restore → strip the Scanner project ref (sed -i '/Scanner/d', matching the
Docker build) → dotnet build -c Release → dotnet test -c Release --no-build. Gates
the image build.
build job
- Compute
INFO_VERSION(git describe+ short sha onmain; tag version onv*). docker/setup-buildx-actionwithbuildkitd-config-inlinesettinghttp = truefor192.168.1.95:3000— BuildKit does not inherit the host daemon'sinsecure-registries, so without this, cache/base-image/push over the HTTP registry fails (http: server gave HTTP response to HTTPS client).docker/login-actionwith repo secretsREGISTRY_USER/REGISTRY_PASSWORD.docker/build-push-action@v6: amd64-only,docker/Dockerfile,INFO_VERSIONbuild-arg, registry layer cache (type=registry,ref=…:buildcache,cache-to … ignore-error=true).- Smoke test: pull the just-pushed
:<sha>, run it, and poll ErsatzTV's web server from inside the container (docker exec … python3→http://localhost:8409/). Unique container name +trap … EXITcleanup. Fails if it never serves HTTP.
Dockerfile notes (docker/Dockerfile)
- Base image:
192.168.1.95:3000/timothy/ersatztv-ffmpeg:7.1.1(our Gitea fork of the archivedghcr.io/ersatztv/ersatztv-ffmpeg). FFmpeg 8 upgrade is backlogged: base image → ersatztv-ffmpeg#4, app-side compat → ersatztv#9. - Copies
Directory.Build.props,Directory.Build.targets,global.json,.editorconfigbeforedotnet restoreso the image build uses the same MSBuild config, SDK pin, and analyzer severities as local/CI builds (it previously copied only*.sln). - amd64-only (jazz is x86_64). No arm32/arm64, no DMG/exe artifacts, no GHCR/DockerHub.
NuGet audit
.NET 10 runs NuGet audit on restore. Several projects set TreatWarningsAsErrors=true,
so vulnerable transitive packages failed the build. Directory.Build.props demotes
low/moderate/high advisories (NU1901-1903) to warnings and promotes NU1904 (critical)
to an error in every project via WarningsAsErrors. Underlying vulnerable deps are
tracked in ersatztv#8.
Registry
Gitea Packages, HTTP-only at 192.168.1.95:3000. jazz's Docker daemon has it as an
insecure-registry (server-management#172). Images: 192.168.1.95:3000/timothy/ersatztv:<tag>.
Test / prod environments
Container/compose wiring lives in server-management (project boundary): test
ersatztv-test on 8410 (:latest), prod ersatztv on 8409 (:prod). See
server-management#481 for the full spec (registry pull on jazz, volumes, Jellyfin
isolation for test, Watchtower/manual promotion).
Retired upstream workflows
The upstream .github/workflows/ (ci.yml, docker.yml, artifacts.yml,
release.yml, pr.yml, issue-stale.yml) were removed — they targeted
GHCR/DockerHub + Azure/Apple signing and called reusable workflows at dead
ersatztv/ersatztv@main paths, and ran as noise (incl. a daily stale-issue cron) on
the Gitea runner. Upstream is archived, so there are no future merges to preserve them
for. The dead .github/dependabot.yml and FUNDING.yml (upstream-pointed) were also
removed.
Known follow-ups
- Pin third-party actions to commit SHAs (currently floating major tags cloned from github.com at runtime) — low priority for a homelab; tracked informally.