From 0205c755c840a30f4317dd9bc5a4fd0e7f14dd69 Mon Sep 17 00:00:00 2001 From: SSobol77 Date: Sat, 11 Jul 2026 00:53:49 +0200 Subject: [PATCH] docs: align release validation gate contracts --- Makefile | 7 +- docs/contributor/development-setup.md | 16 ++- docs/release/release-process.md | 30 +++-- .../test_release_gate_contract_alignment.py | 122 ++++++++++++++++++ .../test_release_asset_count_gate.py | 27 ++++ 5 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 tests/docs/test_release_gate_contract_alignment.py diff --git a/Makefile b/Makefile index c1a1602..c711dd2 100755 --- a/Makefile +++ b/Makefile @@ -169,6 +169,11 @@ help: @echo " make show-artifacts Inspect built artifacts" @echo " make clean Remove local build/cache outputs" @echo "" + @echo "Release validation gates" + @echo " make validate-gate2 Source and structural contract gate" + @echo " make validate-built-artifacts Validate available built artifacts fail-closed" + @echo " make validate-release-assets Verify exact final GitHub Release asset set" + @echo "" @echo "Package groups" @echo " make package-pypi Build wheel + sdist" @echo " make package-linux Build Linux package set" @@ -198,7 +203,6 @@ help-full: help @echo " make validate-full Ruff, mypy, full pytest, runtime imports" @echo " make validate-packaging Packaging contract suite" @echo " make validate-release-contract Release/package matrix contract checks" - @echo " make validate-release-assets Exact 21 GitHub Release asset gate" @echo " make validate-official-evidence-drift" @echo " make validate-version-consistency" @echo " make validate-runtime-imports" @@ -210,7 +214,6 @@ help-full: help @echo " make validate-freebsd-contract" @echo " make validate-macos-contract" @echo " make validate-windows-contract" - @echo " make validate-gate2" @echo "" @echo "Python / PyPI" @echo " make package-pypi Build wheel + sdist" diff --git a/docs/contributor/development-setup.md b/docs/contributor/development-setup.md index 8d35cf1..c44a85a 100644 --- a/docs/contributor/development-setup.md +++ b/docs/contributor/development-setup.md @@ -32,7 +32,7 @@ See the LICENSE file in the project root for full license text. | `task` | Optional | developer convenience wrapper over Makefile targets | all platforms | | `pipx` | Optional | convenient tool installation | all platforms | | `ruff` / `pytest` toolchain | Role-dependent | local validation | maintainer-focused | -| `twine` | Release-only | PyPI artifact validation in `make validate-gate2` | install via `.[release]`, `.[dev]`, or `requirements-dev.txt` | +| `twine` | Release-only | Final PyPI built-artifact validation through `make validate-built-artifacts` / `make validate-pypi-contract` | install via `.[release]`, `.[dev]`, or `requirements-dev.txt` | | `makensis` | Optional by role | Windows packaging | Windows packaging maintainers | | `hdiutil` | Optional by role | macOS DMG packaging | macOS only | | FreeBSD pkg toolchain | Optional by role | FreeBSD package build | FreeBSD environment only | @@ -61,6 +61,20 @@ delegate to existing Makefile targets and must not define Taskfile-only release or packaging behavior. CI and release gates continue to rely on the existing canonical command surfaces. +The release validation layers are intentionally separate: + +- `make validate-gate2` is source and structural contract validation only. It + checks version consistency, runtime imports, Linux official evidence drift, + and the PyPI source contract dry-run; it does not require built release + artifacts or `twine`. +- `make validate-built-artifacts` is explicit release-oriented physical artifact + validation. Complete artifact/sidecar pairs are validated, partial sets fail + closed, absent optional pairs are skipped, and a complete PyPI wheel/sdist set + invokes the final PyPI validator. +- `make validate-release-assets` verifies the exact final GitHub Release asset + set: 21 uploaded ECLI-owned assets, with checksum sidecars kept as + non-uploaded `.checksums/` evidence. + ## Optional Setup by Role - Release maintainer: install platform packaging dependencies. diff --git a/docs/release/release-process.md b/docs/release/release-process.md index 5e6a9d0..9393a81 100644 --- a/docs/release/release-process.md +++ b/docs/release/release-process.md @@ -90,23 +90,33 @@ contract drift. | `.github/workflows/windows-installer.yml` | Windows portable EXE and NSIS installer package path. | | `.github/workflows/windows-validate.yml` | Windows package validation. | -## Release Tooling Prerequisites +## Validation Tooling Prerequisites -Install release tooling before running Gate 2 validation from a clean -maintainer environment: +Gate 2 is source and structural validation. It must run from a clean source +checkout without built release artifacts and without final release-only tooling +such as `twine`: ```sh -python3 -m pip install -e ".[release]" +make validate-gate2 ``` The `validate-gate2` target runs source and structural contract validation -without inspecting pre-existing local release artifacts. Use -`make validate-built-artifacts` for explicit final artifact verification; that +without inspecting pre-existing local release artifacts. It covers version +consistency, runtime imports, Linux official evidence drift, and the PyPI source +contract dry-run. + +Install release tooling before running explicit final built-artifact validation: + +```sh +python3 -m pip install -e ".[release]" +``` + +Use `make validate-built-artifacts` for physical artifact verification; that target validates complete artifact/sidecar pairs and fails closed on partial -artifact sets. When a complete wheel/sdist set and adjacent sidecars are present, -it delegates to `validate-pypi-contract`, which requires `twine` for strict PyPI -wheel/sdist metadata validation. `twine` is declared only in release/development -tooling dependencies, not in ECLI runtime dependencies. +artifact sets. When a complete wheel/sdist set and adjacent sidecars are +present, it delegates to `validate-pypi-contract`, which requires `twine` for +strict PyPI wheel/sdist metadata validation. `twine` is declared only in +release/development tooling dependencies, not in ECLI runtime dependencies. The canonical `Release` workflow runs `make validate-built-artifacts` in the `validate-built-artifacts` job after downloading all build outputs, assembling diff --git a/tests/docs/test_release_gate_contract_alignment.py b/tests/docs/test_release_gate_contract_alignment.py new file mode 100644 index 0000000..14b70f2 --- /dev/null +++ b/tests/docs/test_release_gate_contract_alignment.py @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# Project: Ecli +# File: tests/docs/test_release_gate_contract_alignment.py +# Website: https://www.ecli.io +# Repository: https://github.com/SSobol77/ecli +# PyPI: https://pypi.org/project/ecli-editor/0.0.1/ +# +# Copyright (c) 2026 Siergej Sobolewski +# +# Licensed under the GNU General Public License version 2 only. +# See the LICENSE file in the project root for full license text. + +"""Current documentation contract for Gate 2 and release artifact validation.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + +CURRENT_GATE_DOCS = ( + "docs/contributor/development-setup.md", + "docs/release/release-process.md", + "docs/release/release-checklist.md", + "docs/release/artifact-verification.md", + "docs/release/artifact-contract.md", + "docs/release/packaging-flows.md", + "docs/release/build-matrix.md", + "docs/release/README-release.md", + "README.md", +) + +HISTORICAL_GATE_DOCS = ( + "audit-report.md", + "docs/planning/gate2-phase0-report.md", + "docs/planning/phase1-implementation-report.md", + "docs/planning/phase1-implementation-log.md", + "docs/planning/post-merge-defects.md", +) + + +def _read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def _combined(paths: tuple[str, ...]) -> str: + return "\n".join(_read(path) for path in paths) + + +def test_current_docs_define_three_separate_validation_layers() -> None: + text = " ".join(_combined(CURRENT_GATE_DOCS).split()) + + required = ( + "`make validate-gate2` is source and structural contract validation only", + "does not require built release artifacts or `twine`", + "`make validate-built-artifacts` is explicit release-oriented physical " + "artifact validation", + "partial sets fail closed", + "complete PyPI wheel/sdist set", + "`make validate-release-assets` verifies the exact final GitHub Release " + "asset set", + "21 uploaded ECLI-owned assets", + "non-uploaded `.checksums/` evidence", + ) + for marker in required: + assert marker in text + + +def test_twine_is_not_documented_as_source_only_gate2_dependency() -> None: + development_setup = _read("docs/contributor/development-setup.md") + release_process = _read("docs/release/release-process.md") + + assert "PyPI artifact validation in `make validate-gate2`" not in ( + development_setup + ) + assert "| `twine` | Release-only | Final PyPI built-artifact validation" in ( + development_setup + ) + assert "without built release artifacts and without final release-only tooling" in ( + release_process + ) + assert "such as `twine`" in release_process + + +def test_current_docs_do_not_make_gate2_the_final_artifact_verifier() -> None: + text = _combined(CURRENT_GATE_DOCS) + + forbidden = ( + "Gate 2 validation completed for built artifacts", + "PyPI artifact validation in `make validate-gate2`", + "`make validate-gate2` for explicit final artifact verification", + "`make validate-gate2` validates complete artifact/sidecar pairs", + "`make validate-gate2` delegates to `validate-pypi-contract`", + ) + for marker in forbidden: + assert marker not in text + + +def test_current_release_docs_preserve_asset_and_checksum_contract() -> None: + text = " ".join(_combined(CURRENT_GATE_DOCS).split()) + + required = ( + "exactly 21 ECLI-owned physical GitHub Release assets", + "Source code (zip)", + "Source code (tar.gz)", + "not part of the canonical 21 artifact contract entries", + "Checksum sidecars are mandatory CI/release verification evidence", + "not uploaded as separate GitHub Release assets", + "releases//.checksums/", + ) + for marker in required: + assert marker in text + + +def test_historical_gate2_reports_are_classified_as_history() -> None: + text = _combined(HISTORICAL_GATE_DOCS) + + assert "Gate 2 Phase 0 Report" in text + assert "Gate 2 Phase 1 Implementation Report" in text + assert "Gate 2 validation completed for built artifacts" in text diff --git a/tests/packaging/test_release_asset_count_gate.py b/tests/packaging/test_release_asset_count_gate.py index b630d87..65866c1 100644 --- a/tests/packaging/test_release_asset_count_gate.py +++ b/tests/packaging/test_release_asset_count_gate.py @@ -430,6 +430,33 @@ def test_validate_gate2_separates_source_and_built_artifact_checks( assert 'if [ -d "$(RELEASE_DIR)" ]' in makefile +def test_make_help_distinguishes_release_validation_layers(repo_root: Path) -> None: + canonical_lines = ( + "make validate-gate2 Source and structural contract gate", + "make validate-built-artifacts Validate available built artifacts fail-closed", + "make validate-release-assets Verify exact final GitHub Release asset set", + ) + descriptions = ( + "Source and structural contract gate", + "Validate available built artifacts fail-closed", + "Verify exact final GitHub Release asset set", + ) + assert len(set(descriptions)) == len(descriptions) + + for target in ("help", "help-full"): + result = subprocess.run( + ["make", target], + cwd=repo_root, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0 + for entry in canonical_lines: + assert result.stdout.count(entry) == 1 + + def test_validate_gate2_requires_complete_artifact_sidecar_pairs( read_repo_text: RepoReader, ) -> None: