Files
ersatztv/docs/ci-cd.md
T
timothyandClaude Opus 4.8 c7aad4689e docs: enshrine the fork versioning scheme
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>
2026-06-27 00:29:13 +02:00

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.0 shipped in June 2025, v25.5.0 in Sep, v26.3.0 in Feb 2026 — minors don't track months; and v25.9.0v26.1.0 shows the year-reset.)
  • <patch> — a small follow-up/hotfix on the same release line (e.g. v26.1.0v26.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 (testbuild).

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 Releasedotnet test -c Release --no-build. Gates the image build.

build job

  1. Compute INFO_VERSION (git describe + short sha on main; tag version on v*).
  2. docker/setup-buildx-action with buildkitd-config-inline setting http = true for 192.168.1.95:3000BuildKit does not inherit the host daemon's insecure-registries, so without this, cache/base-image/push over the HTTP registry fails (http: server gave HTTP response to HTTPS client).
  3. docker/login-action with repo secrets REGISTRY_USER / REGISTRY_PASSWORD.
  4. docker/build-push-action@v6: amd64-only, docker/Dockerfile, INFO_VERSION build-arg, registry layer cache (type=registry,ref=…:buildcache, cache-to … ignore-error=true).
  5. Smoke test: pull the just-pushed :<sha>, run it, and poll ErsatzTV's web server from inside the container (docker exec … python3http://localhost:8409/). Unique container name + trap … EXIT cleanup. 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 archived ghcr.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, .editorconfig before dotnet restore so 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.