Guides

Release Guide

Build tooling

Ananke Plexus uses uv for all build, test, and publish operations. twine is not a runtime dependency.

ActionCommand
Install dev envmake install / uv sync --all-extras
Build artifactsmake build / uv build
Validate metadatauv run --with twine twine check dist/*
Publish to TestPyPImake publish-testpypi
Publish to PyPImake publish-pypi
Security scanmake trivy-scan

1. One-time GitHub repository setup

  1. Create the package in both TestPyPI and PyPI with name ananke-plexus.
  2. Configure Trusted Publishers (OIDC) in each index under your PyPI account:
    • Publisher type: GitHub Actions
    • Owner: neo-pragnya
    • Repository: ananke-plexus
    • Workflow: release-pypi.yml (or release-testpypi.yml for TestPyPI)
    • Environment: pypi (or testpypi)
  3. Create protected GitHub environments named pypi and testpypi in repo Settings → Environments.
  4. Enable required reviewers on the pypi environment for extra protection.

No long-lived PyPI API tokens are stored in GitHub secrets — OIDC handles the authentication automatically.


2. Pre-release quality gate

make install       # uv sync --all-extras
make verify        # ruff + mypy + pytest
make trivy-scan    # Trivy filesystem CVE scan (blocks on CRITICAL/HIGH)
make coverage      # coverage report
make acceptance-wheel  # capability scenarios against the built wheel + release gate
make scan          # what package firewalls check, on the built wheel and sdist (scan-report/summary.md)

The acceptance gate (acceptance/) installs the built wheel into a fresh virtual environment and runs more than 60 Gherkin scenarios against the real CLI, each in its own sandbox project. The scenarios cover install, specs, graph, policy, hooks, verify, the registry and its security, Dex Trove, MCP, workflows, docs and the testing harness. The gate decides:

DecisionWhenRelease
PROMOTEevery capability passedship
CONDITIONALknown issues, unverified or non-blocker gaps (all listed in acceptance/report/summary.md)ship deliberately, with the list in the release notes
BLOCKa blocker capability failed or has no scenariodo not ship

The release-pypi workflow runs the same gate on the exact wheel it publishes, and publishing waits for it. See acceptance/README.md to add scenarios.


3. Local token-based publish (manual fallback)

Only needed if OIDC is not configured. Set UV_PUBLISH_TOKEN in .env:

cp .env.example .env
# Edit .env: set UV_PUBLISH_TOKEN and UV_PUBLISH_TOKEN_TESTPYPI

# Publish to TestPyPI first
make publish-testpypi

# Validate the TestPyPI install
uv tool install --index https://test.pypi.org/simple/ ananke-plexus
ananke version

# Publish to PyPI
make publish-pypi

  1. Bump version in src/ananke/plexus/version.py.
  2. Update CHANGELOG.md with release notes.
  3. Commit, push, then tag:
VERSION="$(uv run python -c 'from ananke.plexus.version import __version__; print(__version__)')"

git add src/ananke/plexus/version.py CHANGELOG.md
git commit -m "chore: release v$VERSION"
git tag "v$VERSION"
git push origin main "v$VERSION"

The workflows run in this order:

release-testpypi.yml        release-pypi.yml
  └─ build                    └─ build
                              └─ acceptance (built wheel)
                              └─ dist-scan (blocking: secrets, vulnerable dependencies)
  └─ trivy (blocking)         └─ trivy (blocking)
  └─ provenance               └─ provenance
  └─ publish TestPyPI         └─ publish PyPI
                              └─ GitHub Release

Build-once principle: the same dist artifacts built in the build job are downloaded by the publish job — never rebuilt between validation and publishing.


5. What the release workflows produce

ArtifactDescription
dist/*.whlPython wheel
dist/*.tar.gzSource distribution
dist/sbom.cdx.jsonCycloneDX SBOM
trivy-results.sarifTrivy SARIF (uploaded to GitHub Security)
dist-scan-<tag>Distribution scan report: summary.md and scan.json
Build attestationGitHub artifact provenance via actions/attest-build-provenance

6. Security gates in every release

Both release workflows have Trivy as a hard gate:

trivy job (CRITICAL + HIGH, unfixed) → exit-code 1 → blocks publish job

This means a release cannot proceed if there are unfixed CRITICAL or HIGH CVEs in the package or its dependencies.

Distribution scan: what package firewalls see

Trivy scans the repository. Package firewalls and SCA tools (Black Duck, JFrog Xray and Curation, Sonatype, Socket) scan the published files, and quarantine a release for secret-shaped strings, vulnerable dependencies, malicious-code heuristics or licence mismatches. The dist-scan job runs scripts/scan_dist.py on the exact wheel and sdist the release will publish, using free tools at pinned versions:

CheckToolEffect
Secrets in the wheel and sdistgitleaksblocks publish
Vulnerable dependencies in the tree the wheel resolves to (all,mcp,dex extras)pip-auditblocks publish
Minified or third-party code, and whether License-Expression covers itbuilt inblocks publish unless accepted
Malware-shaped code, docs included: keychain dumps, browser password stores, SSH and cloud keys, pipe-to-shell, encoded PowerShell, running decoded data, chat webhooks, tunnels, hidden Unicodebuilt inblocks publish unless accepted
Malicious-package heuristicsGuardDogblocks on any rule not in the reviewed baseline
High-severity code issues in the shipped codeBanditblocks publish
Metadata: version 2.4 or older, a licence classifier and a licence file, so SCA parsers can read thembuilt inblocks publish
The oldest version each declared range allows, audited, so a range can’t admit a vulnerable releaseuv, pip-auditblocks publish
Every resolved dependency’s licence, and what pulls it inbuilt in (pip-licenses and pipdeptree’s view)blocks on copyleft or unknown unless accepted
Copyleft licence names inside shipped files, which licence search reads as a declarationbuilt inblocks unless accepted; written to triage.md
Dependency versions under 14 days old, which maturity rules blockPyPIreported, with the install command that avoids them
Inventory: scripts, binaries, minified JS, declared licencebuilt inreported

Run it locally with make scan (it needs gitleaks and uv; the Python tools come through uvx).

Before a push, scripts/prepush_scan.py runs osv-scanner on the lock files and Semgrep’s security-audit and secrets rules on the source, in about a minute. Opt in with pre-commit install --hook-type pre-push.

Reviewed findings live in .scan-dist.toml, each with a reason, and a rule can be accepted only in the files it was reviewed in, so the same rule matching anywhere else stops the release. A bundled file is accepted only with a licence that’s in License-Expression.

Why these block now (0.3.29). Releases up to 0.3.28 were quarantined, and every one scanned showed the same causes, which this guide had listed as “reported” and accepted:

  • a vendored, minified mermaid.min.js (3.5 MB) for the offline docs. GuardDog calls it threat-runtime-obfuscation, and it held code under licences the package didn’t declare. It’s gone: the offline docs show diagrams as source.
  • the macOS keychain command that prints a stored password, used to read a keychain secret: infostealers run the same command. Secrets are now read through keyring.

What’s still accepted, with reasons, is in .scan-dist.toml: the governance evaluator’s deny-list containing rm -rf /, read_source refusing .env, and Ananke’s own 0700 credentials file.

triage.md lists each file that names a licence Ananke isn’t under (its licence-detection tables, and the note about Serena’s licence), with the reason to give Black Duck or a similar tool when it reports a licence match.

If your organisation’s firewall quarantines a release, ask for the policy violation names and compare them with this report. Packages only days old are often held by maturity rules too, whatever their content.

Additionally the weekly security.yml workflow runs:

  • Trivy filesystem scan (SARIF + blocking)
  • Semgrep SAST
  • pip-audit dependency CVE check
  • Gitleaks secrets scan

7. Post-release validation

# Install from PyPI and smoke-test
uv tool install ananke-plexus
ananke version
ananke doctor

# Or with pip
pip install -U ananke-plexus
ananke --help

8. Trivy local usage

# Install Trivy: https://aquasecurity.github.io/trivy/latest/getting-started/installation/

# Table report (blocks on CRITICAL/HIGH)
make trivy-scan

# JSON report (all severities)
make trivy-scan-json

# SARIF for IDE or GitHub upload
make trivy-scan-sarif

Trivy config can be customised via .trivyignore at the repo root.

Edit this page on GitHub ↗