Release procedure
Audience: maintainers. The version numbering rules (PATCH, MINOR, MAJOR, pre-release suffixes) are in versioning.md; this page is the step-by-step procedure that turns a commit into a published release.
Before you start
- The working tree is clean and on the branch that will be released.
- CI is green for the commit you intend to release.
cargo install cargo-about cargo-denyhas been run at least once if dependencies changed since the previous release.- You have a Temurin 8, 21, and 25 JDK available locally, or you rely on CI for the Java matrix.
Steps
-
Classify the changes since the previous release as PATCH, MINOR, or MAJOR. During the 0.x series an incompatible change is MINOR and must be marked Breaking in the changelog together with a migration note.
-
Bump the version in
Cargo.toml(version = "X.Y.Z") and refreshCargo.lockwithcargo update --workspace(anycargo buildalso does it). This is the only place the version is written by hand:--version,--help, the log banner, and theImplementation-Versionofwrapper.jarall derive from it. -
Close the changelog section. In
CHANGELOG.md, rename## [Unreleased]to## [X.Y.Z] - YYYY-MM-DDand add a new, empty## [Unreleased]above it.scripts/verify-version.ps1fails if the section for the Cargo version is missing. -
Refresh the license material when dependencies changed:
cargo deny check cargo about generate about.hbs -o THIRD_PARTY_NOTICES.mdReview the diff of
THIRD_PARTY_NOTICES.md; every crate that ships inwrapper.exemust appear under its license. -
Run the full local build:
./scripts/build-release.ps1It validates the version, runs
cargo fmt --check,cargo clippywith-D warnings,cargo test, buildstarget\release\wrapper.exeandtarget\release\wrapper.jar, and verifies both artifacts, printing their SHA-256 hashes. -
Run the Java matrix. With
JSS_REQUIRE_JAVA_TESTS=1set, runcargo test --all-targets --all-featuresonce with each of Temurin 8, 21, and 25 first onPATH, or wait for the CI matrix of the release commit. -
Commit and push with a message such as
Release X.Y.Z(signed off, like every commit). Wait for CI to finish green. -
Tag and push the tag:
git tag -a vX.Y.Z -m "Java Service Steward X.Y.Z" git push origin vX.Y.ZThe tag must be exactly
vfollowed by the Cargo version; the release workflow refuses anything else. -
Let the workflow publish.
.github/workflows/release.ymlbuilds the executable with--locked, builds the JAR, runs the version verification, stagesjava-service-steward-X.Y.Z-windows-x64/withwrapper.exe,wrapper.jar,LICENSE-APACHE,LICENSE-MIT,NOTICE,THIRD_PARTY_NOTICES.md,README.md, andexamples/wrapper.conf.example, zips it, writesSHA256SUMS, and creates the GitHub Release with generated release notes, attaching the zip andSHA256SUMS. -
Verify the published release. Download the zip and
SHA256SUMSfrom the release page and compare:(Get-FileHash .\java-service-steward-X.Y.Z-windows-x64.zip -Algorithm SHA256).Hash.ToLower() Get-Content .\SHA256SUMSUnpack the zip, run
wrapper.exe --version, and confirm that the first line isJava Service Steward 64-bit X.Y.Z. Then edit the release notes: keep the generated list of pull requests if it is useful, and add a link to theCHANGELOG.mdsection for the version. -
Check the crates.io publication. After the GitHub Release is created, the same workflow runs
cargo publish --lockedwhen the repository defines theCARGO_REGISTRY_TOKENsecret (a crates.io API token with thepublish-updatescope forjava-service-steward; forks without the secret skip the step). Confirm that https://crates.io/crates/java-service-steward lists the new version.cargo install java-service-stewardthen builds the releasedwrapper.exe; the crate contains the bridge sources andscripts/build-java-bridge.ps1, butcargo installdoes not producewrapper.jar, so users take it from the GitHub release.If the step failed or the secret is missing, publish by hand with a token saved through
cargo login:git checkout vX.Y.Z cargo publish --dry-run --locked cargo publish --lockedA published version cannot be deleted, only yanked (
cargo yank --version X.Y.Z). docs.rs builds the documentation againstx86_64-pc-windows-msvc([package.metadata.docs.rs]inCargo.toml).
Rules
- Never reuse a version number. If anything in the artifacts must change after the tag was pushed, even by one byte, release the next PATCH version. Do not replace files on an existing release.
- Delete a tag and its release only when the workflow failed before publishing anything; fix the cause, then push the same tag again.
- The JAR packager sorts its entries and uses a fixed timestamp, so two builds
from the same sources and toolchain must produce the same
wrapper.jarhash. Treat a difference as a release process defect and investigate before publishing. - Every release zip contains
LICENSE-APACHE,LICENSE-MIT,NOTICE, andTHIRD_PARTY_NOTICES.md; the workflow fails if any of them is missing. - Pre-releases (
X.Y.Z-rc.1) follow the same procedure. Mark the GitHub Release as a pre-release by hand after the workflow has created it.
Supply-chain artifacts
Every release also publishes:
java-service-steward-<version>-windows-x64.cdx.json: a CycloneDX 1.5 software bill of materials of the Rust dependencies compiled intowrapper.exe, generated withcargo cyclonedxfor thex86_64-pc-windows-msvctarget.wrapper.jarhas no third-party dependencies.SHA256SUMS: hashes of the zip and of the SBOM.- A SLSA build-provenance attestation (Sigstore, recorded through
actions/attest) covering the zip, the SBOM andSHA256SUMS, plus an SBOM attestation that links the SBOM to the zip. Attestations are stored by GitHub, not as release assets.
To verify a download:
# Hashes
Get-FileHash java-service-steward-<version>-windows-x64.zip -Algorithm SHA256
Get-Content SHA256SUMS
# Provenance: proves the file was built by this repository's release workflow
gh attestation verify java-service-steward-<version>-windows-x64.zip --owner jayyanez
# SBOM attestation
gh attestation verify java-service-steward-<version>-windows-x64.zip --owner jayyanez --predicate-type https://cyclonedx.org/bom
wrapper.exe carries a Windows version resource (product name, file
description, file and product version) so that Explorer, SmartScreen and
code-signing services can identify it.
Windows code signing
Tagged releases from jayyanez/java-service-steward sign wrapper.exe with
Microsoft Azure Artifact Signing under the verified publisher Jay Yanez.
Existing 0.3.2 and older release assets remain unsigned and unchanged. The
first newly published signed package must use a new version, at least 0.3.3.
This changes release packaging/signatures, not the application’s runtime API.
The release workflow uses a dedicated Entra service principal and GitHub OIDC,
with no client secret or downloaded PFX. Its only Azure role is Artifact
Signing Certificate Profile Signer on the windows-apps profile. The
release-signing GitHub environment accepts v* tags and main (for manual
validation). Its environment secrets contain the client/tenant/subscription
identifiers. The maintainer’s private
windows-code-signing toolkit
documents installation on another PC, existing cloud resources, and local
signing; public CI calls official Microsoft actions directly.
Signing is ON for tagged releases unless the repository Actions variable
WINDOWS_CODE_SIGNING_ENABLED is 0:
gh variable set WINDOWS_CODE_SIGNING_ENABLED --repo jayyanez/java-service-steward --body 0 # OFF
gh variable set WINDOWS_CODE_SIGNING_ENABLED --repo jayyanez/java-service-steward --body 1 # ON
When ON, authentication/signing/verification failure aborts before publication. The workflow verifies the signature, timestamp, publisher and profile EKU before packaging and after ZIP extraction. Hashes/attestations cover the final package. When deliberately OFF, release notes identify that the build was unsigned. Ordinary push/PR CI does not sign. Fork release workflows skip this maintainer’s signing setup rather than accessing its identity.
To validate the hosted signing integration without publishing a release:
# Default validation is unsigned: zero new signing requests.
gh workflow run release.yml --repo jayyanez/java-service-steward --ref main -f sign=false
# Intentional end-to-end test: one EXE signing request, unless repository signing is OFF.
gh workflow run release.yml --repo jayyanez/java-service-steward --ref main -f sign=true
The manual job uploads a short-lived signing-validation-<run-id> artifact,
including the EXE/JAR test ZIP and signature receipt. It never creates a GitHub
Release or publishes to crates.io. An experimental 0.3.2 validation package is
not a replacement for the public 0.3.2 release.
To verify a downloaded release after extracting it:
Get-AuthenticodeSignature -LiteralPath .\wrapper.exe |
Format-List Status,SignerCertificate,TimeStamperCertificate
# From a source checkout, also verify publisher and durable profile identity:
.\scripts\verify-windows-signature.ps1 -FilePath 'C:\downloads\wrapper.exe'
Expected: Valid, publisher Jay Yanez, timestamp present. The short-lived
certificate rotates; do not pin a leaf thumbprint or re-sign releases every
72 hours. The timestamp preserves ordinary verification after leaf expiry,
subject to revocation/trust. Authenticode does not guarantee that every
SmartScreen reputation prompt disappears. wrapper.jar is not JAR-signed;
ZIP hashes and Sigstore provenance remain separate verification mechanisms.