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:
@@ -0,0 +1,103 @@
|
|||||||
|
name: Bug report
|
||||||
|
about: Something behaves incorrectly
|
||||||
|
title: ""
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Security vulnerabilities do **not** go here — see
|
||||||
|
[SECURITY.md](../../SECURITY.md).
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened
|
||||||
|
description: What you did, what you expected, and what you got instead.
|
||||||
|
placeholder: |
|
||||||
|
1. Opened an album from the Music library
|
||||||
|
2. Tapped the third track
|
||||||
|
3. Playback started from the first track instead
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: JellyTau version
|
||||||
|
description: Settings scrolls to the bottom, or the filename you installed.
|
||||||
|
placeholder: "0.9.1"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
options:
|
||||||
|
- Linux (AppImage)
|
||||||
|
- Linux (deb)
|
||||||
|
- Linux (rpm)
|
||||||
|
- Linux (Arch package)
|
||||||
|
- Windows
|
||||||
|
- Android
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
### Playback questions
|
||||||
|
|
||||||
|
If this involves playback, these three answers decide which of several
|
||||||
|
very different code paths you were on. "I don't know" is a fine answer.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: source
|
||||||
|
attributes:
|
||||||
|
label: Was the media streaming or downloaded?
|
||||||
|
options:
|
||||||
|
- Streaming from the server
|
||||||
|
- Downloaded for offline use
|
||||||
|
- Not playback-related
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: transcode
|
||||||
|
attributes:
|
||||||
|
label: Was the server transcoding?
|
||||||
|
description: Jellyfin's dashboard shows this while something is playing.
|
||||||
|
options:
|
||||||
|
- Direct play
|
||||||
|
- Transcoding
|
||||||
|
- Don't know
|
||||||
|
- Not playback-related
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: kind
|
||||||
|
attributes:
|
||||||
|
label: Music or video?
|
||||||
|
options:
|
||||||
|
- Music
|
||||||
|
- Video (movie)
|
||||||
|
- Video (TV episode)
|
||||||
|
- Not playback-related
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs
|
||||||
|
description: |
|
||||||
|
Android: `adb logcat | grep -i jellytau`.
|
||||||
|
Linux: run from a terminal, or `RUST_LOG=debug jellytau` for more.
|
||||||
|
In the app, `localStorage.setItem("jellytau:logLevel","debug")` in the
|
||||||
|
webview console turns the frontend up too.
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: server
|
||||||
|
attributes:
|
||||||
|
label: Jellyfin server
|
||||||
|
description: Version, and anything unusual about the library layout.
|
||||||
|
placeholder: "10.9.11, series stored without season folders"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Feature request
|
||||||
|
about: Suggest something JellyTau should do
|
||||||
|
title: ""
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: What are you trying to do?
|
||||||
|
description: |
|
||||||
|
The situation, not the solution. "I listen to albums in a fixed order and
|
||||||
|
lose my place when I switch devices" tells us more than "add a sync
|
||||||
|
button", and often has a better answer than the one you had in mind.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposal
|
||||||
|
attributes:
|
||||||
|
label: What would you like it to do?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Which platforms does this matter on?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- Windows
|
||||||
|
- Android
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Anything you have tried, or how other clients handle it
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
## What and why
|
||||||
|
|
||||||
|
<!-- What changes, and the reason. The diff shows the what; the why is what
|
||||||
|
the commit log is for. -->
|
||||||
|
|
||||||
|
## How it was verified
|
||||||
|
|
||||||
|
<!-- What you actually ran or clicked. "Tests pass" on its own says little;
|
||||||
|
"played a transcoded episode on Android, seeked twice, backgrounded it"
|
||||||
|
says a lot. -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] `bun run check`, `bun run test`, `bun run format:check`, `bun run lint`
|
||||||
|
- [ ] `cargo fmt`, `cargo clippy --all-targets -- -D warnings`, `cargo test`
|
||||||
|
- [ ] `bun run check:boundary` — no Jellyfin taxonomy in the frontend
|
||||||
|
- [ ] New requirement-implementing code carries a `TRACES:` comment, and every
|
||||||
|
ID it names exists in `docs/requirements.md` (`bun run traces:validate`)
|
||||||
|
- [ ] **Bug fix:** a test that reproduces it was written *first* and observed
|
||||||
|
failing before the fix
|
||||||
|
- [ ] Android source edits were made in `src-tauri/android/src` and synced with
|
||||||
|
`scripts/sync-android-sources.sh` (never edit `gen/` directly)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## The short version
|
||||||
|
|
||||||
|
Be decent to people. Assume the person you are talking to is acting in good
|
||||||
|
faith and knows things you do not.
|
||||||
|
|
||||||
|
## What that means here
|
||||||
|
|
||||||
|
**Expected:**
|
||||||
|
|
||||||
|
- Criticise code, decisions and ideas — not the people who wrote them.
|
||||||
|
- Accept that "no" is a complete answer. This is a small project with a
|
||||||
|
maintainer who has finite time; a declined feature request is not a slight.
|
||||||
|
- Give people room to be new. Everyone was once confused by Tauri's IPC.
|
||||||
|
- Assume a bug report is someone trying to help, even when it arrives terse or
|
||||||
|
frustrated.
|
||||||
|
|
||||||
|
**Not accepted:**
|
||||||
|
|
||||||
|
- Harassment, personal attacks, or demeaning remarks — including about someone's
|
||||||
|
identity, background, or level of experience.
|
||||||
|
- Sexualised language or imagery, and unwelcome attention of any kind.
|
||||||
|
- Publishing someone's private information without their permission.
|
||||||
|
- Persistently derailing discussions, or badgering people who have already
|
||||||
|
answered you.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This applies in the issue tracker, pull requests, commit messages and any other
|
||||||
|
project space, and to anyone taking part — maintainer included.
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
Email **duncan@tourolle.paris**. Reports are read by the maintainer and handled
|
||||||
|
privately.
|
||||||
|
|
||||||
|
Responses range from a quiet word through to removing comments or blocking an
|
||||||
|
account, depending on what happened. If a report concerns the maintainer, and
|
||||||
|
that makes reporting to them pointless, you are free to say so publicly — a
|
||||||
|
project this size has no separate committee to appeal to, and pretending
|
||||||
|
otherwise would be dishonest.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
Adapted in spirit from the [Contributor Covenant](https://www.contributor-covenant.org),
|
||||||
|
shortened to what a single-maintainer project can actually honour.
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
# Contributing to JellyTau
|
||||||
|
|
||||||
|
Thanks for looking. This file is the short version of how the project is built
|
||||||
|
and what has to be true before a change lands. The long version lives in
|
||||||
|
[CLAUDE.md](CLAUDE.md) and [docs/architecture/](docs/architecture/README.md),
|
||||||
|
which are maintained rather than decorative — read them before a structural
|
||||||
|
change.
|
||||||
|
|
||||||
|
## Getting set up
|
||||||
|
|
||||||
|
Package manager is **bun**. You will also need a Rust toolchain (the exact
|
||||||
|
version is pinned in [src-tauri/rust-toolchain.toml](src-tauri/rust-toolchain.toml)
|
||||||
|
— rustup honours it automatically) and the Tauri Linux dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
bun run hooks:install # do this once: it enables the pre-commit gates
|
||||||
|
bun run tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
`hooks:install` points `core.hooksPath` at [scripts/hooks/](scripts/hooks/), so
|
||||||
|
hook updates arrive with a `git pull` instead of needing a re-install.
|
||||||
|
|
||||||
|
## What has to pass
|
||||||
|
|
||||||
|
Everything below runs in CI, and the fast half runs in the pre-commit hook. None
|
||||||
|
of it is advisory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run check # svelte-check — 0 errors
|
||||||
|
bun run test # vitest
|
||||||
|
bun run format:check # prettier
|
||||||
|
bun run lint # eslint — 0 errors; the warning count is a ratchet
|
||||||
|
bun run check:boundary # no Jellyfin taxonomy in the frontend
|
||||||
|
bun run test:rust # cargo test
|
||||||
|
cd src-tauri && cargo fmt --all && cargo clippy --all-targets -- -D warnings
|
||||||
|
cd src-tauri && cargo deny check # advisories, licences, bans, sources
|
||||||
|
```
|
||||||
|
|
||||||
|
`bun run test:all` runs the whole set.
|
||||||
|
|
||||||
|
Several of these are **ratchets** — a number that only ever moves in the
|
||||||
|
improving direction:
|
||||||
|
|
||||||
|
| Ratchet | Where | Rule |
|
||||||
|
|---|---|---|
|
||||||
|
| eslint `--max-warnings` | [.gitea/workflows/build-and-test.yml](.gitea/workflows/build-and-test.yml) | only goes down |
|
||||||
|
| Coverage thresholds | [vitest.config.ts](vitest.config.ts) | only go up |
|
||||||
|
| Traceability coverage | [.gitea/workflows/traceability-check.yml](.gitea/workflows/traceability-check.yml) | only goes up |
|
||||||
|
|
||||||
|
Never relax one to make a build pass. Fix the thing it caught.
|
||||||
|
|
||||||
|
## The two rules that surprise people
|
||||||
|
|
||||||
|
**1. Bug fixes start with a failing test.** Write a test that reproduces the bug
|
||||||
|
and *watch it fail* before you touch the fix. A test written against
|
||||||
|
already-fixed code can pass for the wrong reason and guards nothing. If the logic
|
||||||
|
is trapped in a component, extract the pure part into a plain `.ts` module and
|
||||||
|
test that — see `episodeStrip.ts` or `TrackList.logic.ts` for the pattern.
|
||||||
|
|
||||||
|
**2. Domain vocabulary lives in Rust.** The frontend is presentation-only. It
|
||||||
|
must not encode Jellyfin's *taxonomy* — for example, the set of item types that
|
||||||
|
makes up a category like "Music". Send an opaque scope across the IPC boundary
|
||||||
|
and let the backend expand it. `bun run check:boundary` is a tripwire, not a
|
||||||
|
proof: it only flags item-type array literals, so a green run does not mean you
|
||||||
|
are clear. [docs/specs/scoped-search-boundary.md](docs/specs/scoped-search-boundary.md)
|
||||||
|
describes the leak that made this a rule.
|
||||||
|
|
||||||
|
## Traceability
|
||||||
|
|
||||||
|
Code that implements a requirement carries a `TRACES:` comment naming the
|
||||||
|
requirement IDs, and a tool builds the matrix from those comments:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// TRACES: UR-005 | DR-001
|
||||||
|
```
|
||||||
|
|
||||||
|
Every ID must exist as a row in [docs/requirements.md](docs/requirements.md) —
|
||||||
|
`bun run traces:validate` fails on a typo or a stale rename. Internal helpers and
|
||||||
|
requirement-less code stay untraced; do not sprinkle IDs to raise the number.
|
||||||
|
|
||||||
|
If you add a requirement, add its row. If you implement one, tag the code.
|
||||||
|
|
||||||
|
## Commits and pull requests
|
||||||
|
|
||||||
|
- Conventional-commit subjects: `fix(player): …`, `feat(updater): …`, `ci: …`.
|
||||||
|
- Explain **why** in the body, not what the diff already shows. The commit log
|
||||||
|
is the main record of why things are the way they are here, and it is used to
|
||||||
|
draft release notes.
|
||||||
|
- One concern per commit. A formatting sweep and a behaviour change in the same
|
||||||
|
commit is unreviewable.
|
||||||
|
- Rebase rather than merge-commit onto `master`.
|
||||||
|
|
||||||
|
## Specs
|
||||||
|
|
||||||
|
New features start from [docs/specs/SPEC-TEMPLATE.md](docs/specs/SPEC-TEMPLATE.md).
|
||||||
|
Its "Layer assignment" section is the point: each piece of logic gets placed in
|
||||||
|
Rust or the frontend *with a reason*. Review against
|
||||||
|
[docs/specs/SPEC-REVIEW-CHECKLIST.md](docs/specs/SPEC-REVIEW-CHECKLIST.md). Do
|
||||||
|
not frame a spec around "no Rust changes required" — correct placement is the
|
||||||
|
goal, not minimal backend churn.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
CI is **Gitea Actions** (`.gitea/workflows/`), not GitHub.
|
||||||
|
|
||||||
|
🔴 **CI installs no system tools.** Every build, test and packaging tool must
|
||||||
|
already be in the Docker builder image. If a job needs a tool the image lacks,
|
||||||
|
add it to [Dockerfile.builder](Dockerfile.builder), rebuild and push the image,
|
||||||
|
and pin the new tag — do not `apt-get` it at job time. Details in
|
||||||
|
[docs/build/ci-operations.md](docs/build/ci-operations.md).
|
||||||
|
|
||||||
|
Fetching the project's own declared dependencies (`bun install`, cargo crates,
|
||||||
|
an advisory database) is not a toolchain install and is fine.
|
||||||
|
|
||||||
|
## Reporting bugs
|
||||||
|
|
||||||
|
Use the issue templates. For anything involving playback, include what the
|
||||||
|
platform was, whether the media was streaming or downloaded, and whether it was
|
||||||
|
transcoding — those three answers determine which of several code paths you were
|
||||||
|
actually on.
|
||||||
|
|
||||||
|
Security issues go to [SECURITY.md](SECURITY.md), not the tracker.
|
||||||
@@ -47,6 +47,29 @@ For the full set of build, test, and Android helper scripts, see
|
|||||||
| Traceability tooling & CI | [docs/traceability.md](docs/traceability.md), [docs/traceability-ci.md](docs/traceability-ci.md) |
|
| Traceability tooling & CI | [docs/traceability.md](docs/traceability.md), [docs/traceability-ci.md](docs/traceability-ci.md) |
|
||||||
| Release checklist | [docs/release-checklist.md](docs/release-checklist.md) |
|
| Release checklist | [docs/release-checklist.md](docs/release-checklist.md) |
|
||||||
| UX flows | [docs/ux-flows.md](docs/ux-flows.md) |
|
| UX flows | [docs/ux-flows.md](docs/ux-flows.md) |
|
||||||
|
| CI operations (builder image, secrets, runner) | [docs/build/ci-operations.md](docs/build/ci-operations.md) |
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
[CONTRIBUTING.md](CONTRIBUTING.md) covers the setup, the gates a change has to
|
||||||
|
pass, and the two rules that catch people out (bug fixes start with a failing
|
||||||
|
test; Jellyfin's taxonomy stays in Rust). Please also read the
|
||||||
|
[Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
Found a security problem? Do not open an issue — see [SECURITY.md](SECURITY.md).
|
||||||
|
|
||||||
|
## Verifying a download
|
||||||
|
|
||||||
|
Every release publishes `SHA256SUMS` covering all of its artifacts, plus an SBOM
|
||||||
|
of what went into the build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sha256sum -c SHA256SUMS
|
||||||
|
```
|
||||||
|
|
||||||
|
Desktop builds update themselves from Settings → Updates, verifying each payload
|
||||||
|
against JellyTau's signing key before installing. Android installs are handled by
|
||||||
|
the system installer, so the app links to the releases page instead.
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
|||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
Email **duncan@tourolle.paris** with `[JellyTau security]` in the subject.
|
||||||
|
Please do **not** open a public issue for a vulnerability — JellyTau handles
|
||||||
|
Jellyfin credentials and media, and an unfixed issue in a public tracker is an
|
||||||
|
advisory for everyone running it.
|
||||||
|
|
||||||
|
Include what you have: what the problem is, how to reproduce it, the version and
|
||||||
|
platform, and what you think an attacker could do with it. A rough report is
|
||||||
|
worth more than a polished one that never gets sent.
|
||||||
|
|
||||||
|
You can expect an acknowledgement within a week. If a fix is warranted it will
|
||||||
|
ship in the next release, and you will be credited in the release notes unless
|
||||||
|
you would rather not be.
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
JellyTau is a single-maintainer project without long-term support branches.
|
||||||
|
**Only the latest release receives fixes.** Desktop builds can update themselves
|
||||||
|
(Settings → Updates); on Android, install the latest APK from the releases page.
|
||||||
|
|
||||||
|
## What is in scope
|
||||||
|
|
||||||
|
The application and its build pipeline:
|
||||||
|
|
||||||
|
- The Tauri backend (`src-tauri/`) and the Svelte frontend (`src/`)
|
||||||
|
- Credential storage — the system keyring and its encrypted-file fallback
|
||||||
|
- The Android player service and its JNI bridge
|
||||||
|
- The loopback media server used for downloaded playback
|
||||||
|
- The release pipeline: artifact signing, the update manifest, the builder image
|
||||||
|
|
||||||
|
**Out of scope:** vulnerabilities in Jellyfin itself (report those to the
|
||||||
|
Jellyfin project), and issues that require an already-compromised device or a
|
||||||
|
malicious server the user deliberately configured and trusted.
|
||||||
|
|
||||||
|
## What the project already does
|
||||||
|
|
||||||
|
Not a guarantee, but so you know what has been considered:
|
||||||
|
|
||||||
|
- **Credentials** never go in plaintext config: the system keyring is used where
|
||||||
|
available, with an AES-GCM encrypted file as fallback (see
|
||||||
|
[docs/architecture/09-security.md](docs/architecture/09-security.md)).
|
||||||
|
- **The webview runs under a restrictive CSP**, and the asset protocol is scoped
|
||||||
|
to the thumbnail cache directory only.
|
||||||
|
- **Path confinement** is enforced on the cache and download roots — a
|
||||||
|
server-supplied id cannot decide where a file lands (DR-210, DR-211).
|
||||||
|
- **Queries and URLs bind or encode their inputs** rather than interpolating
|
||||||
|
them (DR-212).
|
||||||
|
- **Dependencies are scanned on every build** by `cargo deny` against the RustSec
|
||||||
|
advisory database, and licence-checked against an allow-list (DR-216).
|
||||||
|
- **Releases carry `SHA256SUMS` and an SBOM**, so you can verify a download and
|
||||||
|
find out what went into it.
|
||||||
|
- **Desktop updates are signature-verified** against a key held only in CI before
|
||||||
|
anything is installed (DR-217).
|
||||||
|
|
||||||
|
Windows installers are **not** Authenticode-signed — SmartScreen will warn on
|
||||||
|
first run. That is a cost and identity problem, not an oversight; verify the
|
||||||
|
download against `SHA256SUMS` instead.
|
||||||
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