Development¶
Notes for working on Coldarr itself - building, testing, and shipping a release. If you just want to run Coldarr, see the quick start.
Building¶
Go 1.26+. No other build-time dependencies - the web GUI's assets
(templates, CSS, htmx, icons) are all embedded via //go:embed, and the
release Docker image is a standard multi-stage build (see Dockerfile).
Testing¶
For Go changes, all of these should be clean before opening a PR. CI runs these checks as well:
go build ./...
go vet ./...
gofmt -l . # should print nothing
go test ./... -race
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0 run ./...
Coverage is not part of that list and is not computed by CI - the README
badge is static, refreshed locally with scripts/coverage.sh when a change
moves the number. See Contributing.
Every package has tests, including cmd/coldarr (run through its real
command line) and the internal/webui handlers (driven with httptest
against fake Radarr/Sonarr/Lidarr/Jellyfin/qBittorrent servers and a fake
OIDC provider). What
tests can't tell you is how a page looks - internal/webui is a set of Go
html/template pages with no JS framework, so verifying a UI change still
means actually running the server and looking at it:
- Build a throwaway
coldarr.yamlpointing tiers at temp directories, and (if the change needs library data) seed ahistory.jsonand/or stand up minimal fake Radarr/Sonarr HTTP servers - just enough of the Servarr v3 API surface Coldarr actually calls (GET /api/v3/movie,/tag,/qualityprofile,/queue,/movie/{id},PUT /movie/editor, and the/seriesequivalents) to exercise inventory/scoring/planning/apply end-to-end without touching a real Radarr/Sonarr instance. - Run
go run ./cmd/coldarr --config <path> serve --listen :<port>in the background. - Drive it with a headless browser (Playwright works well -
chromium-cliif available, otherwise the Python/Nodeplaywrightpackage directly) and take real screenshots, in both light and dark (prefers-color-scheme) - don't just grep the HTML. Checkconsole/pageerrorevents too; a page can render its shell while a fetch silently fails. - For anything involving the background apply flow, override
COLDARR_SETTLE_CHECK_INTERVAL/COLDARR_SETTLE_STABLE_CHECKS/COLDARR_SETTLE_MAX_WAIT(short durations, e.g.1s/1/3s) so a test run settles in seconds instead of waiting on real disk growth.
tools/gallery/run.sh --serve does steps 1, 2 and 4 for you - see below.
Screenshots (gallery harness)¶
tools/gallery/ runs Coldarr, built from your checkout, against a fake
world in one container: FUSE "drives" whose statfs reports whatever
capacity and usage fixture.yaml asks for (a 16TB satellite at 89% costs a
few KB of sparse files), and fake Radarr/Sonarr/Jellyfin/qBittorrent serving the
fixture's library and downloads. Moves really copy between those drives over a few
seconds, so apply, settle, and landing confirmation all run for real. Each
drive is its own mount, so require_mount, shared-volume detection, and
the dead-drive check behave as they do in production. It's a separate Go
module (go-fuse never touches Coldarr's own go.mod) and needs Docker
with FUSE on the host, plus Playwright for capturing.
tools/gallery/run.sh # every scene, light + dark, into docs/assets/screenshots/
tools/gallery/run.sh --only plan history # just these scenes
tools/gallery/run.sh --only downloads downloads-settings downloads-applying downloads-activity
tools/gallery/run.sh --serve # leave it running at http://127.0.0.1:18478 (password: gallery)
Scenes run in order, since some change state (applying really applies
the plan, downloads-applying moves downloads, and dead-drive unmounts a
satellite). The Downloads scenes cover cross-seeds, import checks, settling,
held downloads, tracker requirements, report-only removal, settings, and
recent activity after real moves. To change what the
screenshots show, edit fixture.yaml (drives, library, downloads, orphans, history)
and tools/gallery/coldarr.yaml (tiers, policy, schedules).
The fake qBittorrent supports the reads, moves, and seed relabels these
scenes use; it does not implement deletion. Its tracker hosts and download
names are fictional, and it never contacts a tracker. The capture checks
browser errors and the expected Downloads states as it goes. To run the
gallery module's own Go checks, use the commands above from tools/gallery/.
Documentation¶
The site uses Zensical. See Working on the documentation for installation, local previews, strict builds, and GitHub Pages setup.
CI/CD¶
.github/workflows/ci.yml- on every PR and push tomain:go build,go vet, agofmt -lcheck,go test -race,golangci-lint(config in.golangci.yml- standard linters plusbodyclose,errorlint,gosec,misspell,unconvert,unparam), and a Docker build (not pushed) that Grype scans for fixable high or critical vulnerabilities.- The security workflows - Semgrep, CodeQL, Dependency Audit (
govulncheckandpip-audit), Gitleaks, zizmor, Fuzz, ZAP, and a scheduled Container Scan of both published architectures. See Security checks for what each finds, when it runs, and how to run it locally. .github/workflows/docs.yml- builds the Zensical site in strict mode on documentation PRs and pushes tomain, and deploys successful builds frommainto GitHub Pages. See publishing setup..github/workflows/renovate.yml- runs Renovate (config in.github/renovate.json5) hourly, after every push tomain, and when CI or another PR workflow finishes on arenovate/*branch. It keeps Go modules, GitHub Actions (pinned by commit), the Dockerfiles' base images, the scanners the workflows pin, and the documentation dependency up to date. Minor and patch updates share one PR, which merges on its own once CI passes and the release is 3 days old; majors wait for approval on the Dependency Dashboard issue. Security fixes skip the wait: Renovate opens them from the repo's Dependabot alerts and from osv.dev, and Dependabot's own security-update PRs are off so each fix arrives once. The alerts and GitHub's CodeQL default setup are configured separately under repo Settings > Security (no workflow file needed for those); the CodeQL workflow is a separate harness that fails PRs on findings.-
.github/workflows/release.yml- on publishing a GitHub Release: builds the amd64 and arm64 images and scans each with Grype, then pushes the multi-arch (amd64/arm64) image to bothghcr.io/voc0der/coldarranddocker.io/voc0der/coldarr, tagged with the release version, itsmajor.minor, andlatest(skipped for prereleases). Also runnable manually viaworkflow_dispatch. GHCR authenticates with the repo's built-inGITHUB_TOKEN- no setup needed. Docker Hub needs two repo-level settings under Settings > Secrets and variables > Actions:- Variable
DOCKERHUB_USERNAME- the Docker Hub username (voc0der) - Secret
DOCKERHUB_TOKEN- a Docker Hub access token (Account Settings > Security > Personal access tokens on hub.docker.com; scope it to "Read & Write" on thecoldarrrepo, not a full account password)
- Variable
Contributing¶
See Contributing for branch/commit/PR naming. The short version: one focused change per branch/PR - don't bundle unrelated fixes/features together, even if they happened to be worked on in the same sitting.
Releasing¶
"Releasing" means more than merging to main - it means cutting an actual
versioned GitHub Release, because that's what release.yml listens for to
build and publish the Docker image. To ship a new version:
- Make sure everything landing in the release is merged to
mainas separate, focused PRs (per Contributing above). - Decide the version bump: patch for pure fixes, minor for any new
feature, following semver off the latest tag
(
git tag --sort=-v:refname | head -1orgh release list --limit 1). -
Cut the release:
Title format:
vX.Y.Z - short summary, joining multiple unrelated changes with " + " if a release bundles more than one (e.g.v0.5.0 - safe concurrent apply + favicon). Notes are hand-written markdown -## Highlightsor## Fixsections, a bold lead-in per change, and why it matters, not just what changed. Look at past releases (gh release view vX.Y.Z) for the tone to match. -
The image build/push happens automatically from there - no manual Docker steps. If its vulnerability scan fails, nothing is pushed; see Security checks.
Roadmap (not yet implemented)¶
- Plex support alongside Jellyfin
- Jellyfin play-history/play-count and request-history (Jellyseerr/ Overseerr) as scoring inputs (Favorites are already in)
- Download clients other than qBittorrent, ratio-based seeding rules, and sending downloads to different seed drives by label
- Editing policy thresholds (tags, cooldown, score threshold) through the GUI, not just tiers/connections/notifications/scheduler
- Translations for the web GUI - it's English-only today, with strings
written directly into the
html/templatepages rather than pulled from a message catalog. Planned approach is Weblate (hosted, free for open source), which would need: extracting GUI strings into a catalog format it can translate (e.g. go-i18n/gotext), a Weblate component pointed at this repo, and a language switcher in the GUI. Not started - no catalog, no Weblate project, no switcher yet.