diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de2d3af5..a05c8e8a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,11 @@ on: required: false type: boolean default: false + waive_v179_qualification: + description: "Owner-approved waiver for the retained v1.7.9 candidate at c6871b9b only" + required: false + type: boolean + default: false permissions: contents: read @@ -980,18 +985,19 @@ jobs: with: python-version: "3.11" - name: Enforce and record the release-specific qualification waiver - if: inputs.waive_v176_qualification || inputs.waive_v178_qualification + if: inputs.waive_v176_qualification || inputs.waive_v178_qualification || inputs.waive_v179_qualification env: RELEASE_TAG: ${{ inputs.release_tag }} WAIVE_V176: ${{ inputs.waive_v176_qualification }} WAIVE_V178: ${{ inputs.waive_v178_qualification }} + WAIVE_V179: ${{ inputs.waive_v179_qualification }} GH_ACTOR: ${{ github.actor }} GH_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} shell: bash run: | set -euo pipefail - case "$WAIVE_V176:$WAIVE_V178:$RELEASE_TAG" in - true:false:v1.7.6|false:true:v1.7.8) ;; + case "$WAIVE_V176:$WAIVE_V178:${WAIVE_V179:-false}:$RELEASE_TAG" in + true:false:false:v1.7.6|false:true:false:v1.7.8|false:false:true:v1.7.9) ;; *) printf 'Waiver must select exactly one authorized release.\n' >&2; exit 1 ;; esac { @@ -1145,7 +1151,7 @@ jobs: cp dist/*.whl dist/*.tar.gz verified-dist/ - name: Require signed full-product qualification before PyPI repair - if: ${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification }} + if: ${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification && !inputs.waive_v179_qualification }} env: RELEASE_TAG: ${{ inputs.release_tag }} ENGRAPHIS_RELEASE_QUALIFICATION: ${{ secrets.ENGRAPHIS_RELEASE_QUALIFICATION }} @@ -1160,10 +1166,11 @@ jobs: --commit "$ENGRAPHIS_REPAIR_COMMIT" --tag "$RELEASE_TAG" - name: Disclose the qualification waiver before PyPI repair - if: inputs.waive_v176_qualification || inputs.waive_v178_qualification + if: inputs.waive_v176_qualification || inputs.waive_v178_qualification || inputs.waive_v179_qualification env: WAIVE_V176: ${{ inputs.waive_v176_qualification }} WAIVE_V178: ${{ inputs.waive_v178_qualification }} + WAIVE_V179: ${{ inputs.waive_v179_qualification }} GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ inputs.release_tag }} @@ -1172,9 +1179,10 @@ jobs: run: | set -euo pipefail # Each exception covers one retained candidate, not future reuse of its tag. - case "$WAIVE_V176:$WAIVE_V178:$RELEASE_TAG:$ENGRAPHIS_REPAIR_COMMIT" in - true:false:v1.7.6:6a441a75c8dd159607fa3933da83f600864b9146|\ - false:true:v1.7.8:dce68e1602e580cd51b71e26db2ab04238df7df4) ;; + case "$WAIVE_V176:$WAIVE_V178:${WAIVE_V179:-false}:$RELEASE_TAG:$ENGRAPHIS_REPAIR_COMMIT" in + true:false:false:v1.7.6:6a441a75c8dd159607fa3933da83f600864b9146|\ + false:true:false:v1.7.8:dce68e1602e580cd51b71e26db2ab04238df7df4|\ + false:false:true:v1.7.9:c6871b9bf506eec6cebe96beee1ac252429a7b99) ;; *) printf 'Waiver does not match an authorized source candidate.\n' >&2; exit 1 ;; esac { @@ -1217,7 +1225,7 @@ jobs: --version "${RELEASE_TAG#v}" --retries 18 --delay 10 - name: Require signed full-product qualification before GitHub repair - if: ${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification }} + if: ${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification && !inputs.waive_v179_qualification }} env: RELEASE_TAG: ${{ inputs.release_tag }} ENGRAPHIS_RELEASE_QUALIFICATION: ${{ secrets.ENGRAPHIS_RELEASE_QUALIFICATION }} @@ -1235,7 +1243,7 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ inputs.release_tag }} - WAIVE_QUALIFICATION: ${{ inputs.waive_v176_qualification || inputs.waive_v178_qualification }} + WAIVE_QUALIFICATION: ${{ inputs.waive_v176_qualification || inputs.waive_v178_qualification || inputs.waive_v179_qualification }} GH_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} shell: bash run: | @@ -1245,7 +1253,7 @@ jobs: promote_latest=true if [ "$WAIVE_QUALIFICATION" = "true" ]; then # A retained older repair must not displace a newer public Latest. - # Both authorized candidates already have a public release history; + # The candidate comparison uses the existing public release history; # failed lookups or unknown tag formats stop before any release write. current_latest="$(gh release view --repo "$GH_REPO" --json tagName --jq .tagName)" promote_latest="$(python - "$RELEASE_TAG" "$current_latest" <<'PY' diff --git a/docs/RELEASE_QUALIFICATION.md b/docs/RELEASE_QUALIFICATION.md index 37eb16d9..3f5850e2 100644 --- a/docs/RELEASE_QUALIFICATION.md +++ b/docs/RELEASE_QUALIFICATION.md @@ -5,7 +5,7 @@ full-product qualification. Passing the public build jobs is necessary but does not replace the mandatory private readiness evidence. The workflow fails closed when qualification configuration is missing, malformed, expired or inconsistent with the selected source and distribution bytes. The owner-authorized repair -waivers for the retained v1.7.6 and v1.7.8 candidates are documented below. +waivers for the retained v1.7.6, v1.7.8 and v1.7.9 candidates are documented below. The public verifier is `scripts/verify_release_qualification.py`. It verifies Ed25519 signatures using `cryptography==50.0.0` in release jobs. It contains no @@ -115,9 +115,9 @@ for these retained release candidates: | `waive_v176_qualification` | `v1.7.6` | `6a441a75c8dd159607fa3933da83f600864b9146` | | `waive_v178_qualification` | `v1.7.8` | `dce68e1602e580cd51b71e26db2ab04238df7df4` | -Select exactly one waiver input together with its matching `release_tag`. Both -inputs default to false. Combining them, selecting the wrong version, or reusing -a tag for another commit fails before any public write. The v1.7.8 waiver follows +Select exactly one waiver input together with its matching `release_tag`. All +waiver inputs default to false. Combining them, selecting the wrong version, or +reusing a tag for another commit fails before any public write. The v1.7.8 waiver follows the owner's explicit instruction to remove publication blockers after integrating and reviewing the beneficial local work. It reuses the distributions and evidence from the successful automated validations of that tagged source. @@ -133,6 +133,28 @@ do not mark any unverified gate as passing. All ordinary tag publications and repairs outside these exact candidates still require a valid owner-signed qualification. +## Owner-authorized retained v1.7.9 repair + +On 2026-10-01, after reviewing the prepared exception and required public +disclosure, the repository owner explicitly approved this retained-candidate +repair and publication. This approval does not qualify the full product or +authorize exceptions for future candidates. + +The `waive_v179_qualification` input defaults to false and is bound only +to `v1.7.9` at `c6871b9bf506eec6cebe96beee1ac252429a7b99`. It cannot be combined +with either earlier waiver or applied to a different tag or source commit. +The retained tag run's automated validation jobs passed; its publisher failed +because the signed full-product qualification receipt was absent. The repair +reuses the exact retained wheel, source archive and public evidence from that run. + +This exception waives only the signed full-product qualification +requirement for those retained bytes. Protected environment review, retained +artifact verification and PyPI file identity checks remain required. Public +GitHub release disclosure must succeed before the first PyPI write and must +state that mandatory full-product acceptance gates remain unverified. Ordinary +tag publication and repair outside the explicitly selected exception still +require signed qualification. + ## Public installed evidence New release evidence requires all six installed surface cells: MCP and dashboard diff --git a/tests/test_release_qualification.py b/tests/test_release_qualification.py index 7224fab1..790d4c74 100644 --- a/tests/test_release_qualification.py +++ b/tests/test_release_qualification.py @@ -182,8 +182,11 @@ def test_publication_writes_require_qualification_except_scoped_release_waivers( root = Path(__file__).resolve().parents[1] workflow = yaml.safe_load((root / ".github/workflows/release.yml").read_text(encoding="utf-8")) dispatch = workflow.get("on", workflow.get(True, {})).get("workflow_dispatch", {}) - waiver_condition = "inputs.waive_v176_qualification || inputs.waive_v178_qualification" - for input_name in ("waive_v176_qualification", "waive_v178_qualification"): + waiver_condition = ( + "inputs.waive_v176_qualification || inputs.waive_v178_qualification" + " || inputs.waive_v179_qualification" + ) + for input_name in ("waive_v176_qualification", "waive_v178_qualification", "waive_v179_qualification"): waiver_input = dispatch.get("inputs", {}).get(input_name, {}) assert waiver_input.get("type") == "boolean" assert waiver_input.get("default") is False @@ -200,15 +203,18 @@ def test_publication_writes_require_qualification_except_scoped_release_waivers( assert step.get("if") == waiver_condition assert "verified-dist/*" not in step["run"] assert "release-evidence/*" not in step["run"] - assert "true:false:v1.7.6:6a441a75c8dd159607fa3933da83f600864b9146" in step["run"] - assert "false:true:v1.7.8:dce68e1602e580cd51b71e26db2ab04238df7df4" in step["run"] + assert "true:false:false:v1.7.6:6a441a75c8dd159607fa3933da83f600864b9146" in step["run"] + assert "false:true:false:v1.7.8:dce68e1602e580cd51b71e26db2ab04238df7df4" in step["run"] + assert "false:false:true:v1.7.9:c6871b9bf506eec6cebe96beee1ac252429a7b99" in step["run"] assert step["env"]["WAIVE_V176"] == "${{ inputs.waive_v176_qualification }}" assert step["env"]["WAIVE_V178"] == "${{ inputs.waive_v178_qualification }}" + assert step["env"]["WAIVE_V179"] == "${{ inputs.waive_v179_qualification }}" continue if "scripts.verify_release_qualification" in step.get("run", ""): if name == "github-release-repair": assert step.get("if") == ( - "${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification }}" + "${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification" + " && !inputs.waive_v179_qualification }}" ) else: assert "if" not in step @@ -238,9 +244,10 @@ def test_publication_writes_require_qualification_except_scoped_release_waivers( waiver_guard = next(step for step in repair_steps if step.get("name") == "Enforce and record the release-specific qualification waiver") assert waiver_guard.get("if") == waiver_condition - assert "true:false:v1.7.6|false:true:v1.7.8" in waiver_guard["run"] + assert "true:false:false:v1.7.6|false:true:false:v1.7.8|false:false:true:v1.7.9" in waiver_guard["run"] assert waiver_guard["env"]["WAIVE_V176"] == "${{ inputs.waive_v176_qualification }}" assert waiver_guard["env"]["WAIVE_V178"] == "${{ inputs.waive_v178_qualification }}" + assert waiver_guard["env"]["WAIVE_V179"] == "${{ inputs.waive_v179_qualification }}" disclosure = next(step for step in repair_steps if step.get("name") == "Disclose the qualification waiver before PyPI repair") publication = next(step for step in repair_steps @@ -258,7 +265,7 @@ def test_publication_writes_require_qualification_except_scoped_release_waivers( @pytest.mark.skipif(os.name == "nt", reason="release workflow executes in Linux bash") @pytest.mark.parametrize("existing,edit_fails", [(False, False), (True, False), (True, True)]) -@pytest.mark.parametrize("tag", ["v1.7.6", "v1.7.8"]) +@pytest.mark.parametrize("tag", ["v1.7.6", "v1.7.8", "v1.7.9"]) def test_waiver_disclosure_cannot_follow_github_publication(tmp_path, existing, edit_fails, tag): yaml = pytest.importorskip("yaml") bash = shutil.which("bash") @@ -412,6 +419,7 @@ def test_waiver_repair_preserves_newer_latest(tmp_path, latest, lookup_fails, ex @pytest.mark.parametrize("tag,commit", [ ("v1.7.6", "6a441a75c8dd159607fa3933da83f600864b9146"), ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4"), + ("v1.7.9", "c6871b9bf506eec6cebe96beee1ac252429a7b99"), ]) def test_public_waiver_notice_precedes_pypi_even_if_later_repair_fails( tmp_path, existing, edit_fails, draft, tag, commit, @@ -454,6 +462,7 @@ def test_public_waiver_notice_precedes_pypi_even_if_later_repair_fails( "ENGRAPHIS_REPAIR_COMMIT": commit, "WAIVE_V176": str(tag == "v1.7.6").lower(), "WAIVE_V178": str(tag == "v1.7.8").lower(), + "WAIVE_V179": str(tag == "v1.7.9").lower(), "GH_RUN_URL": "https://example.test/run/1", "GH_CALLS": str(calls_path), "EXISTING": str(existing).lower(), "EDIT_FAILS": str(edit_fails).lower(), "DRAFT": str(draft).lower()} @@ -475,18 +484,26 @@ def test_public_waiver_notice_precedes_pypi_even_if_later_repair_fails( @pytest.mark.skipif(os.name == "nt", reason="release workflow executes in Linux bash") -@pytest.mark.parametrize("tag,commit,v176,v178", [ - ("v1.7.7", "6a441a75c8dd159607fa3933da83f600864b9146", "true", "false"), - ("v1.7.6", "a" * 40, "true", "false"), - ("v1.7.6", "", "true", "false"), - ("v1.7.8", "a" * 40, "false", "true"), - ("v1.7.8", "", "false", "true"), - ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4", "true", "false"), - ("v1.7.6", "6a441a75c8dd159607fa3933da83f600864b9146", "false", "true"), - ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4", "true", "true"), - ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4", "false", "false"), +@pytest.mark.parametrize("tag,commit,v176,v178,v179", [ + ("v1.7.7", "6a441a75c8dd159607fa3933da83f600864b9146", "true", "false", "false"), + ("v1.7.6", "a" * 40, "true", "false", "false"), + ("v1.7.6", "", "true", "false", "false"), + ("v1.7.8", "a" * 40, "false", "true", "false"), + ("v1.7.8", "", "false", "true", "false"), + ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4", "true", "false", "false"), + ("v1.7.6", "6a441a75c8dd159607fa3933da83f600864b9146", "false", "true", "false"), + ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4", "true", "true", "false"), + ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4", "false", "false", "false"), + ("v1.7.9", "a" * 40, "false", "false", "true"), + ("v1.7.9", "", "false", "false", "true"), + ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4", "false", "false", "true"), + ("v1.7.9", "c6871b9bf506eec6cebe96beee1ac252429a7b99", "false", "true", "true"), + ("v1.7.9", "c6871b9bf506eec6cebe96beee1ac252429a7b99", "true", "false", "true"), + ("v1.7.9", "c6871b9bf506eec6cebe96beee1ac252429a7b99", "false", "false", "false"), ]) -def test_waiver_rejects_a_different_retained_candidate_before_any_public_write(tmp_path, tag, commit, v176, v178): +def test_waiver_rejects_a_different_retained_candidate_before_any_public_write( + tmp_path, tag, commit, v176, v178, v179, +): yaml = pytest.importorskip("yaml") bash = shutil.which("bash") if bash is None: @@ -507,23 +524,30 @@ def test_waiver_rejects_a_different_retained_candidate_before_any_public_write(t "RUNNER_TEMP": str(tmp_path), "RELEASE_TAG": tag, "ENGRAPHIS_REPAIR_COMMIT": commit, "GH_REPO": "test/repo", "WAIVE_V176": v176, "WAIVE_V178": v178, + "WAIVE_V179": v179, "GH_CALLS": str(calls_path)}) assert result.returncode != 0 assert not calls_path.exists() @pytest.mark.skipif(os.name == "nt", reason="release workflow executes in Linux bash") -@pytest.mark.parametrize("tag,v176,v178,allowed", [ - ("v1.7.6", "true", "false", True), - ("v1.7.8", "false", "true", True), - ("v1.7.6", "true", "true", False), - ("v1.7.8", "true", "true", False), - ("v1.7.8", "true", "false", False), - ("v1.7.6", "false", "true", False), - ("v1.7.9", "false", "true", False), - ("v1.7.8", "false", "false", False), +@pytest.mark.parametrize("tag,v176,v178,v179,allowed", [ + ("v1.7.6", "true", "false", "false", True), + ("v1.7.8", "false", "true", "false", True), + ("v1.7.9", "false", "false", "true", True), + ("v1.7.6", "true", "true", "false", False), + ("v1.7.8", "true", "true", "false", False), + ("v1.7.8", "true", "false", "false", False), + ("v1.7.6", "false", "true", "false", False), + ("v1.7.9", "false", "true", "false", False), + ("v1.7.8", "false", "false", "false", False), + ("v1.7.9", "false", "false", "false", False), + ("v1.7.9", "true", "false", "true", False), + ("v1.7.9", "false", "true", "true", False), + ("v1.7.9", "true", "true", "true", False), + ("v1.7.8", "false", "false", "true", False), ]) -def test_waiver_input_guard_rejects_ambiguous_or_unapproved_requests(tmp_path, tag, v176, v178, allowed): +def test_waiver_input_guard_rejects_ambiguous_or_unapproved_requests(tmp_path, tag, v176, v178, v179, allowed): yaml = pytest.importorskip("yaml") bash = shutil.which("bash") if bash is None: @@ -538,6 +562,7 @@ def test_waiver_input_guard_rejects_ambiguous_or_unapproved_requests(tmp_path, t result = subprocess.run([bash, str(script)], cwd=tmp_path, capture_output=True, text=True, timeout=20, env={**os.environ, "RELEASE_TAG": tag, "WAIVE_V176": v176, "WAIVE_V178": v178, + "WAIVE_V179": v179, "GH_ACTOR": "test-actor", "GH_RUN_URL": "https://example.test/run/1", "GITHUB_STEP_SUMMARY": str(summary)}) assert (result.returncode == 0) is allowed, result.stderr