Skip to the content.

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

Steps

  1. 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.

  2. Bump the version in Cargo.toml (version = "X.Y.Z") and refresh Cargo.lock with cargo update --workspace (any cargo build also does it). This is the only place the version is written by hand: --version, --help, the log banner, and the Implementation-Version of wrapper.jar all derive from it.

  3. Close the changelog section. In CHANGELOG.md, rename ## [Unreleased] to ## [X.Y.Z] - YYYY-MM-DD and add a new, empty ## [Unreleased] above it. scripts/verify-version.ps1 fails if the section for the Cargo version is missing.

  4. Refresh the license material when dependencies changed:

    cargo deny check
    cargo about generate about.hbs -o THIRD_PARTY_NOTICES.md
    

    Review the diff of THIRD_PARTY_NOTICES.md; every crate that ships in wrapper.exe must appear under its license.

  5. Run the full local build:

    ./scripts/build-release.ps1
    

    It validates the version, runs cargo fmt --check, cargo clippy with -D warnings, cargo test, builds target\release\wrapper.exe and target\release\wrapper.jar, and verifies both artifacts, printing their SHA-256 hashes.

  6. Run the Java matrix. With JSS_REQUIRE_JAVA_TESTS=1 set, run cargo test --all-targets --all-features once with each of Temurin 8, 21, and 25 first on PATH, or wait for the CI matrix of the release commit.

  7. Commit and push with a message such as Release X.Y.Z (signed off, like every commit). Wait for CI to finish green.

  8. Tag and push the tag:

    git tag -a vX.Y.Z -m "Java Service Steward X.Y.Z"
    git push origin vX.Y.Z
    

    The tag must be exactly v followed by the Cargo version; the release workflow refuses anything else.

  9. Let the workflow publish. .github/workflows/release.yml builds the executable with --locked, builds the JAR, runs the version verification, stages java-service-steward-X.Y.Z-windows-x64/ with wrapper.exe, wrapper.jar, LICENSE-APACHE, LICENSE-MIT, NOTICE, THIRD_PARTY_NOTICES.md, README.md, and examples/wrapper.conf.example, zips it, writes SHA256SUMS, and creates the GitHub Release with generated release notes, attaching the zip and SHA256SUMS.

  10. Verify the published release. Download the zip and SHA256SUMS from the release page and compare:

    (Get-FileHash .\java-service-steward-X.Y.Z-windows-x64.zip -Algorithm SHA256).Hash.ToLower()
    Get-Content .\SHA256SUMS
    

    Unpack the zip, run wrapper.exe --version, and confirm that the first line is Java 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 the CHANGELOG.md section for the version.

  11. Check the crates.io publication. After the GitHub Release is created, the same workflow runs cargo publish --locked when the repository defines the CARGO_REGISTRY_TOKEN secret (a crates.io API token with the publish-update scope for java-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-steward then builds the released wrapper.exe; the crate contains the bridge sources and scripts/build-java-bridge.ps1, but cargo install does not produce wrapper.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 --locked
    

    A published version cannot be deleted, only yanked (cargo yank --version X.Y.Z). docs.rs builds the documentation against x86_64-pc-windows-msvc ([package.metadata.docs.rs] in Cargo.toml).

Rules

Supply-chain artifacts

Every release also publishes:

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.