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>
114 lines
6.1 KiB
Markdown
114 lines
6.1 KiB
Markdown
# 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.0` → `v26.1.0` shows 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
|
|
|
|
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:3000` — **BuildKit 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 … python3` → `http://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.
|