Skip to content

Publishing

Spectre's library modules (:core, :server, :recording, :recording-macos, :recording-linux, :recording-windows, :agent, :agent-runtime, :testing) publish to Sonatype Central via the com.vanniktech.maven.publish plugin. The sample modules (:sample-desktop, :sample-intellij-plugin) never apply the plugin — they're deliverables, not libraries. The :cli module is also intentionally excluded from Maven Central: it is a self-contained application distributed through GitHub release archives and the Homebrew and Scoop package channels.

Tag-driven release flow

The pipeline is owned by .github/workflows/release.yml and triggers on tags matching v*, but the first job rejects anything that is not a SemVer-shaped release tag: v<major>.<minor>.<patch> with optional SemVer pre-release/build metadata (for example v0.2.0 or v0.2.0-rc.1). The leading v is stripped to form the published version, so v0.2.0 publishes coordinates dev.sebastiano.spectre:spectre-core:0.2.0 and the matching version for every published library module.

Jobs on tag push (order simplified; see the workflow for the full graph):

  1. runtime-matrix (reusable workflow) — full {JBR 21, JBR 25, Temurin LTS} × {macOS, Linux, Windows} compatibility matrix. A red matrix blocks the release; this is the executable evidence for the supported JVM set in Stability policy. Per-PR CI stays on single-JDK Temurin 21.
  2. release-gate (Linux runner, depends on runtime-matrix) — validates the tag shape, runs ./gradlew check, installs the docs dependencies, and runs mkdocs build --strict. Helper builds and publish wait for this gate.
  3. mac-helper (macOS runner, depends on release-gate) — builds the arm64+x86_64 universal helper, packages it as SpectreCaptureHelper.app, Developer ID signs the bundle, runs notarytool submit --wait, staples the ticket, verifies codesign --deep --strict + stapler validate, and uploads the app as a GitHub Actions artefact (see NOTARIZATION.md).
  4. linux-helpers (Linux runner, depends on release-gate) — cross-builds the spectre-wayland-helper Rust binary for x86_64 and aarch64 (same toolchain dance documented in ci.yml: dpkg multi-arch + per-arch libdbus sysroot + cross-linker). Uploads the per-arch binaries as a GitHub Actions artefact.
  5. windows-helper (Windows runner, depends on release-gate) — builds the .NET Windows Graphics Capture helper for x64 and arm64, then uploads the per-arch helper directories as a GitHub Actions artefact.
  6. mac-cli-bundles (macOS runner, depends on the gate and mac-helper) — builds the x64 and arm64 Roast .app CLI bundles with the notarised screen-capture helper, signs every Mach-O component in their jlink runtimes with the Developer ID and JVM hardened-runtime entitlements (nested first, outer bundle last), submits each archive to notarytool with a 30-minute bound, staples the ticket, then verifies the exact extracted ZIP with codesign --verify --deep --strict, stapler, and spctl before upload. Intermediate post-sign checks also use --deep --strict so a nested jlink seal break cannot green-pass (#390). The Homebrew formula must keep preserve_rpath and a post_install that re-stages the notarized Spectre.app from the release zip after fix_dynamic_linkage (which can still strip duplicate jlink rpaths and ad-hoc re-sign nested dylibs, breaking the outer seal).
  7. publish (Linux runner, depends on the gate and all helper/bundle jobs) — downloads the helper and signed macOS CLI artefacts, runs :verifyMavenLocalPublication to assert the publication shape, builds the Linux x64/Linux arm64/Windows x64 Roast CLI bundles, and runs publishToMavenCentral against the Sonatype Central Portal. Finally it creates a draft GitHub release and uploads all five self-contained CLI bundles. Maven Central remains the canonical host for library modules only; the CLI is never uploaded there. Do not attach a partial library jar set to the GitHub release.

CLI package channels

The Spectre repository also hosts its release manifests: Formula/spectre.rb is the Homebrew tap formula and bucket/spectre.json is the Scoop bucket manifest. Publishing the draft GitHub release regenerates both from its public CLI archives and their SHA-256 values, then commits them to main.

Package-manifest generation and Homebrew install contracts are gated on Unix ./gradlew check in two tasks (issue #400 — clean Linux must not need undeclared Ruby):

  • verifyCliPackageManifests — generator + structural contracts (python3 + bash only): fixture archives, Scoop JSON, committed Formula/spectre.rb install-body alignment with the generator, wrapper bin entry text contracts.
  • verifyHomebrewFormulaInstallSemantics — behavioral install-semantics (Ruby): dual-layout Spectre.app discovery (Homebrew strip) plus wrapper-vs-symlink behaviour (Roast is argv[0]-sensitive and cannot be exposed via bin.install_symlink). Runs when Ruby is on PATH, and always under CI (actionable preflight if Ruby is missing). Host-dep regression: verifyCliPackageManifestHostDeps.

See Testing — Package-channel contracts.

On macOS, use the repository as an explicit tap because its name is spectre, not Homebrew's homebrew-* shorthand:

brew tap rock3r/spectre https://github.com/rock3r/spectre
brew install rock3r/spectre/spectre

On Windows:

scoop bucket add spectre https://github.com/rock3r/spectre
scoop install spectre

Both the Central Portal deployment and the GitHub release stay in manual- promotion mode (automaticRelease=false in build.gradle.kts, --draft on gh release create) until the first few tagged releases prove the pipeline. Do not promote either surface until the release workflow is green and you've sanity-checked the artefacts side-by-side.

Pre-tag release smoke (required)

Hosted CI does not fail-closed every surface Spectre ships (notably full agent attach / inject / CLI daemon e2e on Windows, live recording helpers, and release-shaped native packaging). Before tagging, complete a scoped release smoke on real desktops:

  1. Inventory the delta since the previous tag and the capability matrix.
  2. Always run the baseline hard cells (library, attach, inject, CLI, helpers).
  3. Add delta hard cells for new or changed paths that lack multi-OS CI evidence.
  4. Record a results table; hard failures block the tag.

Full process, environments (ssh Windows host, Linux VM), recipes, and the baseline matrix: Release smoke.

Do not treat green main alone as “ready to tag.”

Manual promotion checklist (after the tag workflow is green):

  • If this release changes the atomic capture schema (CaptureDocument.SCHEMA_VERSION / capture.json), bump the spectre-capture agent skill (skills/spectre-capture/SKILL.md + package.json) and the capture user-guide page in the same release.

  • Confirm the tag points at the intended, already-reviewed main SHA that completed release smoke (results table on file).

  • Inspect the Central Portal staging deployment for all nine modules, including POM metadata, sources jars, javadoc jars, and Gradle module metadata.
  • Confirm spectre-recording-<version>.jar contains no native/... entries.
  • Confirm spectre-recording-macos-<version>.jar contains native/macos/SpectreCaptureHelper.app.
  • Confirm spectre-recording-linux-<version>.jar contains native/linux/x86_64/spectre-wayland-helper and native/linux/aarch64/spectre-wayland-helper.
  • Confirm spectre-recording-windows-<version>.jar contains a complete multi-file Windows Graphics Capture helper for both x64 and arm64 under native/windows/<arch>/ — not only spectre-window-capture.exe. Each arch must include the renamed apphost, SpectreWindowCapture.dll, SpectreWindowCapture.deps.json, SpectreWindowCapture.runtimeconfig.json, WASDK bootstrap/WinRT/Win2D core DLLs, and every runtime/native asset named by that arch's deps.json (see WindowsGraphicsCaptureHelperPackagingContract in buildSrc).
  • Confirm spectre-agent-runtime-<version>.jar exists and its manifest declares Agent-Class: dev.sebastiano.spectre.agent.runtime.SpectreAgent.
  • Run the Central Portal deployment checker: scripts/central_portal_check.py validate --deployment-id <id> --version <version>.
  • Promote the Central staging deployment from the Central Portal UI.
  • Confirm the GitHub release notes link to Maven Central for artifacts instead of attaching a partial library jar set, and that it includes the Linux x64/Linux arm64, macOS x64/macOS arm64 (signed and stapled), and Windows x64 CLI bundles.
  • Undraft the GitHub release with gh release edit <tag> --draft=false.
  • Confirm the Publish CLI package channels workflow committed the refreshed Homebrew and Scoop manifests to main.

Required secrets

Set these in the repository's Settings → Secrets and variables → Actions:

Secret Used by Purpose
APPLE_DEVELOPER_ID_P12 mac-helper Base64-encoded Developer ID certificate.
APPLE_DEVELOPER_ID_P12_PASSWORD mac-helper P12 password.
APPLE_SIGNING_KEYCHAIN_PASSWORD mac-helper Ad-hoc keychain password.
APPLE_DEVELOPER_IDENTITY mac-helper Developer ID Application: … codesign identity.
APPLE_NOTARY_API_KEY mac-helper Base64-encoded App Store Connect API key.
APPLE_NOTARY_API_KEY_ID mac-helper API key ID.
APPLE_NOTARY_API_ISSUER mac-helper API issuer UUID.
MAVEN_CENTRAL_USERNAME publish Sonatype Central Portal token username.
MAVEN_CENTRAL_PASSWORD publish Central Portal token.
SIGNING_IN_MEMORY_KEY publish ASCII-armored PGP private key (no header line stripping).
SIGNING_IN_MEMORY_KEY_PASSWORD publish PGP key passphrase.

The Maven Central token comes from Central Portal → View Account → Generate User Token. The PGP key must be exported ASCII-armored (gpg --armor --export-secret-key <key-id>) and the public side must already be uploaded to keys.openpgp.org (or another keyserver pair Central trusts).

Local verification

publishToMavenLocal works without any credentials or signing keys — the signing convention only fires when ORG_GRADLE_PROJECT_signingInMemoryKey is set. The :verifyMavenLocalPublication task drives the full shape check:

# Publish all library modules + verify shape. Stub mac helper because Linux cannot build the
# real one; cross-arch Linux helpers come from the real Rust build. Windows helpers are only
# expected when provided explicitly or when running on Windows.
./gradlew verifyMavenLocalPublication \
    -PstubMacHelperForTesting \
    -PallLinuxArches

It asserts that each module ends up with:

  • <artifactId>-<version>.jar (main jar)
  • <artifactId>-<version>-sources.jar
  • <artifactId>-<version>-javadoc.jar (empty — see "Open follow-ups" below)
  • <artifactId>-<version>.pom with the Central-required POM elements (<name>, <description>, <url>, <licenses>, <scm>, <developers>)
  • <artifactId>-<version>.module (Gradle Module Metadata)

It additionally asserts:

  • every sources jar is free of generated native/... helper resources
  • :recording is API/common-only and contains no native/... resources
  • :recording-macos contains the full native/macos/SpectreCaptureHelper.app/ tree (executable, Info.plist, PkgInfo, AppIcon.icns)
  • :recording-linux contains native/linux/x86_64/spectre-wayland-helper and native/linux/aarch64/spectre-wayland-helper when built with release helper inputs
  • :recording-windows contains the full multi-file Windows Graphics Capture helper contract for x64 and arm64 under native/windows/<arch>/ when built with release helper inputs (fixed required basenames + deps.json asset closure — not exe-only)
  • :agent-runtime publishes the Java Agent runtime jar; it must carry the Java Agent manifest and must not bundle Compose, Skiko, Spectre core, or Kotlin stdlib classes

If you have a real notarised mac helper on disk (e.g. downloaded from a previous release-CI run), point at it instead of the stub:

./gradlew verifyMavenLocalPublication \
    -PprebuiltMacHelperPath=/path/to/SpectreCaptureHelper.app \
    -PprebuiltLinuxHelpersDir=/path/to/linux-helpers \
    -PprebuiltWindowsHelpersDir=/path/to/windows-helpers

The prebuiltLinuxHelpersDir directory must contain x86_64/spectre-wayland-helper and aarch64/spectre-wayland-helper. The prebuiltWindowsHelpersDir directory must contain the complete framework-dependent publish output for each arch (as produced by :recording:assembleWindowsScreenshotHelper / the release windows-helper job), shaped as:

windows-helpers/
  x64/
    spectre-window-capture.exe
    SpectreWindowCapture.dll
    SpectreWindowCapture.deps.json
    SpectreWindowCapture.runtimeconfig.json
    Microsoft.WindowsAppRuntime.Bootstrap.dll
    … remaining companions from the .NET publish …
  arm64/
    … same multi-file tree …

Providing only the two renamed executables is not sufficient: the runtime extractor copies the whole arch directory, and packaging verification enforces the multi-file contract (fixed required set + deps.json closure) for both arches.

Coordinates

Module Coordinates
:core dev.sebastiano.spectre:spectre-core:<version>
:server dev.sebastiano.spectre:spectre-server:<version>
:recording dev.sebastiano.spectre:spectre-recording:<version>
:recording-macos dev.sebastiano.spectre:spectre-recording-macos:<version>
:recording-linux dev.sebastiano.spectre:spectre-recording-linux:<version>
:recording-windows dev.sebastiano.spectre:spectre-recording-windows:<version>
:agent dev.sebastiano.spectre:spectre-agent:<version>
:agent-runtime dev.sebastiano.spectre:spectre-agent-runtime:<version>
:testing dev.sebastiano.spectre:spectre-testing:<version>

The shared metadata (group, license, SCM, developer) lives in gradle.properties at the repo root; per-module POM_ARTIFACT_ID / POM_NAME / POM_DESCRIPTION lives in each module's own gradle.properties. gradle.properties is loaded as Latin-1, so the descriptions stay ASCII-safe.

Open follow-ups

  • Dokka HTML javadoc. Currently JavadocJar.Empty() — satisfies Central's gate but doesn't give consumers offline API docs. Wire JavadocJar.Dokka(...) once Dokka 2 is verified against this codebase. Until then the canonical API docs live at https://spectre.sebastiano.dev.
  • Snapshot publishing. The plumbing supports it (the default version is 0.1.0-SNAPSHOT), but no CI workflow currently publishes snapshots — wire a main-push job if/when there's demand from consumers tracking unreleased changes.
  • Automatic Central promotion. automaticRelease=false keeps the staging repo in manual-promote mode. Flip to true once the first published version is in the wild and we trust the pipeline.