Skip to content

Releasing ​

Releases are tag-triggered. Pushing a vX.Y.Z tag runs .github/workflows/release.yml, which:

  1. verifies the tag matches version in pyproject.toml (fails fast on drift),
  2. re-runs the full gate (gates.yml — tests across Python 3.10–3.13, lint, types, benchmark),
  3. publishes in parallel:
    • PyPI — sdist + wheel, so uv tool install malware-detector-py / pipx install malware-detector-py get the new version,
    • GHCR — the Docker image to ghcr.io/benzid-wael/malware-detector, tagged with the version and latest.

The package version is static in pyproject.toml; the tag must match it. Use scripts/release.sh to keep the two in sync (prepare bumps the version, tag tags and pushes).


One-time setup ​

Do these once, before the first release. None require secrets in the repo.

1. PyPI Trusted Publishing (OIDC) ​

Publishing uses PyPI Trusted Publishing — no API token is stored anywhere. Because malware-detector-py is not on PyPI yet, register a pending publisher:

  1. Sign in to PyPI → https://pypi.org/manage/account/publishing/.
  2. Add a new pending trusted publisher:
    • PyPI project name: malware-detector-py
    • Owner: benzid-wael
    • Repository name: malware-detector
    • Workflow name: release.yml
    • Environment name: pypi
  3. The first successful publish creates the project and converts it to a normal trusted publisher.

Prefer a token instead? Store it as the PYPI_API_TOKEN repo secret and swap the pypi job's publish step to with: { password: ${{ secrets.PYPI_API_TOKEN }} }. Trusted Publishing is recommended — there is no long-lived credential to leak or rotate.

2. GitHub pypi environment ​

The pypi job runs in a pypi environment (matching the trusted-publisher config above):

  1. Repo Settings → Environments → New environment → name it pypi.
  2. (Optional, recommended) add a required reviewer so a human approves each PyPI publish, and/or restrict deployments to tag refs.

3. GHCR package visibility ​

The Docker push authenticates with the built-in GITHUB_TOKEN (packages: write) — no setup needed to publish. After the first release:

  1. Repo Packages → malware-detector → Package settings.
  2. Set visibility to Public (if you want unauthenticated docker pull) and link the package to this repository.

Cutting a release ​

A release takes two steps, so the version bump goes through a PR like any other change:

sh
# 1. From main: create release/v0.1.0, bump the version and commit (nothing is pushed)
scripts/release.sh prepare 0.1.0 --dry-run   # preview
scripts/release.sh prepare 0.1.0
#    → push the branch, open a PR, merge it once CI is green

# 2. On main, after the merge: tag the release commit and push the tag
git checkout main && git pull --ff-only
scripts/release.sh tag 0.1.0 --dry-run       # preview
scripts/release.sh tag 0.1.0                 # asks for confirmation

prepare updates version in pyproject.toml, refreshes uv.lock (it records the project's own version, and CI installs with --locked), and points the pre-commit rev: examples in README.md and the pre-commit guide at the new tag. It then commits release: v0.1.0. Run from main, it first switches to a new release/v0.1.0 branch, so main itself is never committed to; from any other branch it commits there.

tag refuses to run unless you're on main, level with origin/main, with a clean tree and a pyproject.toml version equal to the one you pass. It creates the annotated tag v0.1.0 and pushes only the tag, which triggers release.yml → verify → gate → publish to PyPI + GHCR. If the pypi environment has a required reviewer, approve the run under Actions.

Then confirm the artifacts:

sh
uv tool install --force malware-detector-py==0.1.0 && malware-detector --version
docker pull ghcr.io/benzid-wael/malware-detector:0.1.0

Optionally publish release notes: gh release create v0.1.0 --generate-notes.

Note on the version/tag contract: the static version and the tag are two places that must agree. release.yml's verify job is the safety net — a tag that doesn't match pyproject.toml fails before anything is published.

Fixing a bad release ​

PyPI releases are immutable and cannot be overwritten — yank the bad version (pip/uv will skip yanked versions) and publish a new patch. Delete the bad GHCR tag from the package's versions page if needed.