name: release-process description: Capsem release process, CI pipeline, Apple code signing, notarization, documentation site, and post-release verification. Use when preparing a release, debugging CI failures, working with Apple certificates, updating the documentation site, or cutting a new version. Covers the full release lifecycle from pre-release checklist through post-release verification.
Release Process
Pre-release checklist
just doctor # Check tools
scripts/preflight.sh # Validate Apple certs for CI
just test # ALL tests: unit + integration + cross-compile + bench
Cutting a release
Automated (preferred)
just cut-release
Runs test (all tests including integration, cross-compile, benchmarks), then bumps patch version, stamps changelog, commits, tags, pushes, waits for CI.
Manual
- Bump version in both
Cargo.toml(workspace) andcrates/capsem-app/tauri.conf.json - Move
[Unreleased]changelog items into[X.Y.Z] - YYYY-MM-DD - Create/update release page at
docs/src/content/docs/releases/<major>-<minor>.md scripts/preflight.shthenjust test- Commit, tag
vX.Y.Z, push both
Never reuse or move a tag. Always increment the version number.
CI pipeline (release.yaml)
Triggered by vX.Y.Z tag push. Parallelized pipeline (~18 min wall clock):
preflight (30s) ──> build-assets (arm64 + x86_64, 10 min) ──> build-app-macos (15 min) ──┐
└──> test (8 min) ─────────────────────────────────────────────────────────├──> create-release
└──────────────────> build-app-linux (arm64 + x86_64, 10 min) ────────────┘
| Job | Runner | Needs | Purpose |
|---|---|---|---|
preflight | macos-14 | -- | Fail-fast: Apple cert, Tauri key, notarization |
build-assets | ubuntu arm64 + x86_64 | preflight | Kernel + rootfs via Docker |
test | macos-14 | preflight | Unit tests + coverage, frontend, audit |
build-app-macos | macos-14 | preflight, build-assets | Tauri .app build, companion binaries, scripts/build-pkg.sh, notarize + staple .pkg |
build-app-linux | ubuntu arm64 + x86_64 | preflight, build-assets | Tauri build, deb (+ AppImage on x86_64) |
create-release | ubuntu-latest | test, build-app-macos, build-app-linux | Merge latest.json, sign manifest, GitHub release |
Test runs in parallel with builds. A test failure blocks create-release but doesn't delay compilation.
CI invariants (hard-won lessons)
- Per-arch VM assets use arch-prefixed names on GitHub. CI uploads with
gh release upload "$f#${arch}-${base}", renamingvmlinuztoarm64-vmlinuz, etc. The v2 manifest keeps bare filenames in per-archarchesmaps. - Use justfile recipes in CI.
build-assetsmust calljust build-kernelandjust build-rootfs, not reimplement the builder commands. Drift between the justfile and CI caused v0.14.2-v0.14.4 to ship without vmlinuz/initrd.img. - Build both kernel and rootfs. The builder defaults to
--template rootfsonly. The kernel template must be built explicitly. assets/currentmust be a real directory, not a symlink.generate_checksums()creates a symlink, but GitHub Actions strips symlinks from artifacts. After callinggenerate_checksums, replace the symlink withrm -rf assets/current && cp -r assets/arm64 assets/current.Cargo.lockis gitignored. CI resolves a fresh lockfile each build. This means dependency versions can drift between builds. Acceptable for now but a reproducibility risk.- Verify assets before Tauri build. The
Verify assets layoutstep lists assets/arm64/ and assets/current/ to catch missing files early. Tauri's build.rs resolves../../assets/current/vmlinuzrelative tocrates/capsem-app/. - Three files hold the binary version.
Cargo.toml(workspace),crates/capsem-app/tauri.conf.json,pyproject.toml.just _stamp-versionhandles all three automatically.just cut-releaseandjust installboth call it. - No AppImage on any platform. linuxdeploy cannot run on GitHub CI runners -- Ubuntu 24.04 lacks FUSE2, and neither
libfuse2norAPPIMAGE_EXTRACT_AND_RUN=1fixes it reliably. All Linux platforms ship.debonly. CI matrix passesbundles: debfor both arm64 and x86_64.just cross-compilematches this. This cost 14 consecutive failed releases (v0.12.1 through v0.14.14) to discover. - Tauri signing keys on all platforms.
TAURI_SIGNING_PRIVATE_KEYandTAURI_SIGNING_PRIVATE_KEY_PASSWORDmust be passed to everycargo tauri buildstep (macOS and Linux). Missing keys cause "public key found but no private key" failure. The macOS job had them from the start; the Linux job was missing them until v0.14.11. - Collect all updater artifacts. Linux artifact collection must include
.tar.gz,.tar.gz.sig,.AppImage.tar.gz,.AppImage.tar.gz.sig-- not just.deband.AppImage. Tauri's updater needs the.sigfiles. just cross-compileis not a perfect CI replica. It runs in a docker container on macOS, which has FUSE (via Colima's Linux VM). CI runners may not have FUSE, so AppImage bundling that works locally can fail in CI. The recipe catches compile errors and most packaging issues, but environment differences (FUSE, linuxdeploy availability) can still slip through. Always verify the first CI run of a new Linux packaging change.- Platform-gate all macOS-only APIs. Every use of
libc::clonefile,AppleVzHypervisor,core_foundation_sys, etc. must be wrapped in#[cfg(target_os = "macos")]-- struct, impl, AND tests. The Linux app build compiles the full workspace.cargo test --test platform_gatingcatches ungated symbols at unit test time. This burned v0.14.7 through v0.14.9. - Pin Xcode version on macOS runners. Always
sudo xcode-select -s /Applications/Xcode_16.2.app(or latest) before any Apple toolchain use. GitHub periodically updates runner images and the default Xcode can break (Abort trap in xcodebuild). The preflight may pass on one runner instance while build-app-macos gets a different one. v0.14.12 failed because Xcode 15.4's xcodebuild crashed withAbort trap: 6when Tauri tried to locate notarytool -- despite zero workflow changes from v0.14.11 which passed 9 hours earlier. latest.jsonis optional ingh release create. Tauri only generates updaterlatest.jsonfor bundle types that produce.tar.gz+.sigartifacts (AppImage, not deb). With deb-only builds, nolatest.jsonexists. The create-release step must handle this gracefully.- AppImage was dropped after 14 failed releases. linuxdeploy (a FUSE2 AppImage) cannot run on Ubuntu 24.04 CI runners (FUSE3 only). Tested:
libfuse2install,APPIMAGE_EXTRACT_AND_RUN=1env var, both together -- none worked reliably. If AppImage support is needed in the future, the approach would be to pre-extract linuxdeploy (--appimage-extract) and run the extracted binary directly, bypassing FUSE entirely.
Full-test gates
| Gate | What |
|---|---|
| Unit tests | cargo llvm-cov with coverage |
| Cross-compile | capsem-agent for aarch64 + x86_64 musl |
| Frontend | pnpm run check && pnpm run build |
| capsem-doctor | Boot VM, run full diagnostic suite |
| Integration | Boot VM, exercise all 6 telemetry pipelines |
| Benchmark | Boot VM, run capsem-bench |
Apple code signing
p12 encryption (critical gotcha)
macOS Keychain only accepts legacy PKCS12 (3DES/SHA1). OpenSSL 3.x creates PBES2/AES-256-CBC by default, which Keychain rejects with "wrong password."
Check: openssl pkcs12 -in cert.p12 -info -nokeys -nocerts -passin pass:PWD 2>&1 | head -5
PBES2= broken on macOSpbeWithSHA1And3-KeyTripleDES-CBC= works
Fix: scripts/fix_p12_legacy.sh then gh secret set APPLE_CERTIFICATE < private/apple-certificate/capsem-b64.txt
Notarization
Shipping artifact on macOS is a .pkg (productbuild), not a .dmg. Flow:
cargo tauri build --bundles app --skip-stapling-- builds.apponly (Tauri skips stapling the inner app; we staple the outer.pkg).scripts/build-pkg.sh-- productbuildsCapsem-$VERSION.pkgwith the.app+ companion binaries +manifest.json. Heavy VM assets are downloaded on first use by the postinstall.xcrun notarytool submit ... --wait --timeout 30m-- synchronous.xcrun stapler staple+xcrun stapler validate.
Verify credentials locally (before touching a tag):
xcrun notarytool history --key private/apple-certificate/capsem.p8 --key-id KEY_ID --issuer ISSUER_ID
403 "A required agreement is missing or has expired" -- Apple periodically refreshes the Developer Program License Agreement, Paid Apps Agreement, etc. Only the Account Holder (not Admin/Developer) can accept. Check banners at both:
- https://developer.apple.com/account (Program License Agreement)
- https://appstoreconnect.apple.com → Agreements, Tax, and Banking (Free/Paid Apps)
Propagation can lag 1-5 min after accepting. notarytool history must return a list (possibly empty) before you tag -- the CI preflight step runs the same check and fails fast on 403.
CI secrets
| Secret | Purpose |
|---|---|
APPLE_CERTIFICATE | Base64 .p12 (legacy 3DES) |
APPLE_CERTIFICATE_PASSWORD | Password for p12 |
APPLE_SIGNING_IDENTITY | Developer ID Application: Elie Bursztein (L8EGK4X86T) |
APPLE_API_ISSUER | App Store Connect issuer UUID |
APPLE_API_KEY | App Store Connect key ID |
APPLE_API_KEY_PATH | Contents of .p8 private key |
TAURI_SIGNING_PRIVATE_KEY | Tauri updater minisign key |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD | Password for Tauri key |
CODECOV_TOKEN | Codecov upload token |
Local backups: private/apple-certificate/ and private/tauri/ (gitignored).
Post-release verification
gh release view vX.Y.Z
gh release download vX.Y.Z --pattern manifest.json -D /tmp/verify
gh release download vX.Y.Z --pattern '*.pkg' -D /tmp/verify
pkgutil --check-signature /tmp/verify/Capsem-*.pkg
spctl -a -vv -t install /tmp/verify/Capsem-*.pkg # Gatekeeper accepts notarized+stapled
xcrun stapler validate /tmp/verify/Capsem-*.pkg # Staple ticket present
Documentation site
The product website uses Astro Starlight. Docs live in docs/src/content/docs/.
Writing style
Tight and to the point. One topic per page. Tables over prose for configs and test cases. No filler.
Structure
docs/src/content/docs/<category>/<topic>.md- Categories:
security/,testing/,releases/,architecture/ - Frontmatter:
titleanddescriptionrequired.sidebar: { order: N }for ordering.
Release pages
- Path:
docs/src/content/docs/releases/<major>-<minor>.md(hyphens, not dots) - Each page consolidates all patch releases for that minor
- Higher
sidebar.order= newer = listed first
Dev workflow
cd site && pnpm run dev # localhost:4321
cd site && pnpm run build # Production build
Keep docs in sync
When features change (settings, CLI flags, MCP tools, security invariants, benchmarks), update the corresponding doc page. When cutting a new minor, create a new release page.
Update benchmarks before release
Run the host-side benchmarks to generate versioned data files and update the results page:
# Generate benchmarks/fork/data_{version}.json and benchmarks/lifecycle/data_{version}.json
uv run pytest tests/capsem-serial/test_lifecycle_benchmark.py -xvs
# Update docs/src/content/docs/benchmarks/results.md with new numbers
# (manual -- copy from the benchmark summary tables)
Benchmark data files in benchmarks/ are committed to git for historical tracking. The test_fork_benchmark gates ensure fork stays under 500ms and images under 12MB -- these must pass before release.
Changelog
Keep a Changelog format in CHANGELOG.md. Every user-visible change gets an entry under ## [Unreleased] using: Added, Changed, Deprecated, Removed, Fixed, Security.
Versioning
Binary and asset versions are orthogonal:
- Binary:
1.0.{unix_timestamp}-- auto-stamped byjust _stamp-versionon everyjust installandjust cut-release - Assets:
YYYY.MMDD.patch-- auto-derived bygen_manifest.pyfrom the build date
Three files hold the binary version (kept in sync by _stamp-version): Cargo.toml (workspace), crates/capsem-app/tauri.conf.json, pyproject.toml.
The v2 manifest links them via min_binary (oldest binary for these assets) and min_assets (oldest assets for this binary). See /asset-pipeline for manifest format.
Commits
- Include
CHANGELOG.mdupdate in the same commit - Stage files explicitly (no
git add -A) - Conventional messages:
feat:,fix:,chore:,docs: - Author: Elie Bursztein github@elie.net
- No
Co-Authored-Bytrailers