Release pipeline
AudioSilo ships from three pipelines that hang off the same v* tag, plus the
manager's desktop builds. The structural rule that ties them together:
The web image publishes first. Both the server Docker image and the native binaries consume a pinned
audiosilo-webimage; building them before the web image exists (or is current) bakes in the wrong player.
This page explains how the machinery fits together. The step-by-step operator runbook is Releasing.
The pipeline
1. Web image - audiosilo-frontend/.github/workflows/web.yml
Runs on pushes to main and on v* tags (plus manual dispatch). It exports
the player as a static site (npx expo export --platform web --output-dir dist -
app.json experiments.baseUrl: "/web" makes asset URLs resolve under the
server's /web mount) and packages the export into a tiny image via
Dockerfile.web, pushed as ghcr.io/<owner>/audiosilo-web.
Tagging: :latest only from the default branch, a semver tag on v* tags, and a
:sha tag always. GHCR references must be lowercase; the workflows lowercase
${{ github.repository_owner }} themselves.
2. Server Docker image - audiosilo-server/.github/workflows/image.yml
Runs on v* tags (plus manual dispatch with a web_version input, default
latest). The multi-stage Dockerfile:
- builds the CGO-free server binary (
CGO_ENABLED=0,-trimpath), stamping the release version via-ldflags "-X github.com/kodestar/audiosilo-server/internal/api.Version=${VERSION}"(image.ymlpasses the tag as theVERSIONbuild-arg; the default isdev); - pins the player:
FROM ${WEB_IMAGE} AS web…COPY --from=web /web /app/web, withARG WEB_IMAGE=ghcr.io/kodestar/audiosilo-web:latest; - final stage is
alpine:3.20with ffmpeg installed from apk - the Docker image, unlike the native binaries, does bundle ffmpeg - plus aPUID/PGIDentrypoint andAUDIOSILO_WEB_DIR=/app/webpreset.
Because the image pins one specific web build, the server + bundled player always
ship as a known-compatible pair; native apps talking to any server version
negotiate via the GET /api/v1/server capability flags instead.
3. Native binaries - release.yml + .goreleaser.yml
The same v* tag triggers GoReleaser on a single Linux runner. The server is
CGO-free (modernc SQLite), so everything cross-compiles with no C toolchain:
- Targets:
linux,darwin,windows×amd64,arm64(six binaries). - Archives:
.tar.gz(Linux/macOS) and.zip(Windows), namedaudiosilo_<version>_<os>_<arch>, containing the binary plus README/LICENSE/RELEASING. - Linux packages:
.deb/.rpm(nfpm) that depend on the distro's ffmpeg and install a systemd unit (packaging/systemd/audiosilo.service). checksums.txt, and the release is created as a draft for human review before publishing.- Version stamping: the same ldflags as the Docker build -
-X …/internal/api.Version={{ .Version }}overridesvar Version = "dev"ininternal/api/api.go, andGET /server, the admin console, and the web player all report it.
Embedding the player: -tags embedplayer
Native binaries can't mount a baked-in /app/web directory, so the player is
compiled into the binary:
- A GoReleaser
beforehook runsscripts/fetch-web-player.sh, whichdocker create+docker cps the static export out of the pinnedaudiosilo-webimage (theWEB_IMAGEenv var, set byrelease.ymlfrom the lowercased owner + theweb_versiondispatch input, defaultlatest) intointernal/web/player/(gitignored), and fails ifindex.htmlis missing. - Builds use
-tags=embedplayer:internal/web/player_embed.goembeds that directory, while the defaultplayer_disk.go(//go:build !embedplayer) serves fromweb_dirat runtime. The embedded player takes precedence overweb_dir, so/webworks with zero configuration. - Local snapshot builds skip the hook (
goreleaser build --snapshot --clean --skip=before --single-target); a committedinternal/web/player/.gitkeepkeeps the embed compiling without a real player.
ffmpeg/ffprobe are NOT bundled
They're large (~80 MB each) and usually already installed, so the archives stay
small. At startup pkg/launcher (resolveTools in pkg/launcher/app.go)
resolves each tool in order:
- an explicit
--ffmpeg/--ffprobepath, - a copy sitting next to the binary,
$PATH,- only if none is found:
internal/toolfetchdownloads a static build over HTTPS into<data>/tools, self-checks it by running-version, and caches it.
Offline or on an unsupported platform, the server simply runs without
transcoding/probing (both are optional by design) and retries on the next start.
The .deb/.rpm packages sidestep all of this by declaring a dependency on the
distro's ffmpeg.
4. Manager desktop builds - audiosilo-manager/.github/workflows/desktop.yml
The Wails UI can't cross-compile, so desktop.yml runs a per-OS matrix on v*
tags: darwin/universal on macOS, windows/amd64 on Windows, linux/amd64 on
Ubuntu. Each job checks out both repos as siblings (the manager depends on the
server module via a local replace, for pkg/launcher and pkg/match), runs
wails build -platform <os>/<arch> and stamps the version with
-ldflags "-X main.version=<tag>".
:::caution Installers are planned, not shipped
Today the workflow uploads the build outputs as unsigned CI artifacts - it
does not publish a GitHub Release. The signed installers described in the
workspace DISTRIBUTION.md (macOS .dmg + notarization, Windows NSIS .exe,
Linux AppImage) are planned: the signing/notarization steps are stubbed in
desktop.yml pending an Apple Developer ID certificate and a Windows
Authenticode certificate. Until then, downloaded artifacts trigger
Gatekeeper/SmartScreen warnings.
:::
Ordering and compatibility, in one place
- Web image first. Both
image.yml(COPY --from) andrelease.yml(fetch-web-player.sh) pullaudiosilo-web- publish it before either runs, or re-run them after. On a plain tag push all workflows fire together; the web image resolved is whatever:latest(or the dispatchedweb_version) points at, which is why the runbook publishes the web image first. - Docker vs. native: same server code, same pinned player; the differences are
ffmpeg (bundled in the image, resolved/fetched at runtime for binaries) and how
the player is attached (
/app/web+AUDIOSILO_WEB_DIRvs.-tags embedplayer). - Version stamping is the same mechanism everywhere: ldflags →
api.Version→ reported byGET /serverand surfaced in the UIs. - Native app ↔ server compatibility is not pinned - it's negotiated at
runtime via
GET /api/v1/servercapability flags (cross-repo contract, seam 8).
For the human checklist (what to click, in what order, and how to smoke-test the result), see Releasing.