RELEASING
Type
External
Status
Published
Created
Feb 27, 2026
Updated
Jul 17, 2026
Updated by
Dosu Bot

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

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 (this triggers GoReleaser, which builds and publishes the GitHub release — do not also run gh 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 manual gh release create / GitHub-UI flow is only for the emergency local path (just releasegoreleaser release --clean).

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)