| Created | 2026-08-11 |
|---|---|
| Last-Modified | 2026-11-06 |
| SPDX-FileCopyrightText | 2026-present Arthit Suriyawongkul |
| SPDX-FileType | DOCUMENTATION |
| SPDX-License-Identifier | CC0-1.0 |
Steps for cutting a Pitloom release, maintainer-facing. Distinct from CONTRIBUTING.md's per-PR checklist -- this is the release-cutting process itself, run once per version bump.
CI already covers most of this list on the release PR -- check the green
run before repeating it locally. Job-name map (gh pr checks shows job
names, not workflow names): pytest = Test on Python ...; mypy, pyright and
pyrefly = Type checking (mypy, pyright, pyrefly); ruff and flake8 = Ruff (Lint & Format); pylint = Pylint; plugin validation = claude plugin validate; version fields = Check version fields agree; python -m build --wheel + PEP 770
embed + verify-wheel/validate-wheel on Pitloom's own wheel =
Python X on <os> (build.yml, ubuntu/windows/macos); the hook against
the Hatchling floor and latest = Hook on Python X / Hatchling Y
(hatch-integration.yml). Docs-only commits skip most workflows via
paths-ignore.
-
pytest tests/ -q-- 0 failed. -
mypy examples/ src/ tests/-- clean. -
ruff check examples/ src/ tests/andruff format --check examples/ src/ tests/-- clean. -
pylint examples/ src/ tests/ tests/*/-- 10.00/10 (tests/*/: pylint does not descend intotests/subfolders on its own;--ignore-pathsany stray local.venvunderexamples/). -
scripts/is not linted by CI: run ruff, mypy, pylint, flake8,shellcheck -x scripts/action/*.shandactionlinton it by hand when it changed since the last tag. -
claude plugin validate .claude-plugin/plugin.jsonand.../marketplace.json-- both pass. - Version string consistent across every file that carries one:
pyproject.toml,src/pitloom/__about__.py,.claude-plugin/plugin.json,CITATION.cff,codemeta.json,README.md,action.yml,docs/index.md,docs/github-action.md(alsopip install pitloom==<version>in its registry recipe),working-docs/implementation/github-action.md. Check withgrep -rn "<old-version>"across those files -- anything left over is a missed bump. Dependency floors (e.g.hatchling>=) are not covered byscripts/check_version_consistency.py: grep them by hand acrosspyproject.toml, README,docs/, examples, CI matrix, comments and test literals (see recurring-bug-patterns.md). - Skills' minimum Pitloom version (
pitloom>=X.Y.Zin eachskills/*/SKILL.mdcompatibility, their install recipes,references/examples.mdanddocs/agent-skills.md): not above the version being released, and raised whenever this release breaks a CLI flag or subcommand the skills use. Until the release is on PyPI theuvx/pipxexamples that name it fail by design.scripts/check_version_consistency.pydoes not check this:grep -rn "pitloom.*>=" skills/ docs/agent-skills.md. - Pinned older releases in prose are deliberate, not missed bumps
(
docs/id-registry.md's workflow pins,docs/github-action.md's "@v0.20.2reads version 2"): when a doc says "the first release with X", it names the release that introduced X, so re-read each such pin against the new version. - Coverage snapshots:
working-docs/implementation/g7-coverage-history/holds<version>.json(G7) and<standard>-<version>.json(CISA 2026, FSCT 2024, BSI), written byscripts/g7_coverage_report.py --standard g7ai|cisa2026|fsct3|bsi -o FILE(see g7-conformance-tests.md). Regenerate each into a scratch file anddiffit against the committed one: onlyheader.pitloommay differ (it reads the installed package metadata, so run it from an environment whosepitloomis the version being released, or accept that line). -
CHANGELOG.md: every merged PR since the last tag either has an entry, or is a routine dependabot/CI-only/docs-only/test-only change that doesn't need one (cross-checkgit log --oneline <last-tag>..HEAD | grep "Merge pull request"against the[#N]:link refs at the bottom of the file, and check every[#N]used in the new section is defined and every one defined is used; move the[Unreleased]entries under the new version heading, sorted per section by lowest PR number, add the[X.Y.Z]:compare link, and leave the archive alone). Include the version-bump PR itself when it carries more than version strings (0.18.1's bump PR also lowered a dependency floor and changed the skills, and had no entry until this check caught it). -
python -m build --wheelsucceeds locally; the built wheel embeds<name>-<version>.dist-info/sboms/<name>-<version>.spdx3.json(PEP 770). - Run the
Fuzzworkflow (workflow_dispatch, all three targets) for a major release; skim the run for crash artefacts before proceeding. See fuzzing.md for why this is manual/pre-release rather than continuous or RC-tag-triggered.
-
The release SBOM is the one the build hook embedded in the wheel: the publish workflow's
scripts/extract_wheel_sbom.py --require-magikastep copies it out byte for byte (no second generation) and fails unless it is the wheel's only SBOM, named*.spdx3.json, matches itsRECORDhash and has every content type detected by magika (the release build installs.[content-type]; the project itself leaves magika optional).build.ymlruns the same script on every push and PR, so a break shows before release. -
spdx3-validateis a hard gate on that SBOM: the publish workflow validates the release wheel and the extracted SBOM and runs thenetwork-marked tests withPITLOOM_REQUIRE_NETWORK=1beforepublish, so an unreachable spdx.org/PyPI blocks publishing (re-run the workflow); it is never skipped. An outage of a third-party forge fails it too (0.21.1:code.overheid.nlDNS,git.sr.htconnect): "Re-run failed jobs" only, once the host answers; never "Re-run all jobs" afterpublish. -
The same workflow's
scripts/check_sbom_license.pystep fails unless the release SBOM, embedded and extracted, declares and concludesApache-2.0on thepitloompackage. Thebuildjob has no LicenseID database, so the concluded licence comes fromCITATION.cff'slicense, elsecodemeta.json's: keep it there.build.ymlruns the same check on every push and PR, on the hook-embedded andloom projectSBOMs, so a change there fails before release. -
Tag the release, push the tag, publish to PyPI (however this project's release automation does it -- not scripted here). The GitHub Action installs the version its pinned ref carries, so
uses: bact/pitloom@<new-tag>fails until PyPI serves that version; don't announce the tag before the publish completes. -
If a job fails after
publishhas run, use "Re-run failed jobs" only, within the 3-day artifact retention: "Re-run all jobs" rebuilds a wheel with a new SBOMcreatedtime, which PyPI refuses and the release assets would no longer match.
Local build success in step 1 checks Pitloom's own build; it does not prove what PyPI actually serves. Verify the real, externally-hosted wheel directly:
-
Resolve the wheel URL via
https://pypi.org/pypi/pitloom/<version>/jsonand download it; verify its SHA-256 against the digest PyPI's own API publishes for that file. -
Unzip it and inspect
pitloom-<version>.dist-info/sboms/pitloom-<version>.spdx3.jsondirectly -- the actual bytes a consumer gets, not a regenerated copy. -
Confirm PEP 770 location and recommended extension (
loom verify-wheel <downloaded.whl> --sbom-filename pitloom-<version>.spdx3.json), run schema + SHACL validation (loom validate-wheel <downloaded.whl>), recompute everysoftware_File's SHA-256 from the extracted bytes and cross-check against the wheel's ownRECORD, and confirm the main package's PURL/license relationships/creator identity are as expected. -
Record the result as a new dated entry in wheel-sbom-verification.md, following its existing entries' format -- this is what makes each release's verification durable evidence instead of a one-off chat answer that disappears with the session that produced it.
-
Run a throwaway workflow with
uses: bact/pitloom@<new-tag>(and, for the SHA-pin path,@<tag's commit SHA>) against a small project: it must install exactly<version>and emit no warning annotation beyond the expected Python-selection one. See github-action.md.
- If a draft release already exists for this tag, regenerate its
notes before publishing -- a draft created early in the release
window (e.g. right after the first RC) will be missing every PR
merged since. Don't publish a draft without checking its PR list
against
git log --oneline <last-tag>..HEADfirst. - After publishing, the release lists the wheel, the sdist, the
standalone SBOM and a
*.sigstore.jsonbundle for each. Download them and check:cmpthe standalone SBOM against the wheel's.dist-info/sboms/member (unzip -p), then for the wheel, the sdist and the SBOM run (bundle defaults to<file>.sigstore.json):python -m sigstore verify github <file> --cert-identity https://github.com/bact/pitloom/.github/workflows/pypi-publish.yml@refs/tags/v<version>andgh attestation verify <file> -R bact/pitloom --signer-workflow bact/pitloom/.github/workflows/pypi-publish.yml --source-ref refs/tags/v<version>(the attestation is GitHub's, not a release asset; same commands as the README's "Pitloom's own SBOM and signatures").