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):
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.release-gate(Linux runner, depends onruntime-matrix) — validates the tag shape, runs./gradlew check, installs the docs dependencies, and runsmkdocs build --strict. Helper builds and publish wait for this gate.mac-helper(macOS runner, depends onrelease-gate) — builds the arm64+x86_64 universal helper, packages it asSpectreCaptureHelper.app, Developer ID signs the bundle, runsnotarytool submit --wait, staples the ticket, verifiescodesign --deep --strict+stapler validate, and uploads the app as a GitHub Actions artefact (see NOTARIZATION.md).linux-helpers(Linux runner, depends onrelease-gate) — cross-builds thespectre-wayland-helperRust binary forx86_64andaarch64(same toolchain dance documented inci.yml: dpkg multi-arch + per-arch libdbus sysroot + cross-linker). Uploads the per-arch binaries as a GitHub Actions artefact.windows-helper(Windows runner, depends onrelease-gate) — builds the .NET Windows Graphics Capture helper forx64andarm64, then uploads the per-arch helper directories as a GitHub Actions artefact.mac-cli-bundles(macOS runner, depends on the gate andmac-helper) — builds the x64 and arm64 Roast.appCLI 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 tonotarytoolwith a 30-minute bound, staples the ticket, then verifies the exact extracted ZIP withcodesign --verify --deep --strict,stapler, andspctlbefore upload. Intermediate post-sign checks also use--deep --strictso a nested jlink seal break cannot green-pass (#390). The Homebrew formula must keeppreserve_rpathand apost_installthat re-stages the notarizedSpectre.appfrom the release zip afterfix_dynamic_linkage(which can still strip duplicate jlink rpaths and ad-hoc re-sign nested dylibs, breaking the outer seal).publish(Linux runner, depends on the gate and all helper/bundle jobs) — downloads the helper and signed macOS CLI artefacts, runs:verifyMavenLocalPublicationto assert the publication shape, builds the Linux x64/Linux arm64/Windows x64 Roast CLI bundles, and runspublishToMavenCentralagainst 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+bashonly): fixture archives, Scoop JSON, committedFormula/spectre.rbinstall-body alignment with the generator, wrapper bin entry text contracts.verifyHomebrewFormulaInstallSemantics— behavioral install-semantics (Ruby): dual-layoutSpectre.appdiscovery (Homebrew strip) plus wrapper-vs-symlink behaviour (Roast is argv[0]-sensitive and cannot be exposed viabin.install_symlink). Runs when Ruby is onPATH, 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:
On Windows:
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:
- Inventory the delta since the previous tag and the capability matrix.
- Always run the baseline hard cells (library, attach, inject, CLI, helpers).
- Add delta hard cells for new or changed paths that lack multi-OS CI evidence.
- 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 thespectre-captureagent 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
mainSHA 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>.jarcontains nonative/...entries. - Confirm
spectre-recording-macos-<version>.jarcontainsnative/macos/SpectreCaptureHelper.app. - Confirm
spectre-recording-linux-<version>.jarcontainsnative/linux/x86_64/spectre-wayland-helperandnative/linux/aarch64/spectre-wayland-helper. - Confirm
spectre-recording-windows-<version>.jarcontains a complete multi-file Windows Graphics Capture helper for bothx64andarm64undernative/windows/<arch>/— not onlyspectre-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 (seeWindowsGraphicsCaptureHelperPackagingContractinbuildSrc). - Confirm
spectre-agent-runtime-<version>.jarexists and its manifest declaresAgent-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>.pomwith 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 :recordingis API/common-only and contains nonative/...resources:recording-macoscontains the fullnative/macos/SpectreCaptureHelper.app/tree (executable, Info.plist, PkgInfo, AppIcon.icns):recording-linuxcontainsnative/linux/x86_64/spectre-wayland-helperandnative/linux/aarch64/spectre-wayland-helperwhen built with release helper inputs:recording-windowscontains the full multi-file Windows Graphics Capture helper contract forx64andarm64undernative/windows/<arch>/when built with release helper inputs (fixed required basenames + deps.json asset closure — not exe-only):agent-runtimepublishes 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. WireJavadocJar.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 amain-push job if/when there's demand from consumers tracking unreleased changes. - Automatic Central promotion.
automaticRelease=falsekeeps the staging repo in manual-promote mode. Flip totrueonce the first published version is in the wild and we trust the pipeline.