Documents
RELEASING
RELEASING
Type
External
Status
Published
Created
Feb 27, 2026
Updated
Apr 21, 2026
Updated by
Dosu Bot
Source
View

Releasing opnDossier#

This document describes the release process for opnDossier.

Table of Contents#

Version Numbering#

opnDossier follows Semantic Versioning 2.0.0:

Version ComponentWhen to IncrementExample
MAJOR (X.0.0)Breaking changes to CLI interface, config format, or APIRemoving a flag, changing output format
MINOR (0.X.0)New features, backward-compatible additionsNew audit plugin, new output format
PATCH (0.0.X)Bug fixes, documentation, performance improvementsFix 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 assets
  • id-token: write - SLSA provenance and Cosign keyless signing

GitHub Secrets#

For GPG signing of release artifacts, add these repository secrets:

SecretDescription
GPG_PRIVATE_KEYBase64-encoded GPG private key (gpg --armor --export-secret-keys EMAIL | base64)
GPG_PASSPHRASEPassphrase 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 main branch
  • All issues/PRs for the milestone are closed
  • CHANGELOG.md is up to date (or will be auto-generated)
  • Version references in code are correct (if any hardcoded)
  • action.yml default version: 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 — the GitHub Release body is populated from this file via gh release create --notes-file RELEASE_NOTES.md (see Step 4 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 prefixChangelog 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#

# Ensure you're on main with latest changes
git checkout main
git pull origin main

# Create annotated tag
git tag -a v1.2.0 -m "Release v1.2.0"

# Push tag to trigger release workflow
git push origin v1.2.0

Step 4: Create GitHub Release#

Option A: Via GitHub UI (Recommended)

  1. Go to Releases
  2. Click "Draft a new release"
  3. Select the tag you just pushed
  4. Click "Generate release notes" for auto-generated notes
  5. Review and edit the release notes
  6. Click "Publish release"

Option B: Via CLI

# Create release from tag (triggers workflow)
gh release create v1.2.0 \
  --title "v1.2.0" \
  --generate-notes

# Or with custom notes
gh release create v1.2.0 \
  --title "v1.2.0" \
  --notes-file RELEASE_NOTES.md

Step 5: Monitor 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
git tag -a v1.2.0-rc1 -m "Release candidate 1 for v1.2.0"
git push origin v1.2.0-rc1

# Create pre-release on GitHub
gh release create v1.2.0-rc1 \
  --title "v1.2.0-rc1" \
  --prerelease \
  --generate-notes

Promoting RC to Release#

If the RC is stable:

# Tag the same commit as final release
git tag -a v1.2.0 -m "Release v1.2.0"
git push origin v1.2.0

# Create the final release
gh release create v1.2.0 --title "v1.2.0" --generate-notes

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.

VariablePurposeSource
QUILL_SIGN_P12Developer 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_PASSWORDPassphrase that unlocks the P12.Set when exporting the P12.
QUILL_NOTARY_KEYApp 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_ID10-character Key ID for the API key above.Shown next to the key in App Store Connect.
QUILL_NOTARY_ISSUERUUID of the issuer that owns the API key.Shown at the top of the API keys page in App Store Connect.

Optional variables#

VariableDefaultEffect 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_dispatch or --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_P12 is empty the templated quill sign-and-notarize command 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:

ArtifactDescription
opnDossier_<OS>_<arch>.tar.gzBinary archives (Linux, macOS, FreeBSD) with man page and completions
opnDossier_<OS>_<arch>.zipBinary archive (Windows) with THIRD_PARTY_NOTICES
opndossier_<version>_amd64.debDebian/Ubuntu package with THIRD_PARTY_NOTICES in /usr/share/doc
opndossier-<version>-1.x86_64.rpmRHEL/Fedora package with THIRD_PARTY_NOTICES in /usr/share/doc
opndossier_<version>_x86_64.apkAlpine package with THIRD_PARTY_NOTICES
opndossier-<version>-1-x86_64.pkg.tar.zstArch Linux package with THIRD_PARTY_NOTICES
opnDossier_checksums.txtSHA256 checksums for all artifacts
opnDossier_checksums.txt.sigstore.jsonCosign v3 signature bundle
*.sigGPG detached signatures for archives/packages
*.bom.jsonSoftware Bill of Materials (CycloneDX SBOM)
THIRD_PARTY_NOTICESHuman-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 maingh run list --branch main --limit 5
  • just ci-check passes locally (lint, tests, race detector)
  • Review pkg/*/testdata/api-snapshots/*.golden diffs 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 with go test ./pkg/parser/... -run TestPublicAPISnapshot -update only 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.yml default version: input to the new tag (e.g. vX.Y.Z)
  • Sweep README GitHub Action examples (uses: EvilBit-Labs/opnDossier@vX.Y.Z lines) 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 (covers getting-started.md version-output example, installation.md cosign 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.Z after the tag is pushed, or the commit you intend to tag)
  • Commit RELEASE_NOTES.md, CHANGELOG.md, action.yml, and README.md to main
  • Push to main

Tag and Release#

  • Ensure you are on main with 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
  • Create GitHub release — gh release create vX.Y.Z --title "vX.Y.Z" --notes-file RELEASE_NOTES.md

Reminder: Always tag the commit on main, never a feature branch head (see GOTCHAS.md #12.1).

Post-release Verification#

  • Monitor workflow — gh run watch or gh run list --workflow=release.yml
  • Verify artifacts — gh release view vX.Y.Z
  • Verify cosign signature — download checksums + .sigstore.json, run cosign 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_TOKEN is set)