From 0ff9661f67625fbee9e545ee75b4366be8b46ba1 Mon Sep 17 00:00:00 2001 From: Coding-Dev-Tools Date: Sun, 27 Sep 2026 10:58:58 -0400 Subject: [PATCH] chore(release): authorize retained 1.7.8 publication --- .github/workflows/release.yml | 37 ++++++--- docs/RELEASE_QUALIFICATION.md | 37 ++++++--- docs/RELEASE_READINESS.md | 6 +- tests/test_release_qualification.py | 121 ++++++++++++++++++++++------ 4 files changed, 152 insertions(+), 49 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fdeaf480..6aee6ebc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,11 @@ on: required: false type: boolean default: false + waive_v178_qualification: + description: "Owner-authorized qualification waiver for the retained v1.7.8 candidate only" + required: false + type: boolean + default: false permissions: contents: read @@ -964,16 +969,21 @@ jobs: - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" - - name: Enforce and record the v1.7.6-only qualification waiver - if: inputs.waive_v176_qualification + - name: Enforce and record the release-specific qualification waiver + if: inputs.waive_v176_qualification || inputs.waive_v178_qualification env: RELEASE_TAG: ${{ inputs.release_tag }} + WAIVE_V176: ${{ inputs.waive_v176_qualification }} + WAIVE_V178: ${{ inputs.waive_v178_qualification }} GH_ACTOR: ${{ github.actor }} GH_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} shell: bash run: | set -euo pipefail - test "$RELEASE_TAG" = "v1.7.6" + case "$WAIVE_V176:$WAIVE_V178:$RELEASE_TAG" in + true:false:v1.7.6|false:true:v1.7.8) ;; + *) printf 'Waiver must select exactly one authorized release.\n' >&2; exit 1 ;; + esac { printf '# Release qualification waiver\n\n' printf 'Tag: `%s`\n' "$RELEASE_TAG" @@ -1125,7 +1135,7 @@ jobs: cp dist/*.whl dist/*.tar.gz verified-dist/ - name: Require signed full-product qualification before PyPI repair - if: ${{ !inputs.waive_v176_qualification }} + if: ${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification }} env: RELEASE_TAG: ${{ inputs.release_tag }} ENGRAPHIS_RELEASE_QUALIFICATION: ${{ secrets.ENGRAPHIS_RELEASE_QUALIFICATION }} @@ -1140,8 +1150,10 @@ jobs: --commit "$ENGRAPHIS_REPAIR_COMMIT" --tag "$RELEASE_TAG" - name: Disclose the qualification waiver before PyPI repair - if: inputs.waive_v176_qualification + if: inputs.waive_v176_qualification || inputs.waive_v178_qualification env: + WAIVE_V176: ${{ inputs.waive_v176_qualification }} + WAIVE_V178: ${{ inputs.waive_v178_qualification }} GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ inputs.release_tag }} @@ -1149,9 +1161,12 @@ jobs: shell: bash run: | set -euo pipefail - test "$RELEASE_TAG" = "v1.7.6" - # The exception covers this retained candidate, not future reuse of its tag. - test "$ENGRAPHIS_REPAIR_COMMIT" = "6a441a75c8dd159607fa3933da83f600864b9146" + # 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) ;; + *) printf 'Waiver does not match an authorized source candidate.\n' >&2; exit 1 ;; + esac { printf '## Release qualification\n\n' printf 'The owner waived full-product qualification for this release. Mandatory full-product gates are not represented as passed.\n\n' @@ -1175,6 +1190,8 @@ jobs: --generate-notes --notes-file "$RUNNER_TEMP/release-waiver.md" \ --title "Engraphis ${RELEASE_TAG#v}" --latest=false fi + # Editing notes on an existing draft does not make the notice public. + test "$(gh release view "$RELEASE_TAG" --repo "$GH_REPO" --json isDraft --jq .isDraft)" = "false" - name: Publish only missing verified distributions uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 @@ -1190,7 +1207,7 @@ jobs: --version "${RELEASE_TAG#v}" --retries 18 --delay 10 - name: Require signed full-product qualification before GitHub repair - if: ${{ !inputs.waive_v176_qualification }} + if: ${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification }} env: RELEASE_TAG: ${{ inputs.release_tag }} ENGRAPHIS_RELEASE_QUALIFICATION: ${{ secrets.ENGRAPHIS_RELEASE_QUALIFICATION }} @@ -1208,7 +1225,7 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ inputs.release_tag }} - WAIVE_QUALIFICATION: ${{ inputs.waive_v176_qualification }} + WAIVE_QUALIFICATION: ${{ inputs.waive_v176_qualification || inputs.waive_v178_qualification }} GH_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} shell: bash run: | diff --git a/docs/RELEASE_QUALIFICATION.md b/docs/RELEASE_QUALIFICATION.md index 41b3375e..37eb16d9 100644 --- a/docs/RELEASE_QUALIFICATION.md +++ b/docs/RELEASE_QUALIFICATION.md @@ -4,8 +4,8 @@ Every ordinary PyPI publication, GitHub release write, and repair requires a val 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 v1.7.6 -repair waiver is the one-time exception documented below. +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. The public verifier is `scripts/verify_release_qualification.py`. It verifies Ed25519 signatures using `cryptography==50.0.0` in release jobs. It contains no @@ -107,16 +107,31 @@ a matching historical push run and verifies its exact public distribution/eviden hashes, then checks the current owner approval before both repair writes. It uses the peeled release tag commit, never the repair workflow's `main` checkout commit. -For the existing `v1.7.6` release only, the repository owner explicitly directed a -qualification waiver on 2026-09-27. The `workflow_dispatch` input -`waive_v176_qualification` skips the owner qualification verifier only when repairing -`v1.7.6` at commit `6a441a75c8dd159607fa3933da83f600864b9146`. Reusing that -tag for another commit cannot use this exception. The workflow records the actor +On 2026-09-27, the repository owner explicitly authorized qualification waivers +for these retained release candidates: + +| Dispatch input | Release tag | Required source commit | +| --- | --- | --- | +| `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 +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. + +The protected environment approval, exact distribution/evidence verification, +and PyPI file identity checks remain required. The workflow records the actor and run URL, and publishes the waiver in GitHub Release notes before the first -PyPI write. A failed disclosure prevents publication; a later repair failure -leaves the public disclosure in place. This is not a qualification and -does not mark any unverified gate as passing. All ordinary tag publications and -repairs for other versions still require a valid owner-signed qualification. +PyPI write. An existing draft must already be public before publication can +proceed; a successful notes edit alone is insufficient. A failed disclosure +prevents publication; a later repair failure +leaves the public disclosure in place. These waivers are not qualifications and +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. ## Public installed evidence diff --git a/docs/RELEASE_READINESS.md b/docs/RELEASE_READINESS.md index 152acfa5..91ce8656 100644 --- a/docs/RELEASE_READINESS.md +++ b/docs/RELEASE_READINESS.md @@ -92,9 +92,9 @@ engine checkout; `--require-leadership` requires both decisions. All modes retai Actual publication additionally requires the protected, owner-signed approval in [RELEASE_QUALIFICATION.md](RELEASE_QUALIFICATION.md), verified immediately before each normal or repair write. Its environment, authority and approval remain owner setup; -this source change does not configure or issue them. The owner-authorized v1.7.6 -repair waiver documented there is an explicit exception and does not establish -full-product readiness or change any gate status. +this source change does not configure or issue them. The owner-authorized repair +waivers for the exact v1.7.6 and v1.7.8 candidates documented there are explicit +exceptions and do not establish full-product readiness or change any gate status. Planner experiments remain off by default. To require their existing optimization gate: diff --git a/tests/test_release_qualification.py b/tests/test_release_qualification.py index dd995ace..48ca3d40 100644 --- a/tests/test_release_qualification.py +++ b/tests/test_release_qualification.py @@ -176,14 +176,16 @@ def test_cli_requires_configuration_and_never_prints_receipt(qualification, monk assert configuration["ENGRAPHIS_RELEASE_QUALIFICATION"] not in output.out + output.err -def test_publication_writes_require_qualification_except_scoped_v176_waiver(): +def test_publication_writes_require_qualification_except_scoped_release_waivers(): yaml = pytest.importorskip("yaml") 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_input = dispatch.get("inputs", {}).get("waive_v176_qualification", {}) - assert waiver_input.get("type") == "boolean" - assert waiver_input.get("default") is False + waiver_condition = "inputs.waive_v176_qualification || inputs.waive_v178_qualification" + for input_name in ("waive_v176_qualification", "waive_v178_qualification"): + waiver_input = dispatch.get("inputs", {}).get(input_name, {}) + assert waiver_input.get("type") == "boolean" + assert waiver_input.get("default") is False for name, expected_writes in (("publish", 1), ("github-release", 1), ("github-release-repair", 2)): job = workflow["jobs"][name] assert job["environment"] == "release-qualification" @@ -194,14 +196,19 @@ def test_publication_writes_require_qualification_except_scoped_v176_waiver(): # This conditional write publishes only the exception notice. The # ordinary path still needs its signature before any distribution. assert name == "github-release-repair" - assert step.get("if") == "inputs.waive_v176_qualification" + assert step.get("if") == waiver_condition assert "verified-dist/*" not in step["run"] assert "release-evidence/*" not in step["run"] - assert 'test "$ENGRAPHIS_REPAIR_COMMIT" = "6a441a75c8dd159607fa3933da83f600864b9146"' in step["run"] + assert "true:false:v1.7.6:6a441a75c8dd159607fa3933da83f600864b9146" in step["run"] + assert "false:true:v1.7.8:dce68e1602e580cd51b71e26db2ab04238df7df4" in step["run"] + assert step["env"]["WAIVE_V176"] == "${{ inputs.waive_v176_qualification }}" + assert step["env"]["WAIVE_V178"] == "${{ inputs.waive_v178_qualification }}" continue if "scripts.verify_release_qualification" in step.get("run", ""): if name == "github-release-repair": - assert step.get("if") == "${{ !inputs.waive_v176_qualification }}" + assert step.get("if") == ( + "${{ !inputs.waive_v176_qualification && !inputs.waive_v178_qualification }}" + ) else: assert "if" not in step assert not step.get("continue-on-error", False) @@ -228,16 +235,19 @@ def test_publication_writes_require_qualification_except_scoped_v176_waiver(): assert workflow["jobs"]["github-release"]["needs"] == "publish" repair_steps = workflow["jobs"]["github-release-repair"]["steps"] waiver_guard = next(step for step in repair_steps - if step.get("name") == "Enforce and record the v1.7.6-only qualification waiver") - assert waiver_guard.get("if") == "inputs.waive_v176_qualification" - assert 'test "$RELEASE_TAG" = "v1.7.6"' in waiver_guard["run"] + 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 waiver_guard["env"]["WAIVE_V176"] == "${{ inputs.waive_v176_qualification }}" + assert waiver_guard["env"]["WAIVE_V178"] == "${{ inputs.waive_v178_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 if step.get("name") == "Publish only missing verified distributions") assert repair_steps.index(disclosure) < repair_steps.index(publication) + assert '--json isDraft --jq .isDraft)" = "false"' in disclosure["run"] repair = next(step for step in repair_steps if step.get("name") == "Repair GitHub Release") - assert repair["env"]["WAIVE_QUALIFICATION"] == "${{ inputs.waive_v176_qualification }}" + assert repair["env"]["WAIVE_QUALIFICATION"] == "${{ " + waiver_condition + " }}" assert repair["run"].index("gh release edit") < repair["run"].index("gh release upload") assert '"${notes_args[@]}"' in repair["run"].split("gh release create", 1)[1] assert "${{ vars.ENGRAPHIS_RELEASE_" not in ( @@ -247,7 +257,8 @@ def test_publication_writes_require_qualification_except_scoped_v176_waiver(): @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)]) -def test_waiver_disclosure_cannot_follow_github_publication(tmp_path, existing, edit_fails): +@pytest.mark.parametrize("tag", ["v1.7.6", "v1.7.8"]) +def test_waiver_disclosure_cannot_follow_github_publication(tmp_path, existing, edit_fails, tag): yaml = pytest.importorskip("yaml") bash = shutil.which("bash") if bash is None: @@ -276,7 +287,7 @@ def test_waiver_disclosure_cannot_follow_github_publication(tmp_path, existing, calls_path = tmp_path / "calls.txt" result = subprocess.run([bash, str(script)], cwd=tmp_path, capture_output=True, text=True, timeout=20, env={**os.environ, "PATH": str(tmp_path) + os.pathsep + os.environ["PATH"], - "RUNNER_TEMP": str(tmp_path), "RELEASE_TAG": "v1.7.6", + "RUNNER_TEMP": str(tmp_path), "RELEASE_TAG": tag, "WAIVE_QUALIFICATION": "true", "GH_REPO": "test/repo", "GH_RUN_URL": "https://example.test/run/1", "GH_CALLS": str(calls_path), "EXISTING": str(existing).lower(), "EDIT_FAILS": str(edit_fails).lower()}) @@ -300,8 +311,16 @@ def test_waiver_disclosure_cannot_follow_github_publication(tmp_path, existing, @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)]) -def test_public_waiver_notice_precedes_pypi_even_if_later_repair_fails(tmp_path, existing, edit_fails): +@pytest.mark.parametrize("existing,edit_fails,draft", [ + (False, False, False), (True, False, False), (True, True, False), (True, False, True), +]) +@pytest.mark.parametrize("tag,commit", [ + ("v1.7.6", "6a441a75c8dd159607fa3933da83f600864b9146"), + ("v1.7.8", "dce68e1602e580cd51b71e26db2ab04238df7df4"), +]) +def test_public_waiver_notice_precedes_pypi_even_if_later_repair_fails( + tmp_path, existing, edit_fails, draft, tag, commit, +): yaml = pytest.importorskip("yaml") bash = shutil.which("bash") if bash is None: @@ -316,8 +335,12 @@ def test_public_waiver_notice_precedes_pypi_even_if_later_repair_fails(tmp_path, printf '%s\\n' "$*" >> "$GH_CALLS" case "$2" in view) - if [ "$EXISTING" != true ]; then exit 1; fi - printf 'Existing release notes\\n' + if [[ "$*" == *"--json isDraft"* ]]; then + printf '%s\\n' "$DRAFT" + else + if [ "$EXISTING" != true ]; then exit 1; fi + printf 'Existing release notes\\n' + fi ;; edit) if [ "$EDIT_FAILS" = true ]; then exit 7; fi @@ -332,16 +355,19 @@ def test_public_waiver_notice_precedes_pypi_even_if_later_repair_fails(tmp_path, encoding="utf-8") calls_path = tmp_path / "calls.txt" environment = {**os.environ, "PATH": str(tmp_path) + os.pathsep + os.environ["PATH"], - "RUNNER_TEMP": str(tmp_path), "RELEASE_TAG": "v1.7.6", "GH_REPO": "test/repo", - "ENGRAPHIS_REPAIR_COMMIT": "6a441a75c8dd159607fa3933da83f600864b9146", + "RUNNER_TEMP": str(tmp_path), "RELEASE_TAG": tag, "GH_REPO": "test/repo", + "ENGRAPHIS_REPAIR_COMMIT": commit, + "WAIVE_V176": str(tag == "v1.7.6").lower(), + "WAIVE_V178": str(tag == "v1.7.8").lower(), "GH_RUN_URL": "https://example.test/run/1", "GH_CALLS": str(calls_path), - "EXISTING": str(existing).lower(), "EDIT_FAILS": str(edit_fails).lower()} + "EXISTING": str(existing).lower(), "EDIT_FAILS": str(edit_fails).lower(), + "DRAFT": str(draft).lower()} result = subprocess.run([bash, str(script)], cwd=tmp_path, capture_output=True, text=True, timeout=20, env=environment) calls = calls_path.read_text(encoding="utf-8").splitlines() - assert result.returncode == (7 if edit_fails else 9), result.stderr + assert result.returncode == (7 if edit_fails else 1 if draft else 9), result.stderr notice = next(index for index, call in enumerate(calls) if "--notes-file " in call) - if edit_fails: + if edit_fails or draft: assert "pypi-publication" not in calls else: assert notice < calls.index("pypi-publication") @@ -354,9 +380,18 @@ def test_public_waiver_notice_precedes_pypi_even_if_later_repair_fails(tmp_path, @pytest.mark.skipif(os.name == "nt", reason="release workflow executes in Linux bash") -@pytest.mark.parametrize("tag,commit", [("v1.7.7", "6a441a75c8dd159607fa3933da83f600864b9146"), - ("v1.7.6", "a" * 40), ("v1.7.6", "")]) -def test_waiver_rejects_a_different_retained_candidate_before_any_public_write(tmp_path, tag, commit): +@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"), +]) +def test_waiver_rejects_a_different_retained_candidate_before_any_public_write(tmp_path, tag, commit, v176, v178): yaml = pytest.importorskip("yaml") bash = shutil.which("bash") if bash is None: @@ -376,6 +411,42 @@ def test_waiver_rejects_a_different_retained_candidate_before_any_public_write(t timeout=20, env={**os.environ, "PATH": str(tmp_path) + os.pathsep + os.environ["PATH"], "RUNNER_TEMP": str(tmp_path), "RELEASE_TAG": tag, "ENGRAPHIS_REPAIR_COMMIT": commit, "GH_REPO": "test/repo", + "WAIVE_V176": v176, "WAIVE_V178": v178, "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), +]) +def test_waiver_input_guard_rejects_ambiguous_or_unapproved_requests(tmp_path, tag, v176, v178, allowed): + yaml = pytest.importorskip("yaml") + bash = shutil.which("bash") + if bash is None: + pytest.skip("bash is unavailable") + root = Path(__file__).resolve().parents[1] + workflow = yaml.safe_load((root / ".github/workflows/release.yml").read_text(encoding="utf-8")) + guard = next(step for step in workflow["jobs"]["github-release-repair"]["steps"] + if step.get("name") == "Enforce and record the release-specific qualification waiver") + script = tmp_path / "guard.sh" + script.write_text(guard["run"], encoding="utf-8") + summary = tmp_path / "summary.md" + 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, + "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 + assert summary.exists() is allowed + if allowed: + assert f"Tag: `{tag}`" in summary.read_text(encoding="utf-8") + assert "Triggered by: `test-actor`" in summary.read_text(encoding="utf-8")