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
13 changes: 11 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ jobs:
uv run python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)"
uv run python -m build --no-isolation
- run: uv run twine check dist/*
- name: Export hash-locked runtime dependencies
run: >-
uv export --frozen --no-dev --no-emit-project --format requirements-txt
--output-file runtime-requirements.txt
- name: Verify exact artifact version
env:
VERSION: ${{ steps.validate.outputs.version }}
Expand Down Expand Up @@ -313,6 +317,7 @@ jobs:
dist/
artifact-hashes.json
SHA256SUMS
runtime-requirements.txt
if-no-files-found: error
retention-days: 7

Expand Down Expand Up @@ -365,7 +370,9 @@ jobs:
- name: Verify and smoke exact TestPyPI artifacts
env:
VERSION: ${{ needs.build.outputs.version }}
run: ./scripts/verify_registry_artifacts.sh testpypi "${VERSION}" artifact-hashes.json release.yml
run: >-
./scripts/verify_registry_artifacts.sh testpypi "${VERSION}"
artifact-hashes.json release.yml runtime-requirements.txt

publish-pypi:
name: publish-pypi
Expand Down Expand Up @@ -415,7 +422,9 @@ jobs:
- name: Verify and smoke exact PyPI artifacts
env:
VERSION: ${{ needs.build.outputs.version }}
run: ./scripts/verify_registry_artifacts.sh pypi "${VERSION}" artifact-hashes.json release.yml
run: >-
./scripts/verify_registry_artifacts.sh pypi "${VERSION}"
artifact-hashes.json release.yml runtime-requirements.txt

github-release:
name: publish-github-release
Expand Down
11 changes: 7 additions & 4 deletions .github/workflows/testpypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ jobs:
uv run python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)"
uv run python -m build --no-isolation
uv run twine check dist/*
uv export --frozen --no-dev --no-emit-project --format requirements-txt --output-file /tmp/runtime-requirements.txt
uv export --frozen --no-dev --no-emit-project --format requirements-txt --output-file runtime-requirements.txt
python -m venv /tmp/wheel-smoke
/tmp/wheel-smoke/bin/pip install --require-hashes --requirement /tmp/runtime-requirements.txt
/tmp/wheel-smoke/bin/pip install --require-hashes --requirement runtime-requirements.txt
python scripts/install_local_artifact.py \
--python /tmp/wheel-smoke/bin/python \
--artifact "$(find dist -maxdepth 1 -name '*.whl' -print -quit)" \
--requirements /tmp/wheel-smoke-requirements.txt
/tmp/wheel-smoke/bin/ocr-ci --help
python -m venv /tmp/sdist-smoke
/tmp/sdist-smoke/bin/pip install --require-hashes --requirement /tmp/runtime-requirements.txt
/tmp/sdist-smoke/bin/pip install --require-hashes --requirement runtime-requirements.txt
python scripts/install_local_artifact.py \
--python /tmp/sdist-smoke/bin/python \
--artifact "$(find dist -maxdepth 1 -name '*.tar.gz' -print -quit)" \
Expand Down Expand Up @@ -106,6 +106,7 @@ jobs:
path: |
dist
artifact-hashes.json
runtime-requirements.txt
if-no-files-found: error
retention-days: 7
overwrite: true
Expand Down Expand Up @@ -151,4 +152,6 @@ jobs:
- name: Verify provenance and install immutable published artifacts
env:
VERSION: ${{ needs.build.outputs.version }}
run: ./scripts/verify_registry_artifacts.sh testpypi "${VERSION}" artifact-hashes.json testpypi.yml
run: >-
./scripts/verify_registry_artifacts.sh testpypi "${VERSION}"
artifact-hashes.json testpypi.yml runtime-requirements.txt
44 changes: 44 additions & 0 deletions PLANS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,50 @@ before handoff or commit. Completed stable plans are indexed in

## Active Work

## Registry artifact dependency verification repair

Status: complete; corrective PR handoff authorized
Plan Origin: execution-discovered
Release classification: release-required
Target stable version: 0.11.0

### Goal

Repair the immutable registry verifier so published wheel and sdist smoke tests
install the hash-locked runtime dependency set before invoking `ocr-ci`. This
restores the TestPyPI development gate and the identical stable PyPI/TestPyPI
verification boundary without changing runtime behavior or release scope.

### Evidence And Scope

- Main workflow `35442945566` built and published `0.11.0.dev97`, verified its
bytes and provenance, then failed both artifact smokes because the verifier
installed distributions with `--no-deps` into empty environments.
- The build-stage smoke already exports and installs the locked runtime closure;
only the post-publication verifier omitted that input.
- Add one explicit verifier argument for a generated hash-locked requirements
file, pass it from development and stable workflows, and lock the contract in
workflow/verifier tests. No dependency, federation, DLP, receipt or release
authorization semantics change.

### Acceptance And Closure

- Focused verifier/workflow tests and `git diff --check` pass.
- Full repository quality and hosted exact-head checks pass before protected merge.
- The feature branch is pushed only after the local commit is complete; the fix PR
uses protected merge and a subsequent main development workflow must complete.
- Archive this short corrective plan into the v0.11.0 release history and remove it
from active work in the release-preparation commit.

### Completion Evidence

The verifier now requires a checked-in-workflow-generated, hash-locked runtime
requirements artifact and installs it into both fresh registry smoke environments
before installing the immutable wheel or sdist without dependency resolution. The
development and stable workflows transfer that exact requirements file alongside
the reviewed distributions; focused workflow/verifier tests pass. Hosted exact-head
checks and the repaired main development publication remain the protected PR gates.

## v0.11.0 governed MCP federation and feature draft

Status: complete; release handoff authorized
Expand Down
8 changes: 8 additions & 0 deletions scripts/verify_registry_artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ registry=${1:?registry is required}
version=${2:?version is required}
hashes=${3:?hash file is required}
workflow=${4:?expected publisher workflow is required}
runtime_requirements=${5:?hash-locked runtime requirements are required}

test -f "${runtime_requirements}" || {
echo "runtime requirements not found: ${runtime_requirements}" >&2
exit 2
}

case "${workflow}" in
testpypi.yml|release.yml) ;;
Expand Down Expand Up @@ -124,9 +130,11 @@ while IFS="${tab}" read -r provenance_url filename; do
done < "${provenance_downloads}"

python -m venv "${wheel_environment}"
"${wheel_environment}/bin/pip" install --require-hashes --requirement "${runtime_requirements}"
"${wheel_environment}/bin/pip" install --no-deps "${destination}"/*.whl
"${wheel_environment}/bin/ocr-ci" --help
python -m venv "${sdist_environment}"
"${sdist_environment}/bin/pip" install --require-hashes --requirement "${runtime_requirements}"
python scripts/install_local_artifact.py \
--python "${sdist_environment}/bin/python" \
--artifact "$(find "${destination}" -maxdepth 1 -name '*.tar.gz' -print -quit)" \
Expand Down
1 change: 1 addition & 0 deletions tests/test_release_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ def test_release_workflow_builds_reads_back_and_recovers_the_receipt() -> None:
assert "verify_registry_provenance.py" in (
ROOT / "scripts" / "verify_registry_artifacts.sh"
).read_text(encoding="utf-8")
assert "runtime-requirements.txt" in workflow
assert "python scripts/release_receipt.py" in workflow
assert "python scripts/github_release_api.py ensure" in workflow
assert "python scripts/github_release_api.py upload" in workflow
Expand Down
6 changes: 6 additions & 0 deletions tests/test_testpypi_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def test_workflow_automates_one_idempotent_development_build_per_main_run() -> N
assert "${GITHUB_RUN_NUMBER}" in workflow
assert "development-version" in workflow
assert workflow.count("testpypi-development-distributions") == 3
assert "runtime-requirements.txt" in workflow
assert "overwrite: true" in workflow
assert "needs.build.outputs.publish == 'true'" in workflow
assert "needs.publish.result == 'skipped'" in workflow
Expand All @@ -195,6 +196,7 @@ def test_workflow_bounds_and_verifies_every_testpypi_download() -> None:
assert workflow.count("--proto '=https' --proto-redir '=https'") == 1
assert "verify_registry_artifacts.sh testpypi" in workflow
assert "artifact-hashes.json testpypi.yml" in workflow
assert "testpypi.yml runtime-requirements.txt" in workflow
assert "python -m build --no-isolation" in workflow


Expand Down Expand Up @@ -228,6 +230,7 @@ def test_production_release_verifies_reviewed_registry_artifacts() -> None:
assert "attestations: true" in workflow
assert workflow.count("verify_registry_artifacts.sh") == 2
assert workflow.count("artifact-hashes.json release.yml") == 2
assert workflow.count("release.yml runtime-requirements.txt") == 2
assert workflow.count('python: ["3.12", "3.13", "3.14"]') == 2
assert "python scripts/github_release_api.py ensure" in workflow
assert "python scripts/github_release_api.py upload" in workflow
Expand All @@ -243,6 +246,9 @@ def test_production_release_verifies_reviewed_registry_artifacts() -> None:
assert "--max-filesize 1048576" in verifier
assert "--proto '=https' --proto-redir '=https'" in verifier
assert "sha256sum --check --strict" in verifier
assert (
verifier.count('pip" install --require-hashes --requirement "${runtime_requirements}"') == 2
)
assert "verify_registry_provenance.py" in verifier
assert '--workflow "${workflow}"' in verifier
assert "application/vnd.pypi.integrity.v1+json" in verifier
Expand Down
Loading