Releasing opnDossier#
This document describes the release process for opnDossier.
Table of Contents#
- Version Numbering
- Prerequisites
- Pre-release Checklist
- Creating a Release
- Post-release Verification
- Release Candidates
- Hotfix Process
- Troubleshooting
Version Numbering#
opnDossier follows Semantic Versioning 2.0.0:
| Version Component | When to Increment | Example |
|---|---|---|
| MAJOR (X.0.0) | Breaking changes to CLI interface, config format, or API | Removing a flag, changing output format |
| MINOR (0.X.0) | New features, backward-compatible additions | New audit plugin, new output format |
| PATCH (0.0.X) | Bug fixes, documentation, performance improvements | Fix parsing bug, typo fixes |
Pre-release Tags#
- Release Candidates:
v1.2.0-rc1,v1.2.0-rc2- Feature-complete, needs testing - Beta:
v1.2.0-beta.1- Feature incomplete, early testing - Alpha:
v1.2.0-alpha.1- Experimental, unstable
Prerequisites#
Required Tools#
Install these tools before creating a release:
# Install via mise (recommended - see .mise.toml)
mise install
# Or install manually:
# goreleaser - https://goreleaser.com/install/
brew install goreleaser/tap/goreleaser
# git-cliff - https://git-cliff.org/docs/installation
brew install git-cliff
# cosign v3 - https://docs.sigstore.dev/cosign/installation/
# Note: v3+ uses keyless signing by default with .sigstore.json bundles
brew install cosign
# cyclonedx-gomod (for SBOM) - https://github.com/CycloneDX/cyclonedx-gomod
go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest
# go-licenses (for third-party notices) - https://github.com/google/go-licenses
go install github.com/google/go-licenses@latest
# quill (for macOS code signing, optional) - https://github.com/anchore/quill
# Only needed if you want to sign and notarize macOS binaries
curl -sSfL https://raw.githubusercontent.com/anchore/quill/main/install.sh | sh -s -- -b /usr/local/bin
Environment Variables (Optional)#
For signed releases, configure these environment variables:
# macOS Code Signing with Quill (optional)
# See: https://github.com/anchore/quill
export QUILL_SIGN_P12="path/to/certificate.p12" # or base64-encoded P12
export QUILL_SIGN_PASSWORD="certificate-password"
export QUILL_NOTARY_KEY="path/to/AuthKey_XXXXX.p8" # Apple API key
export QUILL_NOTARY_KEY_ID="XXXXXXXXXX" # Key ID from App Store Connect
export QUILL_NOTARY_ISSUER="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Issuer UUID
# Linux Package Signing (optional)
export RPM_SIGNING_KEY_FILE="path/to/rpm-key"
export DEB_SIGNING_KEY_FILE="path/to/deb-key"
export APK_SIGNING_KEY_FILE="path/to/apk-key"
Note
Cosign v3 uses keyless signing via Sigstore OIDC, so no signing keys are needed for artifact signatures when running in GitHub Actions.
GitHub Permissions#
The release workflow requires:
contents: write- Create releases and upload assetsid-token: write- SLSA provenance and Cosign keyless signing
GitHub Secrets#
For GPG signing of release artifacts, add these repository secrets:
| Secret | Description |
|---|---|
GPG_PRIVATE_KEY | Base64-encoded GPG private key (gpg --armor --export-secret-keys EMAIL | base64) |
GPG_PASSPHRASE | Passphrase for the GPG key |
Note
GPG signing is optional. If these secrets are not set, releases will still be created with Cosign signatures for checksums.
Pre-release Checklist#
Before creating a release, verify:
- All CI checks pass on
mainbranch - All issues/PRs for the milestone are closed
-
CHANGELOG.mdis up to date (or will be auto-generated) - Version references in code are correct (if any hardcoded)
-
action.ymldefaultversion:input is bumped to the new tag - README GitHub Action examples (
uses:lines) are swept to the new tag - README "Pinning" section SHA-pin example is updated to the new release commit SHA
- Public-API snapshot fixtures (
pkg/*/testdata/api-snapshots/*.golden) reviewed for unintended diffs since the last tag. These are the authoritative baseline for the semver contract — any unexplained change must be explained or reverted before tagging. See docs/development/public-api.md § API Shape Enforcement. - Documentation reflects new features/changes
- Breaking changes are documented
Verify CI Status#
# Check CI status for main branch
gh run list --branch main --limit 5
# View specific workflow run
gh run view <run-id>
Close Milestone#
# List open milestones
gh milestone list --state open
# Close milestone (goreleaser will also auto-close on release)
gh milestone edit <milestone-number> --state closed
Creating a Release#
Step 1: Validate Configuration#
# Check goreleaser configuration
goreleaser check
# Preview what would be built (no publish)
goreleaser release --snapshot --clean
# Check generated artifacts
ls -la dist/
Note
The release workflow automatically generates THIRD_PARTY_NOTICES via the just notices command as a GoReleaser before-hook. This file provides human-readable license attribution for all dependencies and is included in all distribution archives and packages. The formatting is controlled by the template at packaging/notices.tpl.
RELEASE_NOTES.md convention#
RELEASE_NOTES.md in the repo root holds only the most recent release's notes. Overwrite it on each release. On the normal tag-push path the published GitHub Release body is generated by GoReleaser (its header/footer templates plus the changelog), not from this file; RELEASE_NOTES.md is the human-readable latest-notes artifact and the --notes-file source only for the manual/emergency gh release create path (see Manual Release below). Historical per-release entries live in CHANGELOG.md, which follows the Keep a Changelog 1.1.0 workflow described in Step 2 below; never accumulate past releases inside RELEASE_NOTES.md.
Step 2: Generate Changelog Preview#
CHANGELOG.md follows Keep a Changelog 1.1.0. During development, commits flow into ## [Unreleased] at the top of CHANGELOG.md. On release, git-cliff promotes the Unreleased section to ## [vX.Y.Z] - YYYY-MM-DD and seeds a new empty Unreleased section for the next cycle.
Commit types are mapped to Keep-a-Changelog buckets by cliff.toml:
| Commit prefix | Changelog section |
|---|---|
feat: | Added |
fix: (except fix(security)) | Fixed |
fix(security):, security: | Security |
deprecate:, *(deprecate): | Deprecated |
remove:, *(remove): | Removed |
perf:, refactor:, docs:, chore:, ci:, test:, style:, revert: | Changed |
Commits whose body carries BREAKING CHANGE: land in Changed and are prefixed with [**breaking**] in the bullet — hand-curate migration notes beneath the release header when this happens.
# Preview the Unreleased section as it would appear for the next tag
git-cliff --unreleased
# Preview the full changelog (all tagged versions + Unreleased)
git-cliff --output /dev/stdout
# Generate the final changelog for a specific version and write to disk
just changelog-version vX.Y.Z
Step 3: Create and Push Tag (this publishes the release)#
Pushing a v* tag is the only action needed to publish a release. .github/workflows/release.yml triggers on the tag push and runs GoReleaser, which builds every platform artifact, signs them, generates the SBOM, pushes the GHCR image, updates the Homebrew tap, and creates and publishes the GitHub Release itself.
# Ensure you're on main with latest changes
git checkout main
git pull origin main
# Create annotated tag on the main commit (never a feature-branch head — see GOTCHAS #12.1)
git tag -a v1.2.0 -m "Release v1.2.0"
# Push tag — this triggers GoReleaser, which builds AND publishes the release
git push origin v1.2.0
Important
Do not run gh release create (or draft a release in the GitHub UI) after pushing the tag. GoReleaser owns release creation on this repo; a manual create races the in-progress workflow and will conflict. The release body comes from the GoReleaser header/footer templates plus the generated changelog. RELEASE_NOTES.md is only consumed by the manual/emergency path below.
Step 4: Monitor the Release Workflow#
# Watch the release workflow
gh run watch
# Or list recent workflow runs
gh run list --workflow=release.yml --limit 5
Post-release Verification#
After the release workflow completes:
Verify Artifacts#
# List release assets
gh release view v1.2.0
# Download and verify checksums
gh release download v1.2.0 --pattern "*checksums*"
sha256sum -c opnDossier_checksums.txt
Verify SLSA Provenance#
# Install slsa-verifier
brew install slsa-framework/tap/slsa-verifier
# Download provenance
gh release download v1.2.0 --pattern "*.intoto.jsonl"
# Verify provenance
slsa-verifier verify-artifact \
--provenance-path opnDossier-v1.2.0.intoto.jsonl \
--source-uri github.com/EvilBit-Labs/opnDossier \
--source-tag v1.2.0 \
opnDossier_checksums.txt
Verify Cosign Signatures (v3)#
# Download checksum file and its signature bundle
gh release download v1.2.0 --pattern "*checksums*"
gh release download v1.2.0 --pattern "*.sigstore.json"
# Verify signature with Cosign v3 (keyless, using Sigstore bundle format)
cosign verify-blob \
--certificate-identity "https://github.com/EvilBit-Labs/opnDossier/.github/workflows/release.yml@refs/tags/v1.2.0" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle opnDossier_checksums.txt.sigstore.json \
opnDossier_checksums.txt
Note
Cosign v3 uses .sigstore.json bundle format instead of separate .sig and .pem files.
Verify GPG Signatures#
All release archives and packages are signed with the EvilBit Labs software signing key.
# Import the public key (one-time setup)
curl -sSL https://raw.githubusercontent.com/EvilBit-Labs/opnDossier/main/keys/software-signing.asc | gpg --import
# Or from a local clone
gpg --import keys/software-signing.asc
# Download an artifact and its signature
gh release download v1.2.0 --pattern "opnDossier_Linux_x86_64.tar.gz*"
# Verify the signature
gpg --verify opnDossier_Linux_x86_64.tar.gz.sig opnDossier_Linux_x86_64.tar.gz
Key details:
- Email:
software@evilbitlabs.io - Fingerprint:
138B FA78 8F37 7661 EA48 2C1D EFC6 F4CA BED2 2E8E - Key type: RSA 4096
- Expires: 2030-02-03
Test Installation#
# Test binary download and execution
gh release download v1.2.0 --pattern "*Darwin_arm64*"
tar -xzf opnDossier_Darwin_arm64.tar.gz
./opndossier --version
# Test package installation (Linux)
# Debian/Ubuntu
sudo dpkg -i opndossier_1.2.0_amd64.deb
opndossier --version
# RHEL/Fedora
sudo rpm -i opndossier-1.2.0-1.x86_64.rpm
opndossier --version
Release Candidates#
Use release candidates for significant releases that need broader testing.
Creating an RC#
# Tag release candidate — the push triggers GoReleaser, which publishes the
# pre-release automatically. GoReleaser's `prerelease: auto` detects the -rc
# suffix and marks the GitHub Release as a pre-release. Do NOT run
# `gh release create` afterward.
git tag -a v1.2.0-rc1 -m "Release candidate 1 for v1.2.0"
git push origin v1.2.0-rc1
Promoting RC to Release#
If the RC is stable:
# Tag the same commit as the final release; the push triggers GoReleaser, which
# builds and publishes the final (non-prerelease) GitHub Release automatically.
git tag -a v1.2.0 -m "Release v1.2.0"
git push origin v1.2.0
Hotfix Process#
For urgent fixes to a released version — including security patches tracked via SECURITY.md. When a confirmed vulnerability requires a release outside the normal cadence, use the procedure below.
When to hotfix vs wait for the next release:
- Hotfix: confirmed security vulnerability (any severity), data-loss bug, or critical regression blocking a large share of users.
- Wait: cosmetic or low-impact fixes that can ride the next scheduled release.
Hotfixes are patch-version bumps only (vX.Y.Z → vX.Y.Z+1). Feature additions must wait for a minor release. Changelog entries for hotfixes go under the ### Security bucket (if applicable) or ### Fixed; breaking changes are never allowed in a hotfix.
Step 1: Create Hotfix Branch#
# Branch from the release tag
git checkout -b hotfix/v1.2.1 v1.2.0
# Make the fix
# ... edit files ...
# Commit with conventional commit format
git commit -m "fix(parser): handle edge case in XML parsing"
Step 2: Create PR and Merge#
# Push hotfix branch
git push origin hotfix/v1.2.1
# Create PR targeting main
gh pr create --title "fix: critical parsing bug" --base main
# After review and merge, tag the release
git checkout main
git pull
git tag -a v1.2.1 -m "Hotfix release v1.2.1"
git push origin v1.2.1
Troubleshooting#
Common Issues#
goreleaser check fails#
# Validate YAML syntax
yamllint .goreleaser.yaml
# Check for deprecated options
goreleaser check --deprecated
Cosign signing fails#
# Verify cosign is configured for keyless signing
cosign version
# Test keyless signing locally
echo "test" | cosign sign-blob --yes - --bundle test.bundle
SLSA provenance fails#
Check the workflow logs:
gh run view <run-id> --log-failed
Manual Release (Emergency)#
If the automated workflow fails, you can release manually:
# Build locally
goreleaser release --clean
# Or skip certain steps
goreleaser release --clean --skip=sign
macOS Signing (Quill)#
macOS binaries are signed and notarized using Quill, an open-source alternative to gon that works cross-platform. The post-build hook lives in .goreleaser.yaml (search for quill sign-and-notarize) and is invoked from .github/workflows/release.yml (see lines 97–102, where the QUILL_* env vars are wired in from repository secrets).
All QUILL_* inputs are optional. If QUILL_SIGN_P12 is unset the entire hook is a no-op — the universal binary ships unsigned and nothing downstream fails. Set every variable in the table below (as repository secrets for CI, or exported locally for manual builds) if you want a fully signed and notarized macOS release.
Required variables (all or none)#
Either configure the full set or leave them all unset. Partial configuration will fail at notarization time.
| Variable | Purpose | Source |
|---|---|---|
QUILL_SIGN_P12 | Developer ID Application certificate (P12 file path or base64-encoded contents). Acts as the on/off toggle for the entire hook. | Export from Keychain Access after enrolling in the Apple Developer Program. |
QUILL_SIGN_PASSWORD | Passphrase that unlocks the P12. | Set when exporting the P12. |
QUILL_NOTARY_KEY | App Store Connect API key (path to AuthKey_XXXXX.p8 or base64-encoded contents). | Download once from App Store Connect → Users and Access → Integrations → App Store Connect API. |
QUILL_NOTARY_KEY_ID | 10-character Key ID for the API key above. | Shown next to the key in App Store Connect. |
QUILL_NOTARY_ISSUER | UUID of the issuer that owns the API key. | Shown at the top of the API keys page in App Store Connect. |
Optional variables#
| Variable | Default | Effect if unset |
|---|---|---|
QUILL_LOG_FILE | /tmp/quill-universal.log (set in .goreleaser.yaml) | Quill logs to stderr only; the release workflow keeps the goreleaser default path. |
How the hook behaves#
- Snapshot builds (
workflow_dispatchor--snapshot): ad-hoc signing only, notarization skipped (--dry-run=true --ad-hoc=true). Useful for smoke-testing without consuming Apple notarization quota. - Release builds (tag push): full sign + notarize against Apple (
--dry-run=false --ad-hoc=false). - The hook runs on the lipo'd universal binary, not per-arch, so the signed artifact is what ends up in the tarball.
- If
QUILL_SIGN_P12is empty the templatedquill sign-and-notarizecommand is not emitted at all — nothing to debug when signing is intentionally off.
Verify a signed macOS binary#
After downloading the release tarball:
tar -xzf opnDossier_Darwin_arm64.tar.gz
codesign -dv --verbose=4 ./opndossier # Shows Authority, TeamIdentifier, Timestamp
codesign --verify --strict --verbose=2 ./opndossier
spctl -a -vv -t exec ./opndossier # Gatekeeper check; should say "accepted" + "notarized"
quill itself can also round-trip the check:
quill extract signature ./opndossier # Inspects the embedded signature
See the Quill README for the full command surface and troubleshooting tips.
Release Artifacts#
Each release includes:
| Artifact | Description |
|---|---|
opnDossier_<OS>_<arch>.tar.gz | Binary archives (Linux, macOS, FreeBSD) with man page and completions |
opnDossier_<OS>_<arch>.zip | Binary archive (Windows) with THIRD_PARTY_NOTICES |
opndossier_<version>_amd64.deb | Debian/Ubuntu package with THIRD_PARTY_NOTICES in /usr/share/doc |
opndossier-<version>-1.x86_64.rpm | RHEL/Fedora package with THIRD_PARTY_NOTICES in /usr/share/doc |
opndossier_<version>_x86_64.apk | Alpine package with THIRD_PARTY_NOTICES |
opndossier-<version>-1-x86_64.pkg.tar.zst | Arch Linux package with THIRD_PARTY_NOTICES |
opnDossier_checksums.txt | SHA256 checksums for all artifacts |
opnDossier_checksums.txt.sigstore.json | Cosign v3 signature bundle |
*.sig | GPG detached signatures for archives/packages |
*.bom.json | Software Bill of Materials (CycloneDX SBOM) |
THIRD_PARTY_NOTICES | Human-readable license attribution for all dependencies |
Quick Release Checklist#
Copy-paste checklist for cutting a release. See sections above for details on each step.
Pre-flight#
- CI green on
main—gh run list --branch main --limit 5 -
just ci-checkpasses locally (lint, tests, race detector) - Review
pkg/*/testdata/api-snapshots/*.goldendiffs since the last tag for unintended public-API changes. Every new or removed line in these fixtures is a stability-tracked change — see docs/development/public-api.md § API Shape Enforcement. Regenerate withgo test ./pkg/parser/... -run TestPublicAPISnapshot -updateonly for intentional changes. - Milestone closed —
gh milestone list --state open, then close if exists - No uncommitted or unrelated changes on
main
Prepare#
- Preview Unreleased section —
git-cliff --unreleased(confirms commits are bucketed into Added / Changed / Deprecated / Removed / Fixed / Security correctly) - Generate changelog —
just changelog-version vX.Y.Z(promotes## [Unreleased]to## [vX.Y.Z] - YYYY-MM-DD) - Review
CHANGELOG.md— verify entries are correct, complete, and in the right Keep-a-Changelog buckets - Hand-curate Breaking Changes / Migration subsections beneath the new release header if any commits carry
BREAKING CHANGE: - Write or update
RELEASE_NOTES.md - Bump
action.ymldefaultversion:input to the new tag (e.g.vX.Y.Z) - Sweep README GitHub Action examples (
uses: EvilBit-Labs/opnDossier@vX.Y.Zlines) to the new tag — grep for the previous tag to catch every callsite - Sweep user-guide docs for stale version examples —
grep -rn "vPREV\.Y\.Z" docs/user-guide/and update all matches to the new tag (coversgetting-started.mdversion-output example,installation.mdcosign TAG example, and any future callsites) - Update the SHA-pin example in the README "Pinning" section to the new release commit SHA (
git rev-parse vX.Y.Zafter the tag is pushed, or the commit you intend to tag) - Commit
RELEASE_NOTES.md,CHANGELOG.md,action.yml, andREADME.mdtomain - Push to
main
Tag and Release#
- Ensure you are on
mainwith latest —git checkout main && git pull origin main - Create annotated tag —
git tag -a vX.Y.Z -m "Release vX.Y.Z" - Push tag —
git push origin vX.Y.Z(this triggers GoReleaser, which builds and publishes the GitHub release — do not also rungh release create)
Reminder: Always tag the commit on
main, never a feature branch head (see GOTCHAS.md #12.1). GoReleaser creates the release automatically on tag push; the manualgh release create/ GitHub-UI flow is only for the emergency local path (just release→goreleaser release --clean).
Post-release Verification#
- Monitor workflow —
gh run watchorgh run list --workflow=release.yml - Verify artifacts —
gh release view vX.Y.Z - Verify cosign signature — download checksums +
.sigstore.json, runcosign verify-blob - Test binary — download, run
opndossier --version, confirm version - Verify Docker —
docker pull ghcr.io/evilbit-labs/opndossier:vX.Y.Z - Verify Homebrew cask updated (if
HOMEBREW_TAP_TOKENis set)