Skip to content

release: validate build and upload tooling before pushing the tag - #181

Merged
villelaitila merged 1 commit into
softagram:mainfrom
villelaitila:fix/release-tooling-preflight
Aug 21, 2026
Merged

release: validate build and upload tooling before pushing the tag#181
villelaitila merged 1 commit into
softagram:mainfrom
villelaitila:fix/release-tooling-preflight

Conversation

@villelaitila

Copy link
Copy Markdown
Contributor

Releasing 1.11.0 crashed twice, and the second crash happened after the git tag was already public. This fixes the ordering that made that possible, and the two checks that were testing the wrong thing.

Three defects

A — the check probed a different interpreter than the one that runs the code. validate_preconditions and upload_to_pypi checked which twine (a PATH lookup), while the upload executes [sys.executable, "-m", "twine", ...] — the virtualenv interpreter. Those are two different Pythons, so the check passed against a system-wide twine while the actual call failed:

=== the script's check: which twine (PATH) ===
PASSES -> /Library/Frameworks/Python.framework/Versions/3.12/bin/twine

=== what the script actually runs: sys.executable -m twine ===
.../bin/python3: No module named twine

B — the build step checked nothing at all. build_distribution runs sys.executable setup.py sdist bdist_wheel with nothing verifying setuptools and wheel are importable there.

C — the ordering made A and B maximally expensive, and this is the real defect. Phase 2 runs sync → push tag (public, irreversible) → build → upload → release, and complete_release never called validate_preconditions at all. So a missing build tool fails after the tag exists, stranding a published tag with no artifacts behind it. Fixing A and B alone would leave that intact for the next tooling drift.

Neither A nor B mattered until an environment created by uv venv made both reachable at once — such an environment ships no pip, setuptools or twine, only the interpreter:

$ ls .venv/bin/
activate  activate.bat  ...  python  python3  python3.12

The fix

  • _module_available() probes sys.executable -c "import <module>" — the interpreter that will actually run the module
  • validate_release_tooling() runs first in complete_release, ahead of the tag, and raises rather than warns; gh stays a warning because it is recoverable by hand
  • the remaining which calls become shutil.which (one fewer subprocess, and it works on Windows, where which does not exist)

Raising rather than warning is deliberate: phase 2's failure mode is a half-published release, and a warning scrolling past is exactly what let this through.

validate_release_tooling is a separate method rather than an addition to validate_preconditions, because that one hard-fails on uncommitted changes and on not being on main — both wrong for phase 2, and probably why it was never called there.

Verification

End-to-end against a genuinely bare uv venv, reproducing the exact state that broke the release — it now fails before anything irreversible:

=== Validating release tooling ===
ReleaseError raised BEFORE any tag push:

  .../bin/python3 cannot import: setuptools, wheel, twine.
  Install them into this environment first:
    uv pip install setuptools wheel twine

Mutation testing — the implementation was deliberately broken four ways, and every one was caught by the test meant to catch it:

Mutation Result
preflight moved after the tag push (defect C) CAUGHT
probe reverted to a PATH lookup (defect A) CAUGHT
missing tooling warns instead of raising CAUGHT
setuptools/wheel dropped from the probe list (defect B) CAUGHT

327 tests pass. flake8 unchanged at its existing baseline, with no violations on the new lines.

Two existing tests were repaired

Moving the gh check to shutil.which silently degraded both TestPushAndCreatePr tests, and neither failure was visible from a green run:

  • test_returns_none_when_gh_not_available began taking the gh-available branch and returned None only because the mocked stdout contained no PR URL — passing for the wrong reason
  • test_returns_pr_number_from_gh_output became dependent on gh being installed on the machine running the suite, and would fail on any machine without it

Both now patch release.shutil.which, and the first additionally asserts gh pr create is never reached.

Documentation

scripts/README.md gains a prerequisite explaining that the modules must be importable by the interpreter running the script, not merely present on PATH, and that a uv venv environment ships none of them. Both step-lists (module docstring and README) are renumbered for the new step.

The file is added to the repository in this change: CLAUDE.md already directs readers to scripts/README.md for setup instructions, but it had never been tracked, so that reference was dangling for anyone cloning the repo.

The PyPI step checked `which twine` but ran `sys.executable -m twine`, so a
twine belonging to some other Python satisfied the check while the actual
call failed. The build step checked nothing at all. Neither mattered until
a `uv venv` environment - which ships no pip, setuptools or twine - made
both reachable at once.

