Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down
16 changes: 15 additions & 1 deletion docs/contributor/development-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
30 changes: 20 additions & 10 deletions docs/release/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
122 changes: 122 additions & 0 deletions tests/docs/test_release_gate_contract_alignment.py
Original file line number Diff line number Diff line change
@@ -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/<version>/.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
27 changes: 27 additions & 0 deletions tests/packaging/test_release_asset_count_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading