# Releasing SoundCraft Every push to the `release` branch runs `.github/workflows/release.yml`. The workflow builds signed installers for macOS, Windows, Linux, FreeBSD and the web, then creates or updates a **draft** GitHub Release named `SoundCraft v`. Nobody sees a draft until a maintainer publishes it. This is SoundCraft's implementation of the shared [release playbook](release-playbook.md). User-facing names say **SoundCraft**. Files, binaries and ids stay lowercase (`soundcraft---.`, `ai.storyteller.soundcraft`). ## Cutting a release 1. **Bump the version** on `main`. The only place it lives is `[workspace.package] version` in the root `Cargo.toml`; the app reads it from `CARGO_PKG_VERSION` (*About SoundCraft*, `soundcraft --version`, `soundcraft-cli --version`). ```sh cargo xtask version # prints the current version, e.g. 0.1.0 cargo xtask version set 0.2.0 # or 0.2.0-rc.1; updates Cargo.toml and Cargo.lock ``` Commit the change (`Cargo.toml` + `Cargo.lock`) through the normal review flow. 2. **Merge `main` into `release`** (or fast-forward it) and push. The workflow starts by itself. 3. **Wait for the draft.** After about 30 to 60 minutes (notarization and the FreeBSD VM are the slow parts), the Releases page has a draft `SoundCraft v0.2.0`, tagged `v0.2.0` on the pushed commit, with every artifact and `SHA256SUMS.txt`. The notes are generated from the merged PRs. 4. **Check it.** Download an installer or two and look at the job summaries. Any `::warning::` there means a signing secret was missing and that artifact is unsigned. 5. **Publish** the draft in the GitHub UI. Publishing creates the `v0.2.0` tag. Versions with a pre-release suffix (`-rc.1`) are marked as pre-releases. Pushing to `release` again before you publish rebuilds the same draft and replaces its assets. After the draft is published, the workflow refuses to touch that version again, so bump it first. **Test runs:** *Actions → Release → Run workflow* runs the whole pipeline by hand. The optional `version` input (such as `0.2.0-rc.1`) overrides `Cargo.toml` for that run only. The jobs apply it with `cargo xtask version set` before building, so the binaries report it too. Only the signing jobs (macOS, Windows) and the draft-release job use the `release` environment, which only the `release` branch can use. On any other branch the run is a dry run: Linux, Flatpak, FreeBSD and web build and are checked, the signing jobs are refused, and no release is drafted. ## What gets built | Platform | Artifacts | Built on | |---|---|---| | macOS 11+ (universal: Apple silicon + Intel) | `soundcraft--macos-universal.dmg`, `soundcraft-cli--macos-universal.zip` | `macos-15` | | Windows 10+ x64 | `soundcraft--windows-x64.msi`, `soundcraft--windows-x64-portable.zip` | `windows-latest` | | Windows 10+ x86 (32-bit) | `soundcraft--windows-x86.msi`, `soundcraft--windows-x86-portable.zip` | `windows-latest` | | Windows 11 ARM64 | `soundcraft--windows-arm64.msi`, `soundcraft--windows-arm64-portable.zip` | `windows-latest` (cross-compiled; `windows-arm64.yml` installs and runs it on ARM64) | | Linux x86_64 | `soundcraft--linux-x86_64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04` | | Linux aarch64 | `soundcraft--linux-aarch64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04-arm` | | AppImage updates | `soundcraft--linux-{x86_64,aarch64}.AppImage.zsync` | with the AppImage | | Flatpak x86_64, aarch64 | `soundcraft--linux-{x86_64,aarch64}.flatpak` | `ubuntu-24.04`, `ubuntu-24.04-arm` (repackages the Linux tarball) | | FreeBSD 14 x86_64 | `soundcraft--freebsd-x86_64.tar.gz` (a `/usr/local` tree) | FreeBSD 14.3 VM on `ubuntu-latest` | | Web | `soundcraft-web-.zip` (static site; see [`packaging/web/README.md`](../packaging/web/README.md)) | `ubuntu-latest` | The from-source Flatpak manifest (`packaging/linux/flatpak/ai.storyteller.soundcraft.yml`) is kept ready for a Flathub submission; the release's `.flatpak` bundles come from `ai.storyteller.soundcraft.bundle.yml` (see Linux below). ### Audio libraries SoundCraft plays and records through [cpal](https://github.com/RustAudio/cpal): CoreAudio on macOS, WASAPI on Windows, ALSA on Linux and FreeBSD (PulseAudio and PipeWire through their ALSA plugins), and WebAudio in the browser. - **Linux** builds need the ALSA headers (`libasound2-dev` and `pkg-config` on Ubuntu). The binary links `libasound.so.2`, so the `.deb` depends on `libasound2 | libasound2t64` and the `.rpm` on `alsa-lib`; the AppImage and tarball expect it on the host (every desktop distro has it). - **FreeBSD** builds need `alsa-lib` (`pkg install alsa-lib`); at runtime `alsa-plugins-oss` routes ALSA to the OSS `sound(4)` devices. Without a device the player falls back to its silent clock, so the transport still runs. - **Flatpak**: alsa-lib and its PulseAudio plugin come with the freedesktop runtime; the manifests grant `--socket=pulseaudio` (PipeWire's pulse server answers on it). No raw ALSA devices and no JACK: SoundCraft doesn't use either. - **Web**: browsers start audio only after a user gesture, so the first click in the page may be needed before sound plays. The web build has no recording. ### macOS `packaging/macos/package.sh` builds `aarch64-apple-darwin` and `x86_64-apple-darwin` with `MACOSX_DEPLOYMENT_TARGET=11.0`, joins them with `lipo`, and assembles `SoundCraft.app`: - `Info.plist` is generated from `Info.plist.in`. The bundle id is `ai.storyteller.soundcraft`. It declares the `.scraft` session type (`ai.storyteller.soundcraft.session`, MIME `application/x-soundcraft-session`, role Editor, rank Owner) and lists WAV, AIFF/AIFC, FLAC, MP3 and MIDI as Viewer/Alternate, so SoundCraft appears under *Open With* without taking over the default player. `NSMicrophoneUsageDescription` explains the recording prompt. - **Signing** goes inside-out with the hardened runtime and a secure timestamp. The executable is signed first, then the bundle. There's no `--deep` on the final signature. `entitlements.plist` holds a single exception, `com.apple.security.device.audio-input`, without which the hardened runtime blocks recording. - **Notarization:** the app is zipped and sent with `xcrun notarytool submit --wait`, then the ticket is stapled to the app. The app goes on a DMG (`hdiutil`, with an `Applications` link). The DMG is signed, notarized and stapled too. Its Finder window (background, icon size and positions) comes from [`packaging/macos/dmg/`](../packaging/macos/dmg/README.md), and its volume is named `SoundCraft` without the version, which the window's background needs; the DMG file name keeps the version. - **CLI:** the universal `soundcraft-cli` is signed with the hardened runtime, zipped and notarized. Locally, without certificates, the script signs ad-hoc (`codesign -s -`) and skips notarization: ```sh packaging/macos/package.sh # universal; needs both rustup targets packaging/macos/package.sh --arch aarch64 # quicker, host-only open dist/release/soundcraft-*-macos-*.dmg ``` ### Windows `packaging/windows/package.ps1 -Arch x64|x86|arm64` builds with `-C target-feature=+crt-static` (in `CARGO_TARGET__RUSTFLAGS`, so host build scripts aren't affected), so neither the MSI nor the portable zip needs the Visual C++ redistributable. - `apps/soundcraft/build.rs` embeds the icon (`assets/app-icon/soundcraft.ico`) and VERSIONINFO with `winresource`, only when targeting Windows. Release packaging sets `SOUNDCRAFT_REQUIRE_WINRES=1` so a missing resource compiler fails the build. - The script checks both PE headers: the machine type matches `-Arch`, `soundcraft.exe` is a GUI app (no console window) and `soundcraft-cli.exe` a console app. - `soundcraft.wxs` (WiX v5) is a per-machine install into Program Files with an advertised Start Menu shortcut. SoundCraft becomes the default app for `.scraft` and is listed under *Open with* for `.wav`, `.aif`, `.aiff`, `.flac`, `.mp3`, `.mid` and `.midi`. It also registers App Paths (Win+R `soundcraft`). The MSI version is the numeric `X.Y.Z`; same-version upgrades let release candidates replace each other. The upgrade code `EE53D3E5-D0AE-40F9-808D-9417FDEBF61A` is fixed forever. - **Signing:** `packaging/windows/sign.ps1` signs both `.exe` files and then the `.msi` with `signtool` (SHA-256, RFC 3161 timestamp), using a `.pfx` (`WINDOWS_CERTIFICATE*`) if present, otherwise Azure Trusted Signing (all six `AZURE_*`), otherwise nothing (a warning). Locally on Windows: `dotnet tool install -g wix --version 5.0.2`, then `pwsh packaging/windows/package.ps1 -Arch x64`. ### Linux `packaging/linux/package.sh` stages one FHS tree and makes every format from it: both binaries, `ai.storyteller.soundcraft.desktop` (with `MimeType=` for sessions, WAV, AIFF, FLAC, MP3 and MIDI), hicolor icons from 16 px to 512 px plus a scalable SVG, AppStream metainfo, and a shared-mime-info file declaring `application/x-soundcraft-session` (`*.scraft`). - **AppImage** (maintained `AppImage/appimagetool`, static runtime: no libfuse2 needed). Each embeds the update information `gh-releases-zsync|storytold|soundcraft|latest|soundcraft-*-linux-.AppImage.zsync`, and the `.zsync` next to it (written when `zsyncmake` from the `zsync` package is installed, as in CI) lets AppImageUpdate fetch only the changed blocks of the newest non-pre-release. - **.deb** and **.rpm** by [nfpm](https://nfpm.goreleaser.com/) from one `nfpm.yaml`; the `postinst.sh` hook refreshes the desktop, MIME and icon caches. - **.tar.gz** for people who manage their own `/opt` or `~/.local`. - **Flatpak**: the `flatpak` job runs `packaging/linux/flatpak-bundle.sh` on the Linux job's tarball (no Rust build) with `flatpak/ai.storyteller.soundcraft.bundle.yml`, then installs the bundle and runs `soundcraft-cli --version` in the sandbox. Install with `flatpak install --user soundcraft--linux-.flatpak`. Both manifests use the freedesktop 25.08 runtime with Wayland/X11, `dri`, PulseAudio and `xdg-music`; other files go through the portals. For the VST3 and CLAP hosts they grant `~/.vst3` and `~/.clap` read-only and mount Flathub's `org.freedesktop.LinuxAudio.Plugins` extensions (pointed to by `VST3_PATH` and `CLAP_PATH`); system plugin folders aren't visible in the sandbox. `ai.storyteller.soundcraft.yml` builds from source for Flathub and needs vendored crates (`cargo-sources.json` from `flatpak-cargo-generator.py`); the commands are in its header. Binaries are built on Ubuntu 22.04 and need **glibc ≥ 2.35**. X11, Wayland, xkbcommon, Vulkan and EGL are loaded at runtime, so the packages declare them as dependencies or recommendations. Locally (on Linux): install nfpm, then `packaging/linux/package.sh` (or `--formats "deb tar"`). ### FreeBSD GitHub has no FreeBSD runners, so the `freebsd` job builds inside a FreeBSD 14.3 VM (`vmactions/freebsd-vm`, pinned by commit) and runs `packaging/freebsd/package.sh`, which stages a `/usr/local`-style tree (binaries, desktop entry, MIME type, metainfo, icons, docs) as a `.tar.gz`. Install with `tar -xzf soundcraft-*-freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local`. `.github/workflows/freebsd.yml` builds and tests the workspace there on every push to `main`. ### Web `packaging/web/package.sh` runs `trunk build --release` in `apps/soundcraft-web` (output in `dist/web`, `public_url = "./"`) and zips the site with sample `_headers` and `.htaccess` files and the hosting guide. `apps/soundcraft-web` runs the same `SoundApp` as the desktop app on the synthesised demo session, rendering with wgpu (WebGPU, or WebGL2 with `?webgl`) and playing through WebAudio. ## Secrets All secrets live in the repository's **`release` environment** (*Settings → Environments → release*), restricted to the `release` branch. Only the jobs that sign (macOS, Windows) and the draft-release job declare `environment: release`; the others get no secrets. Each secret is optional: if one is missing, that platform's artifacts are unsigned and the run shows a `::warning::`. Scripts never echo secret values; certificates are written with `umask 077` and deleted in `always()`/`finally` steps. | Secret | Used for | |---|---| | `APPLE_CERTIFICATE` | base64 `.p12` with "Developer ID Application: Learning Machines LLC (DJ6XS33FX8)" | | `APPLE_CERTIFICATE_PASSWORD` | password for that `.p12` | | `KEYCHAIN_PASSWORD` | password for the temporary CI keychain (random if unset) | | `APPLE_ID` | Apple ID used by `notarytool` | | `APPLE_PASSWORD` | app-specific password for that Apple ID | | `APPLE_TEAM_ID` | `DJ6XS33FX8` | | `WINDOWS_CERTIFICATE` / `WINDOWS_CERTIFICATE_PASSWORD` | base64 `.pfx` (unused: Windows uses Azure) | | `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` | service principal for Azure Trusted Signing | | `AZURE_SIGNING_ENDPOINT`, `AZURE_SIGNING_ACCOUNT`, `AZURE_CERT_PROFILE` | Trusted Signing endpoint, account and profile | `GITHUB_TOKEN` creates the release. Only the final job gets `contents: write`. Provisioning the environment, ruleset and secrets for a new repo: craftrules `release/signing-setup.md`. ## Icons `assets/app-icon/icon.svg` is the canonical icon: a **placeholder** singing nightingale, drawn as original SVG in the three-colour icon palette (ink, paper, SoundCraft teal `#14a9c4`); see `assets/app-icon/README.md`. `packaging/icons.sh` regenerates the 1024 px PNG, the macOS 512 px window icon, the `.icns`, the `.ico` (packed by `cargo xtask ico`) and the hicolor PNGs from it. It needs `resvg`, plus `iconutil` on macOS. The outputs are committed, so packaging never needs those tools. `apps/soundcraft/src/main.rs` sets the runtime window/Dock/taskbar icon from them. ## Checks `.github/workflows/packaging-lint.yml` runs in seconds on any change to `packaging/`, the workflows or the icons: actionlint, shellcheck, a PowerShell parse, xmllint (WiX, plists, AppStream, MIME), `desktop-file-validate`, `appstreamcli validate`, and a check that the two Flatpak manifests agree on runtime, finish-args and plugin extensions.