Phase 2 pushes the git tag before it builds, so either failure stranded a
published tag with no artifacts behind it. That is the defect: the checks
being wrong is recoverable, the ordering is not.

- _module_available() probes sys.executable, the interpreter that will
  actually run the module, rather than PATH
- validate_release_tooling() runs first in complete_release, before the
  tag, and raises rather than warns; gh stays a warning because it is
  recoverable by hand
- the remaining `which` calls become shutil.which

Two existing tests were repaired: moving the gh check to shutil.which had
left test_returns_none_when_gh_not_available taking the gh-available branch
and passing for the wrong reason, and made the other test depend on gh
being installed on the machine running it.

scripts/README.md is added to the repository - CLAUDE.md already points
readers to it, but it had never been tracked.
@softagram-bot

Copy link
Copy Markdown

Softagram Impact Report for pull/181 (head commit: 2831bd7)

TL;DR Arch. Impact: -10 | Changed code files: 3 | Directly impacted code files: 0

⭐ Change Overview

Showing the changed files, dependency changes and the impact - click for full size
(Open in Softagram Desktop for full details)

⭐ Details of Dependency Changes (diagram)

details of dependency changes - click for full size
(Open in Softagram Desktop for full details)

🤖 AGENTS - machine-readable impact data (3 files changed, 0 impacted, +17/-1 deps)

Change overview

Head 2831bd7915f5 vs base 6eb45ab058d5. 3 code files changed. 0 unchanged files directly depend on the changed files (see Impacted files). Dependencies: 17 added, 1 removed. New external components: 1. Removed external components: 0.

Added dependencies (17)

from to type roles signal
sgraph/scripts/release.py External/PythonLibs/shutil import prod→external new external component
sgraph/tests/test_release_automation.py/TestCompleteReleaseOrdering/setup_method External/Python/Usual dependencies import test→external external dependency
sgraph/tests/test_release_automation.py/TestModuleAvailable/setup_method External/Python/Usual dependencies import test→external external dependency
sgraph/tests/test_release_automation.py/TestModuleAvailable/test_probes_sys_executable_not_path External/PythonLibs/Usual dependencies import_ref test→external external dependency
sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/setup_method External/Python/Usual dependencies import test→external external dependency
sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/test_error_names_every_missing_module External/PythonLibs/Usual dependencies import_ref test→external external dependency
sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/test_missing_gh_only_warns External/PythonLibs/Usual dependencies import_ref test→external external dependency
sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/test_passes_when_all_modules_present External/PythonLibs/Usual dependencies import_ref test→external external dependency
sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/test_raises_when_a_module_is_missing External/PythonLibs/Usual dependencies import_ref test→external external dependency
sgraph/scripts/release.py/ReleaseAutomation/complete_release sgraph/scripts/release.py/ReleaseAutomation/validate_release_tooling func_ref prod→prod regular
sgraph/scripts/release.py/ReleaseAutomation/validate_release_tooling sgraph/scripts/release.py/ReleaseError/init new prod→prod regular
sgraph/tests/test_release_automation.py/TestCompleteReleaseOrdering/setup_method sgraph/tests/test_release_automation.py/TestCompleteReleaseOrdering/var automation var_ref test→test regular
sgraph/tests/test_release_automation.py/TestModuleAvailable/setup_method sgraph/tests/test_release_automation.py/TestModuleAvailable/var automation var_ref test→test regular
sgraph/tests/test_release_automation.py/TestModuleAvailable/test_probes_sys_executable_not_path sgraph/tests/test_release_automation.py/TestModuleAvailable/var return_value var_ref test→test regular
sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/setup_method sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/var automation var_ref test→test regular
sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/test_error_names_every_missing_module sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/var side_effect var_ref test→test regular
sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/test_raises_when_a_module_is_missing sgraph/tests/test_release_automation.py/TestValidateReleaseTooling/var side_effect var_ref test→test regular

Removed dependencies (1)

from to type roles signal
sgraph/tests/test_release_automation.py/TestPushAndCreatePr/test_returns_pr_number_from_gh_output sgraph/tests/test_release_automation.py/TestPushAndCreatePr/var return_value var_ref test→test regular

Impacted files (0)

None.

Complete data

[]

📄 Full report

Impact Report explained. Give feedback on this report to support@softagram.com

@villelaitila
villelaitila merged commit b55c0aa into softagram:main Aug 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants