Skip to content

Release Process

This document describes the automated release process for httptap.

Overview

Releases are fully automated using GitHub Actions. The workflow handles versioning, changelog generation, testing, building, signing, publishing to TestPyPI and PyPI, and pushing a signed container image to GHCR.

Prerequisites

Before creating a release, ensure:

  1. GitHub Environments - release, testpypi, and pypi environments configured in repository settings, each allowing deployments from main only; pypi has a required reviewer
  2. PyPI Trusted Publishing - Configured for both PyPI and TestPyPI (OIDC, no tokens)
  3. Deploy Key - SSH deploy key with write access, stored as the DEPLOY_KEY secret of the release environment only, and allowed to bypass the main branch protection and the tag ruleset that protects refs/tags/v*
  4. GHCR access - packages: write permission on the release job (granted per-workflow)
  5. All tests passing - CI must be green on main branch

Release Workflow

The release process is triggered manually via GitHub Actions.

Triggering a Release

  1. Go to Actions → Release workflow
  2. Click Run workflow
  3. Choose version strategy:
    • Explicit version: Enter exact version (e.g., 0.3.0)
    • Semantic bump: Select patch, minor, or major

Semantic Versioning

Bump Type Example Use Case
patch 0.1.0 → 0.1.1 Bug fixes, small improvements
minor 0.1.0 → 0.2.0 New features, backwards compatible
major 0.1.0 → 1.0.0 Breaking changes

What Happens Automatically

  1. Version Update

    uv version 0.2.0  # or
    uv version --bump minor
    
    Updates version in pyproject.toml

  2. Lockfile Refresh

    uv lock
    
    Regenerates uv.lock so it stays in sync with the new version

  3. Changelog Generation

    git cliff --tag v0.2.0 --unreleased --prepend CHANGELOG.md
    
    Generates changelog from conventional commits

  4. Signed Commit and Tag (local only)

    git commit -S -m "chore: release v0.2.0"
    git tag -s v0.2.0 -m "Release v0.2.0"
    git bundle create release.bundle "^$GITHUB_SHA" HEAD refs/tags/v0.2.0
    
    Keyless Sigstore signing via gitsign: a short-lived Fulcio certificate is issued through the workflow's OIDC identity, so no long-lived GPG keys are required. Nothing is pushed yet: the commit and tag travel to the next jobs as a git bundle artifact.

  5. Build

    uv sync --locked --no-dev --group test
    uv run --no-sync pytest  # Full test suite
    uv build  # Create wheel and sdist
    uv venv "$RUNNER_TEMP/httptap-wheel"
    uv pip install --python "$RUNNER_TEMP/httptap-wheel" "$(echo dist/httptap-*.whl)[otel]"
    uv sync --locked --no-dev --no-install-project --group test --group e2e
    uv run --no-sync pytest tests/e2e --no-cov -n auto --httptap "$RUNNER_TEMP/httptap-wheel/bin/httptap"
    
    Runs on the unpushed release tag from the bundle. The end-to-end suite then runs the CLI from the built wheel, installed with the otel extra, so a packaging mistake fails the release before anything is attested or uploaded.

  6. Push Commit and Tag

    git push --atomic origin "v0.2.0^{commit}:refs/heads/main" refs/tags/v0.2.0:refs/tags/v0.2.0
    
    Only after the build and attestation succeed. The push is fast-forward only and atomic, so if main moved during the release neither the branch nor the tag is updated and the workflow stops here, before anything is published.

  7. Publish to TestPyPI

    • Uploads to TestPyPI first via OIDC Trusted Publishing, with PEP 740 attestations, as a smoke test before the production push.
  8. Publish to PyPI

    • Uses OIDC Trusted Publishing (no tokens required)
    • Uploads wheel and source distribution with PEP 740 attestations
  9. Publish container image to GHCR

    • Runs only after the PyPI publish, so it waits for the pypi review
    • Builds multi-arch (linux/amd64, linux/arm64) image
    • Pushes to ghcr.io/ozeranskii/httptap with {version}, {major}.{minor}, {major}, and latest tags
    • Signs the image with cosign (keyless Sigstore)
    • Attaches SLSA build provenance via actions/attest-build-provenance
  10. GitHub Release

    • Creates release with generated notes
    • Attaches build artifacts, SBOMs, VEX, and the man page

Workflow Configuration

The release workflow is defined in .github/workflows/release.yml:

Key Jobs

1. Prepare Release

  • Checks out code (read-only, no deploy key)
  • Configures Python and uv
  • Updates version in pyproject.toml
  • Generates changelog
  • Adds the release to the fixed statements of .vex/httptap.openvex.json and bumps the document version
  • Creates the signed release commit and tag locally
  • Uploads them as a release-bundle artifact; nothing is pushed

2. Build Package

  • Checks out the unpushed release tag from the bundle
  • Runs full test suite
  • Builds wheel and sdist
  • Runs the end-to-end suite (tests/e2e) against the built wheel with the otel extra
  • Generates SBOM in CycloneDX and SPDX JSON formats via Syft
  • Fails if a fixed statement in .vex/httptap.openvex.json does not list the release, then copies the document into the sbom/ directory as httptap-X.Y.Z.openvex.json
  • Generates a gzipped man(1) page with argparse-manpage
  • Uploads dist/, sbom/, and man/ artifacts separately

3. Push Release Commit and Tag

  • Runs only after the build and provenance attestation succeed
  • The only job that writes to the git repository; it pushes over SSH with the deploy key of the release environment, so its workflow token is read-only (contents: read)
  • Fast-forwards main to the release commit and pushes the tag in one atomic push; fails without publishing anything if main moved during the release

4. Publish to TestPyPI

  • Downloads dist/ artifacts
  • Publishes via TestPyPI OIDC Trusted Publishing with PEP 740 attestations

5. Publish to PyPI

  • Runs only after TestPyPI succeeds
  • Publishes using Trusted Publishing with PEP 740 attestations

6. Publish container image to GHCR

  • Runs only after the PyPI publish, so nothing reaches GHCR before the pypi environment's review
  • Builds multi-arch image with Buildx + QEMU
  • Signs with cosign (keyless Sigstore OIDC)
  • Attaches SLSA build provenance

7. Create GitHub Release

  • The only job with contents: write, which it needs to create the release
  • Downloads dist/, sbom/, and man/ artifacts
  • Creates GitHub release with changelog notes
  • Attaches wheel, sdist, SBOM (*.cdx.json, *.spdx.json), VEX (*.openvex.json), and the man page

Changelog Generation

Changelogs are automatically generated using git-cliff based on conventional commits.

Commit Format

<type>(<scope>): <subject>

<body>

<footer>

Supported Types

Type Changelog Section Example
feat Features feat(cli): add --timeout flag
fix Bug Fixes fix(tls): handle expired certificates
perf Performance perf(dns): optimize resolver cache
docs Documentation docs: update API reference
refactor Refactor refactor(core): extract analyzer logic
test Testing test: add integration tests
chore Miscellaneous chore: update dependencies

Breaking Changes

Mark breaking changes in commit footer:

feat(api): redesign analyzer interface

BREAKING CHANGE: HTTPTapAnalyzer constructor signature changed

Version Strategy

httptap follows Semantic Versioning:

  • Major version (1.0.0) - Breaking changes
  • Minor version (0.1.0) - New features, backwards compatible
  • Patch version (0.0.1) - Bug fixes

Pre-1.0 Development

During pre-1.0 development (0.x.x):

  • Minor version may include breaking changes
  • Patch version for bug fixes and minor features
  • Move to 1.0.0 when API is stable

Troubleshooting

Branch Protection Errors

If push fails due to branch protection:

  1. Verify deploy key has write access
  2. Check deploy key is in bypass list for the branch protection rules and the refs/tags/v* tag ruleset
  3. Ensure ssh-key is configured in workflow checkout
  4. Check that DEPLOY_KEY is a secret of the release environment and that the workflow was run from main, the only branch the environment accepts

Changelog Empty

If changelog generation returns empty:

  1. Ensure commits follow conventional format
  2. Check git-cliff configuration in .release/git-cliff.toml
  3. Verify tag doesn't already exist

PyPI Publishing Fails

If PyPI publishing fails:

  1. Verify pypi environment exists
  2. Check Trusted Publishing is configured on PyPI
  3. Ensure workflow has id-token: write permission

Test Failures

If tests fail during release:

  1. Workflow will stop before publishing
  2. Fix issues and re-run workflow
  3. No partial releases will occur

Post-Release

After successful release:

  1. Verify package on PyPI: https://pypi.org/project/httptap/
  2. Check GitHub release: https://github.com/ozeranskii/httptap/releases
  3. Test installation: uv pip install httptap=={version}
  4. Announce release (e.g. GitHub Discussions, Telegram)

Release Checklist

Before triggering release:

  • All CI checks passing on main
  • No known critical bugs
  • Documentation updated
  • Breaking changes documented
  • Migration guide written (for major versions)
  • Dependencies updated
  • Security vulnerabilities addressed

See Also