Release automation
BHTune uses a guarded, single-product release state machine. The release process is designed to make one release preparation change, one product tag, and one GitHub Release, while keeping artifact generation and supply-chain evidence in one workflow.
The first stable release is not cut. Side-effecting release automation runs only when the
repository variable RELEASE_AUTOMATION_ENABLED is exactly true; a missing or false value
fails closed. The release PAT is not needed for dry-run validation and must not be provisioned
until the approved RC has passed its canary.
Ownership
| Responsibility | Owner |
|---|---|
Product version, product tag, and root CHANGELOG.md | bhtune through release-plz |
| Release preparation PR | .github/workflows/release-plz.yml |
| Release PR integrity status | .github/workflows/release-integrity.yml |
| Protected-branch squash auto-merge request | .github/workflows/auto-merge.yml |
| GitHub Release and release assets | .github/workflows/release.yml |
| Prerelease artifact and platform acceptance | .github/workflows/release-canary.yml |
| Windows NSIS installer build and acceptance | .github/workflows/windows-installer.yml |
release-plz is configured as git-only. It does not publish BHTune crates to crates.io and does
not create GitHub Releases. The release.yml workflow is the only workflow that creates a GitHub
Release or uploads release artifacts.
Normal release flow
- A meaningful product change reaches
main. - With automation enabled,
release-plzcreates or refreshes one release PR targetingmain. The PR uses the deterministic titlechore(release): prepare vX.Y.Z, contains one root changelog entry covering the workspace crates, and includes the exact stable documentation snapshot when the version is stable. Release integrity, the normal required checks, CLA validation, and the applicable SonarQube analysis must pass. The release PR must be authored bymikeboiko, come from the exactbytehound-labs/bhtunerepository on arelease-plz-*branch, and targetmain.- The guarded auto-merge workflow requests GitHub's protected-branch squash auto-merge. It does not bypass branch protection or merge directly.
- After the release PR is merged,
release-plzcreates the single product tag. The exact prerelease or stable tag is then handled byrelease.yml. release.ymlcreates the GitHub Release and publishes the matching archives, packages, checksums, CycloneDX SBOM, GitHub artifact provenance, and Sigstore bundles.
The release-rate guard runs immediately before GitHub Release creation. It counts stable and prerelease releases and rejects the operation when there are already three or more releases in the preceding hour or twelve or more in the preceding 24 hours. API, authentication, pagination, timestamp, malformed-response, and network failures also reject the operation.
Safe dry runs
Manual dry runs are available while the kill switch is false. They do not receive the release PAT and must not create a pull request, push a tag, publish a crate, create a GitHub Release, or upload release assets.
Run the policy checks locally before dispatching a dry run:
python3 scripts/check_release_automation_test.py
python3 scripts/check_release_plz_config.py
The release-plz workflow's manual dry run validates the release-plz configuration without activating the side-effecting jobs. The release workflow's manual dispatch builds and packages the supported matrix without uploading anything to a GitHub Release.
On a stable release PR, also verify the exact documentation snapshot and retention metadata:
python3 scripts/sync_docs_version.py --repository . --check
Before the first stable release, the unversioned site intentionally has no snapshot, so this
check is expected to fail on main and on ordinary pre-release branches.
Windows installer workflow
The reusable Windows installer workflow is separate from release.yml. In dry-run mode it
builds BHTune from the selected source ref; in release mode it consumes the exact stable Windows
archive and its checksum, Sigstore, and provenance evidence. Both modes independently download
and verify the official gateway release pinned by
installer/windows/opcda-gateway-release.json, then embed that verified payload into the NSIS
installer as an optional component. The installer never downloads a gateway at runtime.
Run a dry build and lifecycle acceptance for the feature branch before release integration:
gh workflow run windows-installer.yml \
-f mode=dry-run \
-f source_ref=<branch-or-commit> \
-f attest=false
The workflow must verify the pinned gateway tag and source commit, release-workflow blob,
upstream checksum and Sigstore evidence, GitHub provenance, exact archive contents, executable
SHA-256 and 32-bit PE architecture, version, and required protocol handshake. The SYSTEM lifecycle
diagnostic runs a separately compiled test-only installer on the disposable hosted Windows
runner. That build uses a unique workspace-owned install and ProgramData root, service names, and
ownership registry key; it is never uploaded. The scheduled lifecycle task must prove the
S-1-5-18 SYSTEM identity and verify protected services, registry state, and ports 7600 and
8787 both before and after testing. The artifact contains only the production installer and
the lifecycle evidence log.
The complete lifecycle matrix covers the gateway-free silent default, explicit gateway installation, gateway add-on ending stopped, running/stopped managed upgrades, two-service rollback, ProgramData-preserving uninstall, unowned service and listener conflicts, a gateway-wide version/protocol handshake on a vanilla runner without OPCEnum, and an unchanged Windows Firewall fingerprint. The full SYSTEM-context matrix has a one-hour overall budget because it performs repeated real SCM install, upgrade, rollback, and uninstall cycles; each installer or uninstaller process remains independently capped at two minutes.
Updating the bundled gateway is a reviewed source change, not a release-time lookup:
- Select an already-published stable
bytehound-labs/opcda-bridgegateway release. - Update the tag, source commit, archive/executable hashes, target, release-workflow blob, and
required protocol versions in
installer/windows/opcda-gateway-release.json. - Update
installer/windows/third-party/opcda-bridge/NOTICE.txtto identify the same release and source commit. Keep the exact upstream MIT license unchanged unless upstream changes it. - Run the installer contract suite and the Windows installer dry-run workflow. Never weaken a checksum, workflow-identity, architecture, provenance, or protocol assertion to accept a mismatched release.
- Complete controlled read-only OPC acceptance before making the installer a stable release asset. Do not write tags or start a namespace index as part of release acceptance.
Prerelease acceptance
An RC requires explicit maintainer approval. Keep RELEASE_AUTOMATION_ENABLED=false throughout
RC creation and validation.
-
Create a temporary branch from the accepted
maincommit and update the workspace version to an exact SemVer prerelease such as0.1.0-rc.1, including internal dependency metadata and the lockfile. -
Do not merge the temporary version commit into
mainand do not create a documentation snapshot for the RC. -
After approval, create and push the exact prerelease tag.
release.ymlalone creates the prerelease GitHub Release and its assets. -
Dispatch the canary with the exact tag:
gh workflow run release-canary.yml -f tag=v0.1.0-rc.1 -
Require the canary to verify the asset inventory, checksums, SBOM, provenance, Sigstore evidence, archive execution,
--version, simulator behavior, loopback server health/UI responses, and Linux package installation. macOS and Windows run their matching archive and runtime checks; the Windows check remains CLI-only. For prerelease RPMs, the workflow maps the SemVer prerelease separator to RPM's ordering syntax (for example,0.1.0-rc.1becomes the RPM version0.1.0~rc.1); archives and executables retain the exact0.1.0-rc.1version. The RPM metadata override must preserve the literal tilde; replacing it with a filesystem path produces an invalid package version and fails the release before assets are uploaded. -
Retain the evidence before removing the temporary branch. Keep the public RC tag and release as the tested reference for the stable release.
Prerelease tags are not stable comparison points. The stable release-content guard ignores them,
so the final v0.1.0 can promote the exact tested RC content.
First stable release
The first stable release uses version 0.1.0. Because there is no earlier stable product tag,
release integrity requires the configured first-release baseline
2016c5c945d20a42140ec57b6e09f4a49ec98aef to be an ancestor of the release PR base. The stable
release must contain meaningful product content beyond generated release metadata.
After the RC canary is accepted and explicit activation approval is recorded:
- Provision a repository-scoped
RELEASE_PLZ_TOKENwith only the repository contents and pull request permissions needed by the guarded workflow. Do not use a registry token. - Set
RELEASE_AUTOMATION_ENABLED=trueonly after verifying the secret name and workflow wiring. - Dispatch
release-plz.ymlto create or refresh the one stable release PR. - Confirm the PR contains the
0.1.0release commit, root changelog entry, and exact0.1.0documentation snapshot. Confirm that no crates.io publication or GitHub Release has happened. - Require
Release integrityas a branch-protection status along with the normal repository checks, CLA, and applicable SonarQube zero-issue status. - Let the guarded auto-merge request squash merge the PR. The merged release commit creates
exactly
v0.1.0;release.ymlthen creates the one stable GitHub Release.
Stable documentation snapshots use exact X.Y.Z directories, exclude docs/internal/**, and
retain only the newest three stable versions. Before the first stable snapshot exists, the
Docusaurus site remains unversioned and does not show a version selector.
Recovery and non-negotiable rules
- If more than one release-plz PR appears, stop release automation and inspect the candidates.
Do not merge or tag until exactly one same-repository PR targeting
main, authored bymikeboiko, and using the expected release title remains. - If a product tag exists but its GitHub Release is missing or incomplete, keep the tag, inspect the failed workflow, and rerun the exact tag workflow after the cause is understood. Do not create a second tag or a replacement release by hand.
- If the release-rate guard or any GitHub API query cannot establish a safe answer, treat the release as blocked. Do not bypass the guard or substitute a best-effort count.
- If artifact or canary evidence fails, do not activate the stable release path. Fix the focused issue, retain the failed evidence, and use a newly approved prerelease when the release contents change.
- Never provision or print release credentials in local logs, workflow output, source files, or documentation. Never add a registry token to the git-only release flow.
- Never create a public RC, enable the kill switch, or cut
v0.1.0without explicit maintainer approval.
For the evidence review after a release or canary completes, use the release-verification guide. For installation, service-state, database, and operator recovery procedures, use the installation guide and operator runbook.