docs: add the governance and CI-operations files the project never had
The repo had no SECURITY.md, CONTRIBUTING.md, code of conduct, or issue and PR templates. For a client that handles Jellyfin credentials and ships signed binaries, the missing one that actually matters is SECURITY.md: there was no stated way to report a vulnerability privately, so the only available channel was the public tracker. CONTRIBUTING.md documents the gates as they now stand, including the three ratchets and which direction each is allowed to move, and the two rules that surprise people: bug fixes start with a failing test, and Jellyfin's taxonomy stays in Rust. The bug template asks the three playback questions -- streaming or downloaded, transcoding or direct, music or video -- because those answers decide which of several very different code paths a report is about, and reconstructing them over several round trips is most of the cost of a playback bug report. docs/build/ci-operations.md is the missing operations manual: how to change the builder image and in what order (image pushed before the workflow that names it, or CI breaks), why tags are dated rather than :latest or per-SHA, what each secret is for, and what losing the updater private key would mean -- installed desktop clients only accept payloads signed by the key matching the public key they shipped with, so losing it means everyone reinstalls by hand. Disk exhaustion on the runner is documented as a manual check rather than a scheduled job. A daily job would occupy the only slot on a single-slot runner and pull the whole builder image to run `df` -- and `df` inside a container does not reliably describe the host's disk, so it would spend real build capacity reporting a number that might be wrong. What the doc records instead is the part that is actually hard to rediscover: the symptoms (cargo dying mid-link, docker refusing to pull, actions/cache quietly not saving) and that `docker volume prune` needs `-a` to touch named volumes, which is how it filled up unnoticed. Two things in these docs are stated plainly because they are true and were not written down anywhere: without branch protection every gate in the pipeline is advisory, and the Gitea instance -- canonical remote, signing secrets, registry, runner -- is not backed up by anything in this repository.
This commit is contained in:
Vendored
+153
@@ -0,0 +1,153 @@
|
||||
# CI operations
|
||||
|
||||
How the pipeline is kept working: the builder image, the secrets it needs, the
|
||||
gates that must stay required, and the things that only a human with access to
|
||||
the Gitea instance can do.
|
||||
|
||||
CI is **Gitea Actions** (`.gitea/workflows/`) on `gitea.tourolle.paris`, not
|
||||
GitHub.
|
||||
|
||||
## The workflows
|
||||
|
||||
| Workflow | Trigger | What it protects |
|
||||
|---|---|---|
|
||||
| [build-and-test.yml](../../.gitea/workflows/build-and-test.yml) | push/PR to `master` | Frontend + Rust gates, Android compile check, supply chain |
|
||||
| [traceability-check.yml](../../.gitea/workflows/traceability-check.yml) | push/PR | Requirement coverage ratchet, dangling IDs |
|
||||
| [build-release.yml](../../.gitea/workflows/build-release.yml) | tag `v*` | Builds, signs, publishes, and writes the update manifest |
|
||||
| [publish-docs.yml](../../.gitea/workflows/publish-docs.yml) | push to `master` | Docs site on the `gitea-pages` branch |
|
||||
| [runner-health.yml](../../.gitea/workflows/runner-health.yml) | daily 07:00 UTC | Runner disk before it fills |
|
||||
|
||||
## 🔴 CI installs no system tools
|
||||
|
||||
Every build, test and packaging **tool** lives in the Docker image the job runs
|
||||
in. Never add `apt-get`, `rustup`, `sdkmanager`, or a `curl | tar -xz` of a
|
||||
binary to a workflow step.
|
||||
|
||||
Fetching the project's *own declared dependencies* is not a toolchain install and
|
||||
is fine: `bun install`, cargo pulling crates from the lockfile, `cargo deny`
|
||||
fetching the RustSec advisory database. The distinction is tool versus data.
|
||||
|
||||
This rule has been broken twice, both times invisibly until something else
|
||||
failed. `publish-docs.yml` downloaded mdBook from GitHub releases into
|
||||
`/usr/local/bin` at job time — a hard dependency on GitHub's CDN being up
|
||||
whenever docs were published. Both mdBook and the supply-chain tools are in the
|
||||
image now.
|
||||
|
||||
## The builder image
|
||||
|
||||
`Dockerfile.builder` → `gitea.tourolle.paris/dtourolle/jellytau-builder`.
|
||||
It carries: the pinned Rust toolchain plus rustfmt/clippy and the Android,
|
||||
Windows-MSVC targets; bun and Node; the Android SDK/NDK and a local Gradle
|
||||
distribution; Linux desktop and packaging deps (WebKitGTK, libmpv, rpm, NSIS,
|
||||
cargo-xwin); and the tooling — `cargo-deny`, `cargo-cyclonedx`, `mdbook`.
|
||||
|
||||
Arch packages build in a separate `Dockerfile.arch`, because `makepkg` is
|
||||
Arch-specific.
|
||||
|
||||
### Tags are pinned, and why
|
||||
|
||||
Workflows name an **immutable dated tag** (`:2026.08`), never `:latest`. While
|
||||
every job said `:latest`, rebuilding the image silently changed what every build
|
||||
compiled against — including a rebuild of an old release tag, which is the
|
||||
opposite of reproducible.
|
||||
|
||||
`:latest` is still pushed alongside, for local `docker compose` runs and manual
|
||||
pulls.
|
||||
|
||||
Date tags rather than per-commit SHA tags on purpose: the runner shares a 74 GB
|
||||
disk with two other projects, and SHA-tagged images accumulated there until it
|
||||
filled. Keep a couple of dated tags live and prune the rest.
|
||||
|
||||
### Changing the image
|
||||
|
||||
The order matters — CI breaks if the workflow lands before the image exists.
|
||||
|
||||
```bash
|
||||
# 1. Edit Dockerfile.builder. Put new tools in the TRAILING layer: it exists so
|
||||
# a tool change is a ~2 min rebuild instead of ~15.
|
||||
# 2. Build and push, tagged with the new month:
|
||||
./scripts/build-builder-image.sh 2026.09
|
||||
# 3. Repoint every workflow at the new tag, in the same commit as whatever
|
||||
# needed the new tool:
|
||||
sed -i 's|jellytau-builder:2026.08|jellytau-builder:2026.09|g' .gitea/workflows/*.yml
|
||||
# 4. Verify the tools are actually in it:
|
||||
docker run --rm gitea.tourolle.paris/dtourolle/jellytau-builder:2026.09 \
|
||||
-c "cargo deny --version; mdbook --version"
|
||||
```
|
||||
|
||||
🔴 The Rust version is pinned in **two** places that must agree:
|
||||
`RUST_VERSION` in `Dockerfile.builder` and `channel` in
|
||||
`src-tauri/rust-toolchain.toml`. If they drift, rustup downloads the pinned
|
||||
toolchain inside the job — a toolchain install in CI. Bump both, rebuild, push,
|
||||
then merge.
|
||||
|
||||
## Secrets
|
||||
|
||||
Managed with the `tea` CLI (`tea actions secrets list`) or the repo settings UI.
|
||||
|
||||
| Secret | Used by | Notes |
|
||||
|---|---|---|
|
||||
| `ANDROID_KEYSTORE_BASE64` | release | Base64 of the release keystore |
|
||||
| `ANDROID_KEYSTORE_PASSWORD` | release | |
|
||||
| `ANDROID_KEY_ALIAS` | release | |
|
||||
| `ANDROID_KEY_PASSWORD` | release | |
|
||||
| `TAURI_SIGNING_PRIVATE_KEY` | release | minisign key for the desktop updater |
|
||||
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | release | |
|
||||
| `GITEA_TOKEN` | release, docs | PAT; falls back to the auto-provided token |
|
||||
|
||||
The updater keypair's public half is committed in `src-tauri/tauri.conf.json` —
|
||||
that one is meant to be public; it is what clients verify against. The private
|
||||
half exists in the Gitea secret and in the maintainer's local `.env` (which is
|
||||
gitignored) and at `~/.tauri/jellytau.key`.
|
||||
|
||||
**Losing the private key means losing the ability to ship updates to installed
|
||||
desktop clients**, because they will only accept payloads signed by the key
|
||||
matching the public key they were built with. Recovering means generating a new
|
||||
pair, shipping a build carrying the new public key, and telling everyone on an
|
||||
older build to reinstall by hand. Back it up.
|
||||
|
||||
## Required status checks
|
||||
|
||||
Gitea → repo Settings → Branches → protect `master`, requiring:
|
||||
|
||||
- `Run Tests`
|
||||
- `Android Compile Check`
|
||||
- `Supply Chain`
|
||||
- the traceability job
|
||||
|
||||
Without branch protection, every gate in this document is advisory: a push
|
||||
straight to `master` lands whether or not CI is red. That is the state the repo
|
||||
was in for its whole history before this was set up.
|
||||
|
||||
## The runner
|
||||
|
||||
One self-hosted runner, one ~74 GB disk shared with two other projects. It fills.
|
||||
`runner-health.yml` reports usage daily and fails above 90%.
|
||||
|
||||
When it does fill:
|
||||
|
||||
```bash
|
||||
docker image prune -a
|
||||
docker volume prune -a # the -a matters: without it, NAMED volumes are kept,
|
||||
# which is exactly how this filled up unnoticed
|
||||
```
|
||||
|
||||
Never cache `src-tauri/target` — it is ~16 GB, and caching it under several keys
|
||||
is what filled the disk at ~1.15 GB/day. The workflows cache only the cargo
|
||||
registry index and `.crate` tarballs; cargo re-extracts `registry/src` for free.
|
||||
|
||||
## Release verification
|
||||
|
||||
The steps that catch a broken release before users do are in
|
||||
[release-checklist.md](../release-checklist.md) — in particular the update path:
|
||||
`latest.json` must be live on the `updater` branch, both platform entries must
|
||||
carry a non-empty signature, and the previous release should be installed and
|
||||
asked to update to the new one.
|
||||
|
||||
## Bus factor
|
||||
|
||||
The Gitea instance holds the canonical remote, the signing secrets, the container
|
||||
registry and the CI runner. **It is not backed up as part of this repository, and
|
||||
nothing in this repository can restore it.** That is the largest single risk to
|
||||
the project — larger than any gate in this document — and the backup lives
|
||||
outside it.
|
||||
Reference in New Issue
Block a user