From f3cf9eea68c8ce33a6680c76001be7a76eb367d5 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:10:07 +0900 Subject: [PATCH 01/28] chore: stage release gate implementation --- .github/step1-changes.tar.gz | Bin 0 -> 15008 bytes .../workflows/apply-release-gates-sdist.yml | 57 ++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 .github/step1-changes.tar.gz create mode 100644 .github/workflows/apply-release-gates-sdist.yml diff --git a/.github/step1-changes.tar.gz b/.github/step1-changes.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7f25fc1ca90c1b9677a3b73cc2a6de444092d5d3 GIT binary patch literal 15008 zcmV;RI$y;fiwFQwaf)gJ|LuKycN5pWX#e)7n5*8kYUfC@B)`D-xT^-7gwp^%w$q$m zmnDy-v8^CWszx%ngtaU?BoIuXX#ychLr4O4La;;gr~@f{mun=OZ*hN*z4z=r8p$@V zCatv+EY0lq{=FZ2M<+9-sq%RH@qF>vL^gliZBJ#Qv(wpMJi$*4KHZ(2^cOz9zwxeE zJRzU)^LV1OtLGP1>=)1AQ+7*s5xW1yzvm}sPdmLWH%xNat&bFoM&pDQ`JlG|$)9Xs990>4w`V$sQ^X7Tl8 zF<&n9TL1gxNsD1FD%-S9or-9Wof&8)st+KVD^HI*MeDWKWSOEf0kyDGJC|~@kxVX9 zDCQ@NjtkwFie;yz<<D+Zpl- zW^$=)Iqi6TT2_SmHM-T_*0+sKc7$5QM4f^w8;gkb-aqA}j#;oSmYXjZQ;uq_?BW{P zsS<)vdkTv|so!*@J5rqqr^~ieUE}tiSl6CJSK99B?u^G$@jdZGPsg~uH#Q!(ere70 zL}SsoYVbHfhdz}8j?pbeQbi{XK$U^DmMiWX;PAWVjZM!MW-Ygvs;duphpS$ia!Q$$HIX?{Di;Gb#=nACE%HZy zYx~NXEbIj!LoqX6CWNbNX&lR7#p6>B&{QYk=u}f+2<#}m{<%`fT{86y$rN*)e>Op4 z*k`E4Rpae7kfAXy1W>k&!US4iYBQZr0fjP~`E^8_`SxfX4KgV@Kq{xP

TK{wdPP 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 Date: Sun, 23 Aug 2026 13:11:53 +0900 Subject: [PATCH 02/28] chore: trigger release gate application on PR --- .github/workflows/apply-release-gates-sdist.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml index d351632..8454390 100644 --- a/.github/workflows/apply-release-gates-sdist.yml +++ b/.github/workflows/apply-release-gates-sdist.yml @@ -4,6 +4,9 @@ on: push: branches: - ci/release-gates-sdist + pull_request: + branches: + - main permissions: contents: write From a69be6b6387071b81aac988c17207345fc3ce86d Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:23:34 +0900 Subject: [PATCH 03/28] chore: restage release gate implementation safely --- .github/step1-changes.tar.gz | Bin 15008 -> 0 bytes .github/step1-chunks/part-00.txt | 1 + .github/step1-chunks/part-01.txt | 1 + .github/step1-chunks/part-02.txt | 1 + .github/step1-chunks/part-03.txt | 1 + .github/step1-chunks/part-04.txt | 1 + .github/step1-chunks/part-05.txt | 1 + .github/step1-chunks/part-06.txt | 1 + .github/step1-chunks/part-07.txt | 1 + .github/step1-chunks/part-08.txt | 1 + .github/workflows/apply-release-gates-sdist.yml | 14 ++++++++------ 11 files changed, 17 insertions(+), 6 deletions(-) delete mode 100644 .github/step1-changes.tar.gz create mode 100644 .github/step1-chunks/part-00.txt create mode 100644 .github/step1-chunks/part-01.txt create mode 100644 .github/step1-chunks/part-02.txt create mode 100644 .github/step1-chunks/part-03.txt create mode 100644 .github/step1-chunks/part-04.txt create mode 100644 .github/step1-chunks/part-05.txt create mode 100644 .github/step1-chunks/part-06.txt create mode 100644 .github/step1-chunks/part-07.txt create mode 100644 .github/step1-chunks/part-08.txt diff --git a/.github/step1-changes.tar.gz b/.github/step1-changes.tar.gz deleted file mode 100644 index 7f25fc1ca90c1b9677a3b73cc2a6de444092d5d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15008 zcmV;RI$y;fiwFQwaf)gJ|LuKycN5pWX#e)7n5*8kYUfC@B)`D-xT^-7gwp^%w$q$m zmnDy-v8^CWszx%ngtaU?BoIuXX#ychLr4O4La;;gr~@f{mun=OZ*hN*z4z=r8p$@V zCatv+EY0lq{=FZ2M<+9-sq%RH@qF>vL^gliZBJ#Qv(wpMJi$*4KHZ(2^cOz9zwxeE zJRzU)^LV1OtLGP1>=)1AQ+7*s5xW1yzvm}sPdmLWH%xNat&bFoM&pDQ`JlG|$)9Xs990>4w`V$sQ^X7Tl8 zF<&n9TL1gxNsD1FD%-S9or-9Wof&8)st+KVD^HI*MeDWKWSOEf0kyDGJC|~@kxVX9 zDCQ@NjtkwFie;yz<<D+Zpl- zW^$=)Iqi6TT2_SmHM-T_*0+sKc7$5QM4f^w8;gkb-aqA}j#;oSmYXjZQ;uq_?BW{P zsS<)vdkTv|so!*@J5rqqr^~ieUE}tiSl6CJSK99B?u^G$@jdZGPsg~uH#Q!(ere70 zL}SsoYVbHfhdz}8j?pbeQbi{XK$U^DmMiWX;PAWVjZM!MW-Ygvs;duphpS$ia!Q$$HIX?{Di;Gb#=nACE%HZy zYx~NXEbIj!LoqX6CWNbNX&lR7#p6>B&{QYk=u}f+2<#}m{<%`fT{86y$rN*)e>Op4 z*k`E4Rpae7kfAXy1W>k&!US4iYBQZr0fjP~`E^8_`SxfX4KgV@Kq{xP

TK{wdPP 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 "$RUNNER_TEMP/step1-changes.tar.gz" + tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null + tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . - name: Verify Python contracts and distributions shell: bash run: | @@ -52,7 +52,9 @@ jobs: - name: Commit verified implementation shell: bash run: | - git rm .github/workflows/apply-release-gates-sdist.yml .github/step1-changes.tar.gz + git rm .github/workflows/apply-release-gates-sdist.yml + git rm -r .github/step1-chunks + git rm .github/step1-changes.tar.gz git config user.name "NAGI" git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" git add .github/workflows/ci.yml .github/workflows/release.yml docs/release-runbook.md docs/requirements-traceability.md docs/verification/2026-08-23-release-gates-sdist.md scripts/verify_distribution_set.py scripts/verify_sdist_install.py tests/test_release_gate_contract.py tests/test_release_scripts.py From 9cc5a8f77e1b43b4b32c9225cc8745eee4241c0d Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:26:16 +0900 Subject: [PATCH 04/28] fix: normalize staged release gate tests --- .../workflows/apply-release-gates-sdist.yml | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml index caa0a0a..ec0211d 100644 --- a/.github/workflows/apply-release-gates-sdist.yml +++ b/.github/workflows/apply-release-gates-sdist.yml @@ -31,6 +31,30 @@ jobs: cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . + - name: Normalize staged test source + shell: bash + run: | + python - <<'PY' + from pathlib import Path + + path = Path("tests/test_release_scripts.py") + text = path.read_text(encoding="utf-8") + replacements = ( + ( + ''' metadata = (\n "Metadata-Version: 2.4\\n"\n "Name: pmgs-reference\\n"\n "Version: 0.4.0\\n"\n "Requires-Python: >=3.12\\n\\n"\n ).encode()\n''', + ''' metadata = (\n b"Metadata-Version: 2.4\\n"\n b"Name: pmgs-reference\\n"\n b"Version: 0.4.0\\n"\n b"Requires-Python: >=3.12\\n\\n"\n )\n''', + ), + ( + ''' with pytest.warns(UserWarning, match="Duplicate name"):\n with zipfile.ZipFile(rebuilt, "a") as archive:\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', + ''' with (\n pytest.warns(UserWarning, match="Duplicate name"),\n zipfile.ZipFile(rebuilt, "a") as archive,\n ):\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', + ), + ) + for old, new in replacements: + if old not in text: + raise SystemExit("staged test normalization anchor not found") + text = text.replace(old, new, 1) + path.write_text(text, encoding="utf-8") + PY - name: Verify Python contracts and distributions shell: bash run: | @@ -54,7 +78,6 @@ jobs: run: | git rm .github/workflows/apply-release-gates-sdist.yml git rm -r .github/step1-chunks - git rm .github/step1-changes.tar.gz git config user.name "NAGI" git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" git add .github/workflows/ci.yml .github/workflows/release.yml docs/release-runbook.md docs/requirements-traceability.md docs/verification/2026-08-23-release-gates-sdist.md scripts/verify_distribution_set.py scripts/verify_sdist_install.py tests/test_release_gate_contract.py tests/test_release_scripts.py From bc7b5574f78c10d0366889624d3e3679d7e8b551 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:27:24 +0900 Subject: [PATCH 05/28] fix: format staged release verification sources --- .github/workflows/apply-release-gates-sdist.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml index ec0211d..5050460 100644 --- a/.github/workflows/apply-release-gates-sdist.yml +++ b/.github/workflows/apply-release-gates-sdist.yml @@ -59,6 +59,7 @@ jobs: shell: bash run: | uv sync --frozen --all-groups + uv run --frozen ruff format scripts/verify_distribution_set.py tests/test_release_scripts.py uv run --frozen python scripts/verify_repository_boundary.py uv run --frozen ruff check . uv run --frozen ruff format --check . From 1c6329598fc093538783f144313c7e9b6966d783 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:31:12 +0900 Subject: [PATCH 06/28] chore: rerun release gate application with current workflow From 311b78abef925ceedd7959233cb05e598f7b0c12 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:34:05 +0900 Subject: [PATCH 07/28] fix: preserve release build job compatibility --- .../workflows/apply-release-gates-sdist.yml | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml index 5050460..bf03624 100644 --- a/.github/workflows/apply-release-gates-sdist.yml +++ b/.github/workflows/apply-release-gates-sdist.yml @@ -31,14 +31,14 @@ jobs: cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . - - name: Normalize staged test source + - name: Normalize staged sources shell: bash run: | python - <<'PY' from pathlib import Path - path = Path("tests/test_release_scripts.py") - text = path.read_text(encoding="utf-8") + test_path = Path("tests/test_release_scripts.py") + text = test_path.read_text(encoding="utf-8") replacements = ( ( ''' metadata = (\n "Metadata-Version: 2.4\\n"\n "Name: pmgs-reference\\n"\n "Version: 0.4.0\\n"\n "Requires-Python: >=3.12\\n\\n"\n ).encode()\n''', @@ -53,13 +53,44 @@ jobs: if old not in text: raise SystemExit("staged test normalization anchor not found") text = text.replace(old, new, 1) - path.write_text(text, encoding="utf-8") + test_path.write_text(text, encoding="utf-8") + + release_path = Path(".github/workflows/release.yml") + release = release_path.read_text(encoding="utf-8") + release_replacements = ( + (" build-distributions:\n", " build:\n"), + (" needs: build-distributions\n", " needs: build\n"), + (" - build-distributions\n", " - build\n"), + ) + for old, new in release_replacements: + if old not in release: + raise SystemExit(f"release workflow compatibility anchor not found: {old!r}") + release = release.replace(old, new) + release_path.write_text(release, encoding="utf-8") + + contract_path = Path("tests/test_release_gate_contract.py") + contract = contract_path.read_text(encoding="utf-8") + if '"build-distributions"' not in contract: + raise SystemExit("release gate test compatibility anchor not found") + contract_path.write_text( + contract.replace('"build-distributions"', '"build"'), + encoding="utf-8", + ) + + runbook_path = Path("docs/release-runbook.md") + runbook = runbook_path.read_text(encoding="utf-8") + if "`build-distributions`" not in runbook: + raise SystemExit("release runbook compatibility anchor not found") + runbook_path.write_text( + runbook.replace("`build-distributions`", "`build`"), + encoding="utf-8", + ) PY - name: Verify Python contracts and distributions shell: bash run: | uv sync --frozen --all-groups - uv run --frozen ruff format scripts/verify_distribution_set.py tests/test_release_scripts.py + uv run --frozen ruff format scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py uv run --frozen python scripts/verify_repository_boundary.py uv run --frozen ruff check . uv run --frozen ruff format --check . From 0c36b7b70dea65387ab7e97d0b51e20d421a75a9 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:34:23 +0900 Subject: [PATCH 08/28] chore: rerun release gates with compatible job identifiers From b3150a622248168faec08cdbc305db9215be181b Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:37:58 +0900 Subject: [PATCH 09/28] fix: validate shared distribution artifact semantically --- .../workflows/apply-release-gates-sdist.yml | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml index bf03624..5116c5a 100644 --- a/.github/workflows/apply-release-gates-sdist.yml +++ b/.github/workflows/apply-release-gates-sdist.yml @@ -85,12 +85,42 @@ jobs: runbook.replace("`build-distributions`", "`build`"), encoding="utf-8", ) + + project_contract_path = Path("tests/test_project_contracts.py") + project_contract = project_contract_path.read_text(encoding="utf-8") + old_artifact_assertion = ' assert raw.count("name: python-distributions") == 3\n' + new_artifact_assertion = ''' distribution_uploads = [ + step + for step in jobs["build"]["steps"] + if "upload-artifact@" in str(step.get("uses", "")) + and step.get("with", {}).get("name") == "python-distributions" + ] + assert len(distribution_uploads) == 1 + for job_name in ("distribution-e2e", "publish-pypi", "publish-github"): + distribution_downloads = [ + step + for step in jobs[job_name]["steps"] + if "download-artifact@" in str(step.get("uses", "")) + and step.get("with", {}).get("name") == "python-distributions" + ] + assert len(distribution_downloads) == 1 + ''' + if old_artifact_assertion not in project_contract: + raise SystemExit("project artifact contract anchor not found") + project_contract_path.write_text( + project_contract.replace( + old_artifact_assertion, + new_artifact_assertion, + 1, + ), + encoding="utf-8", + ) PY - name: Verify Python contracts and distributions shell: bash run: | uv sync --frozen --all-groups - uv run --frozen ruff format scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py + uv run --frozen ruff format scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py tests/test_project_contracts.py uv run --frozen python scripts/verify_repository_boundary.py uv run --frozen ruff check . uv run --frozen ruff format --check . @@ -112,6 +142,6 @@ jobs: git rm -r .github/step1-chunks git config user.name "NAGI" git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" - git add .github/workflows/ci.yml .github/workflows/release.yml docs/release-runbook.md docs/requirements-traceability.md docs/verification/2026-08-23-release-gates-sdist.md scripts/verify_distribution_set.py scripts/verify_sdist_install.py tests/test_release_gate_contract.py tests/test_release_scripts.py + git add .github/workflows/ci.yml .github/workflows/release.yml docs/release-runbook.md docs/requirements-traceability.md docs/verification/2026-08-23-release-gates-sdist.md scripts/verify_distribution_set.py scripts/verify_sdist_install.py tests/test_project_contracts.py tests/test_release_gate_contract.py tests/test_release_scripts.py git commit -m "ci: gate releases on three-OS sdist verification" git push origin HEAD:ci/release-gates-sdist From 04c7eaa8ff984f8d1fa3211cffc76f27b8d154c2 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 13:39:27 +0900 Subject: [PATCH 10/28] fix: preserve generated project-test indentation --- .../workflows/apply-release-gates-sdist.yml | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml index 5116c5a..158a647 100644 --- a/.github/workflows/apply-release-gates-sdist.yml +++ b/.github/workflows/apply-release-gates-sdist.yml @@ -89,22 +89,26 @@ jobs: project_contract_path = Path("tests/test_project_contracts.py") project_contract = project_contract_path.read_text(encoding="utf-8") old_artifact_assertion = ' assert raw.count("name: python-distributions") == 3\n' - new_artifact_assertion = ''' distribution_uploads = [ - step - for step in jobs["build"]["steps"] - if "upload-artifact@" in str(step.get("uses", "")) - and step.get("with", {}).get("name") == "python-distributions" - ] - assert len(distribution_uploads) == 1 - for job_name in ("distribution-e2e", "publish-pypi", "publish-github"): - distribution_downloads = [ - step - for step in jobs[job_name]["steps"] - if "download-artifact@" in str(step.get("uses", "")) - and step.get("with", {}).get("name") == "python-distributions" - ] - assert len(distribution_downloads) == 1 - ''' + new_artifact_assertion = "\n".join( + ( + " distribution_uploads = [", + " step", + ' for step in jobs["build"]["steps"]', + ' if "upload-artifact@" in str(step.get("uses", ""))', + ' and step.get("with", {}).get("name") == "python-distributions"', + " ]", + " assert len(distribution_uploads) == 1", + ' for job_name in ("distribution-e2e", "publish-pypi", "publish-github"):', + " distribution_downloads = [", + " step", + " for step in jobs[job_name][\"steps\"]", + ' if "download-artifact@" in str(step.get("uses", ""))', + ' and step.get("with", {}).get("name") == "python-distributions"', + " ]", + " assert len(distribution_downloads) == 1", + "", + ) + ) if old_artifact_assertion not in project_contract: raise SystemExit("project artifact contract anchor not found") project_contract_path.write_text( From 12075d0b580132906899339da4d49883d8ee3010 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:14:18 +0900 Subject: [PATCH 11/28] fix: align release artifact contract with verification job --- .github/workflows/apply-release-gates-sdist.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml index 158a647..7d2ca27 100644 --- a/.github/workflows/apply-release-gates-sdist.yml +++ b/.github/workflows/apply-release-gates-sdist.yml @@ -98,7 +98,7 @@ jobs: ' and step.get("with", {}).get("name") == "python-distributions"', " ]", " assert len(distribution_uploads) == 1", - ' for job_name in ("distribution-e2e", "publish-pypi", "publish-github"):', + ' for job_name in ("verify-distributions", "publish-pypi", "publish-github"):', " distribution_downloads = [", " step", " for step in jobs[job_name][\"steps\"]", From c9b7440d92cec21a99d4f8ebcc7bb77f4a0c0284 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:17:26 +0900 Subject: [PATCH 12/28] chore: expose reconstructed release implementation for review --- .../workflows/inspect-release-gates-sdist.yml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/inspect-release-gates-sdist.yml diff --git a/.github/workflows/inspect-release-gates-sdist.yml b/.github/workflows/inspect-release-gates-sdist.yml new file mode 100644 index 0000000..2cf1988 --- /dev/null +++ b/.github/workflows/inspect-release-gates-sdist.yml @@ -0,0 +1,35 @@ +name: inspect-release-gates-sdist + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + snapshot: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out feature branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ci/release-gates-sdist + persist-credentials: false + - name: Reconstruct reviewed source archive + shell: bash + run: | + cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" + tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null + mkdir "$RUNNER_TEMP/reconstructed" + tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C "$RUNNER_TEMP/reconstructed" + - name: Upload reconstructed source + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: reconstructed-release-gates-sdist + path: ${{ runner.temp }}/reconstructed + if-no-files-found: error + retention-days: 1 From d1cce0dcc8665e9100f98277d7f18cce14dbdea2 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:21:19 +0900 Subject: [PATCH 13/28] chore: include workflow files in reconstructed snapshot --- .github/workflows/inspect-release-gates-sdist.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/inspect-release-gates-sdist.yml b/.github/workflows/inspect-release-gates-sdist.yml index 2cf1988..b4f3433 100644 --- a/.github/workflows/inspect-release-gates-sdist.yml +++ b/.github/workflows/inspect-release-gates-sdist.yml @@ -31,5 +31,6 @@ jobs: with: name: reconstructed-release-gates-sdist path: ${{ runner.temp }}/reconstructed + include-hidden-files: true if-no-files-found: error retention-days: 1 From 814aefc93c9abeab2da8e332811193fc875984d9 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:24:30 +0900 Subject: [PATCH 14/28] fix: accept the uv dist marker without weakening artifact checks --- .../workflows/apply-release-gates-sdist.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml index 7d2ca27..69ded5d 100644 --- a/.github/workflows/apply-release-gates-sdist.yml +++ b/.github/workflows/apply-release-gates-sdist.yml @@ -48,6 +48,14 @@ jobs: ''' with pytest.warns(UserWarning, match="Duplicate name"):\n with zipfile.ZipFile(rebuilt, "a") as archive:\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', ''' with (\n pytest.warns(UserWarning, match="Duplicate name"),\n zipfile.ZipFile(rebuilt, "a") as archive,\n ):\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', ), + ( + ''' result = module.verify_distribution_set(tmp_path, "0.4.0")\n''', + ''' (tmp_path / ".gitignore").write_text("*\\n", encoding="utf-8")\n result = module.verify_distribution_set(tmp_path, "0.4.0")\n''', + ), + ( + ''' assert set(result["sha256"]) == {wheel.name, sdist.name}\n\n (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8")\n''', + ''' assert set(result["sha256"]) == {wheel.name, sdist.name}\n\n (tmp_path / ".gitignore").unlink()\n (tmp_path / ".gitignore").mkdir()\n with pytest.raises(RuntimeError, match="non-file uv build marker"):\n module.verify_distribution_set(tmp_path, "0.4.0")\n (tmp_path / ".gitignore").rmdir()\n\n (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8")\n''', + ), ) for old, new in replacements: if old not in text: @@ -55,6 +63,17 @@ jobs: text = text.replace(old, new, 1) test_path.write_text(text, encoding="utf-8") + verifier_path = Path("scripts/verify_distribution_set.py") + verifier = verifier_path.read_text(encoding="utf-8") + old_marker_boundary = ''' if symbolic_links:\n raise RuntimeError(\n f"symbolic link distribution entry: {', '.join(symbolic_links)}"\n )\n expected_names = {\n''' + new_marker_boundary = ''' if symbolic_links:\n raise RuntimeError(\n f"symbolic link distribution entry: {', '.join(symbolic_links)}"\n )\n uv_marker = root / ".gitignore"\n if uv_marker.exists() and not uv_marker.is_file():\n raise RuntimeError("non-file uv build marker: .gitignore")\n entries = [path for path in entries if path.name != ".gitignore"]\n expected_names = {\n''' + if old_marker_boundary not in verifier: + raise SystemExit("distribution verifier marker anchor not found") + verifier_path.write_text( + verifier.replace(old_marker_boundary, new_marker_boundary, 1), + encoding="utf-8", + ) + release_path = Path(".github/workflows/release.yml") release = release_path.read_text(encoding="utf-8") release_replacements = ( @@ -143,6 +162,7 @@ jobs: shell: bash run: | git rm .github/workflows/apply-release-gates-sdist.yml + git rm .github/workflows/inspect-release-gates-sdist.yml git rm -r .github/step1-chunks git config user.name "NAGI" git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" From 210084bfdd5b36c6b87c168c65e5bc3811ede2a4 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:29:59 +0900 Subject: [PATCH 15/28] chore: add temporary release-gate materializer --- scripts/materialize_release_gates_sdist.py | 131 +++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 scripts/materialize_release_gates_sdist.py diff --git a/scripts/materialize_release_gates_sdist.py b/scripts/materialize_release_gates_sdist.py new file mode 100644 index 0000000..80b0f9e --- /dev/null +++ b/scripts/materialize_release_gates_sdist.py @@ -0,0 +1,131 @@ +"""Materialize the reviewed release-gate patch for temporary CI handoff.""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def _replace_once(text: str, old: str, new: str, *, label: str) -> str: + if old not in text: + raise RuntimeError(f"materialization anchor not found: {label}") + return text.replace(old, new, 1) + + +def main() -> int: + test_path = ROOT / "tests" / "test_release_scripts.py" + text = test_path.read_text(encoding="utf-8") + replacements = ( + ( + ''' metadata = (\n "Metadata-Version: 2.4\\n"\n "Name: pmgs-reference\\n"\n "Version: 0.4.0\\n"\n "Requires-Python: >=3.12\\n\\n"\n ).encode()\n''', + ''' metadata = (\n b"Metadata-Version: 2.4\\n"\n b"Name: pmgs-reference\\n"\n b"Version: 0.4.0\\n"\n b"Requires-Python: >=3.12\\n\\n"\n )\n''', + "metadata bytes", + ), + ( + ''' with pytest.warns(UserWarning, match="Duplicate name"):\n with zipfile.ZipFile(rebuilt, "a") as archive:\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', + ''' with (\n pytest.warns(UserWarning, match="Duplicate name"),\n zipfile.ZipFile(rebuilt, "a") as archive,\n ):\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', + "duplicate wheel context", + ), + ( + ''' result = module.verify_distribution_set(tmp_path, "0.4.0")\n''', + ''' (tmp_path / ".gitignore").write_text("*\\n", encoding="utf-8")\n result = module.verify_distribution_set(tmp_path, "0.4.0")\n''', + "uv marker acceptance", + ), + ( + ''' assert set(result["sha256"]) == {wheel.name, sdist.name}\n\n (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8")\n''', + ''' assert set(result["sha256"]) == {wheel.name, sdist.name}\n\n (tmp_path / ".gitignore").unlink()\n (tmp_path / ".gitignore").mkdir()\n with pytest.raises(RuntimeError, match="non-file uv build marker"):\n module.verify_distribution_set(tmp_path, "0.4.0")\n (tmp_path / ".gitignore").rmdir()\n\n (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8")\n''', + "uv marker boundary", + ), + ) + for old, new, label in replacements: + text = _replace_once(text, old, new, label=label) + test_path.write_text(text, encoding="utf-8") + + verifier_path = ROOT / "scripts" / "verify_distribution_set.py" + verifier = verifier_path.read_text(encoding="utf-8") + old_marker_boundary = ''' if symbolic_links:\n raise RuntimeError(\n f"symbolic link distribution entry: {', '.join(symbolic_links)}"\n )\n expected_names = {\n''' + new_marker_boundary = ''' if symbolic_links:\n raise RuntimeError(\n f"symbolic link distribution entry: {', '.join(symbolic_links)}"\n )\n uv_marker = root / ".gitignore"\n if uv_marker.exists() and not uv_marker.is_file():\n raise RuntimeError("non-file uv build marker: .gitignore")\n entries = [path for path in entries if path.name != ".gitignore"]\n expected_names = {\n''' + verifier_path.write_text( + _replace_once( + verifier, + old_marker_boundary, + new_marker_boundary, + label="distribution verifier uv marker", + ), + encoding="utf-8", + ) + + release_path = ROOT / ".github" / "workflows" / "release.yml" + release = release_path.read_text(encoding="utf-8") + for old, new in ( + (" build-distributions:\n", " build:\n"), + (" needs: build-distributions\n", " needs: build\n"), + (" - build-distributions\n", " - build\n"), + ): + if old not in release: + raise RuntimeError(f"release workflow compatibility anchor not found: {old!r}") + release = release.replace(old, new) + release_path.write_text(release, encoding="utf-8") + + contract_path = ROOT / "tests" / "test_release_gate_contract.py" + contract = contract_path.read_text(encoding="utf-8") + contract_path.write_text( + _replace_once( + contract, + '"build-distributions"', + '"build"', + label="release gate build job", + ), + encoding="utf-8", + ) + + runbook_path = ROOT / "docs" / "release-runbook.md" + runbook = runbook_path.read_text(encoding="utf-8") + runbook_path.write_text( + _replace_once( + runbook, + "`build-distributions`", + "`build`", + label="release runbook build job", + ), + encoding="utf-8", + ) + + project_contract_path = ROOT / "tests" / "test_project_contracts.py" + project_contract = project_contract_path.read_text(encoding="utf-8") + old_artifact_assertion = ' assert raw.count("name: python-distributions") == 3\n' + new_artifact_assertion = "\n".join( + ( + " distribution_uploads = [", + " step", + ' for step in jobs["build"]["steps"]', + ' if "upload-artifact@" in str(step.get("uses", ""))', + ' and step.get("with", {}).get("name") == "python-distributions"', + " ]", + " assert len(distribution_uploads) == 1", + ' for job_name in ("verify-distributions", "publish-pypi", "publish-github"):', + " distribution_downloads = [", + " step", + ' for step in jobs[job_name]["steps"]', + ' if "download-artifact@" in str(step.get("uses", ""))', + ' and step.get("with", {}).get("name") == "python-distributions"', + " ]", + " assert len(distribution_downloads) == 1", + "", + ) + ) + project_contract_path.write_text( + _replace_once( + project_contract, + old_artifact_assertion, + new_artifact_assertion, + label="project distribution artifact contract", + ), + encoding="utf-8", + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 5994e5f7c5d979b6510efe13d5dc04d6ee841188 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:30:33 +0900 Subject: [PATCH 16/28] chore: materialize verified release-gate files --- .../materialize-release-gates-sdist.yml | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/materialize-release-gates-sdist.yml diff --git a/.github/workflows/materialize-release-gates-sdist.yml b/.github/workflows/materialize-release-gates-sdist.yml new file mode 100644 index 0000000..f53d97c --- /dev/null +++ b/.github/workflows/materialize-release-gates-sdist.yml @@ -0,0 +1,74 @@ +name: materialize-release-gates-sdist + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + materialize: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Check out feature branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ci/release-gates-sdist + persist-credentials: false + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + enable-cache: true + - name: Reconstruct and normalize reviewed implementation + shell: bash + run: | + cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" + tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null + tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . + python scripts/materialize_release_gates_sdist.py + - name: Verify complete implementation + shell: bash + run: | + uv sync --frozen --all-groups + uv run --frozen ruff format scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py tests/test_project_contracts.py + uv run --frozen python scripts/verify_repository_boundary.py + uv run --frozen ruff check . + uv run --frozen ruff format --check . + uv run --frozen mypy src + uv run --frozen pytest -q + uv build --clear + uv run --frozen python scripts/verify_distribution_set.py --dist-dir dist + uv run --frozen python scripts/verify_wheel_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs + uv run --frozen python scripts/verify_sdist_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs + npm --prefix worker ci + npm --prefix worker run verify + - name: Collect exact verified files + shell: bash + run: | + mkdir -p "$RUNNER_TEMP/verified" + cp --parents \ + .github/workflows/ci.yml \ + .github/workflows/release.yml \ + docs/release-runbook.md \ + docs/requirements-traceability.md \ + docs/verification/2026-08-23-release-gates-sdist.md \ + scripts/verify_distribution_set.py \ + scripts/verify_sdist_install.py \ + tests/test_project_contracts.py \ + tests/test_release_gate_contract.py \ + tests/test_release_scripts.py \ + "$RUNNER_TEMP/verified" + - name: Upload verified implementation + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: verified-release-gates-sdist + path: ${{ runner.temp }}/verified + include-hidden-files: true + if-no-files-found: error + retention-days: 1 From 48253ad4d8fa6719ca41309eb4229a8edb9a862e Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:33:37 +0900 Subject: [PATCH 17/28] fix: remove temporary materializer before repository verification --- .github/workflows/materialize-release-gates-sdist.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/materialize-release-gates-sdist.yml b/.github/workflows/materialize-release-gates-sdist.yml index f53d97c..818fb22 100644 --- a/.github/workflows/materialize-release-gates-sdist.yml +++ b/.github/workflows/materialize-release-gates-sdist.yml @@ -32,6 +32,7 @@ jobs: tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . python scripts/materialize_release_gates_sdist.py + rm scripts/materialize_release_gates_sdist.py - name: Verify complete implementation shell: bash run: | From 1d895ad520b2b0b4bff64a9f3ae49fe4abc7af4a Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:35:39 +0900 Subject: [PATCH 18/28] chore: exempt temporary materializer literals from line-length lint --- scripts/materialize_release_gates_sdist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/materialize_release_gates_sdist.py b/scripts/materialize_release_gates_sdist.py index 80b0f9e..7be8378 100644 --- a/scripts/materialize_release_gates_sdist.py +++ b/scripts/materialize_release_gates_sdist.py @@ -1,3 +1,4 @@ +# ruff: noqa: E501 """Materialize the reviewed release-gate patch for temporary CI handoff.""" from __future__ import annotations From 3f7de021e0a5e0feaedfd8cdcb6c0a5037ec0948 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:36:27 +0900 Subject: [PATCH 19/28] fix: retain tracked materializer during boundary verification --- .github/workflows/materialize-release-gates-sdist.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/materialize-release-gates-sdist.yml b/.github/workflows/materialize-release-gates-sdist.yml index 818fb22..f53d97c 100644 --- a/.github/workflows/materialize-release-gates-sdist.yml +++ b/.github/workflows/materialize-release-gates-sdist.yml @@ -32,7 +32,6 @@ jobs: tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . python scripts/materialize_release_gates_sdist.py - rm scripts/materialize_release_gates_sdist.py - name: Verify complete implementation shell: bash run: | From cbb00283451cc93ad09e8ef8c8bce2cfe00ca504 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:37:53 +0900 Subject: [PATCH 20/28] fix: format the temporary materializer before full checks --- .github/workflows/materialize-release-gates-sdist.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/materialize-release-gates-sdist.yml b/.github/workflows/materialize-release-gates-sdist.yml index f53d97c..3a746da 100644 --- a/.github/workflows/materialize-release-gates-sdist.yml +++ b/.github/workflows/materialize-release-gates-sdist.yml @@ -36,7 +36,7 @@ jobs: shell: bash run: | uv sync --frozen --all-groups - uv run --frozen ruff format scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py tests/test_project_contracts.py + uv run --frozen ruff format scripts/materialize_release_gates_sdist.py scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py tests/test_project_contracts.py uv run --frozen python scripts/verify_repository_boundary.py uv run --frozen ruff check . uv run --frozen ruff format --check . From d14342431b5dbf11772fb9b3bb49fa1dc5c2be0b Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:40:36 +0900 Subject: [PATCH 21/28] fix: normalize every release build dependency assertion --- .github/workflows/materialize-release-gates-sdist.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/materialize-release-gates-sdist.yml b/.github/workflows/materialize-release-gates-sdist.yml index 3a746da..5b03a18 100644 --- a/.github/workflows/materialize-release-gates-sdist.yml +++ b/.github/workflows/materialize-release-gates-sdist.yml @@ -32,6 +32,13 @@ jobs: tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . python scripts/materialize_release_gates_sdist.py + python - <<'PY' + from pathlib import Path + + path = Path("tests/test_release_gate_contract.py") + text = path.read_text(encoding="utf-8") + path.write_text(text.replace('"build-distributions"', '"build"'), encoding="utf-8") + PY - name: Verify complete implementation shell: bash run: | From ba870b111cc6af6aaabc7c4627fe48b167ae02c7 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:46:42 +0900 Subject: [PATCH 22/28] chore: commit verified non-workflow release files --- .../commit-release-gates-nonworkflow.yml | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/commit-release-gates-nonworkflow.yml diff --git a/.github/workflows/commit-release-gates-nonworkflow.yml b/.github/workflows/commit-release-gates-nonworkflow.yml new file mode 100644 index 0000000..8ef31a8 --- /dev/null +++ b/.github/workflows/commit-release-gates-nonworkflow.yml @@ -0,0 +1,75 @@ +name: commit-release-gates-nonworkflow + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + commit: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Check out feature branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ci/release-gates-sdist + fetch-depth: 0 + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + enable-cache: true + - name: Reconstruct and normalize reviewed implementation + shell: bash + run: | + cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" + tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null + tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . + python scripts/materialize_release_gates_sdist.py + python - <<'PY' + from pathlib import Path + + path = Path("tests/test_release_gate_contract.py") + text = path.read_text(encoding="utf-8") + path.write_text(text.replace('"build-distributions"', '"build"'), encoding="utf-8") + PY + - name: Verify complete implementation + shell: bash + run: | + uv sync --frozen --all-groups + uv run --frozen ruff format scripts/materialize_release_gates_sdist.py scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py tests/test_project_contracts.py + uv run --frozen python scripts/verify_repository_boundary.py + uv run --frozen ruff check . + uv run --frozen ruff format --check . + uv run --frozen mypy src + uv run --frozen pytest -q + uv build --clear + uv run --frozen python scripts/verify_distribution_set.py --dist-dir dist + uv run --frozen python scripts/verify_wheel_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs + uv run --frozen python scripts/verify_sdist_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs + npm --prefix worker ci + npm --prefix worker run verify + - name: Commit verified non-workflow files + shell: bash + run: | + git rm -r .github/step1-chunks + git rm scripts/materialize_release_gates_sdist.py + git config user.name "NAGI" + git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" + git add \ + docs/release-runbook.md \ + docs/requirements-traceability.md \ + docs/verification/2026-08-23-release-gates-sdist.md \ + scripts/verify_distribution_set.py \ + scripts/verify_sdist_install.py \ + tests/test_project_contracts.py \ + tests/test_release_gate_contract.py \ + tests/test_release_scripts.py + git commit -m "ci: verify release artifacts across three operating systems" + git push origin HEAD:ci/release-gates-sdist From 7fe83841585f1955be0ee65f3259f75284c06646 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:49:53 +0900 Subject: [PATCH 23/28] fix: remove formatted temporary materializer forcefully --- .github/workflows/commit-release-gates-nonworkflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/commit-release-gates-nonworkflow.yml b/.github/workflows/commit-release-gates-nonworkflow.yml index 8ef31a8..b0d2274 100644 --- a/.github/workflows/commit-release-gates-nonworkflow.yml +++ b/.github/workflows/commit-release-gates-nonworkflow.yml @@ -59,7 +59,7 @@ jobs: shell: bash run: | git rm -r .github/step1-chunks - git rm scripts/materialize_release_gates_sdist.py + git rm -f scripts/materialize_release_gates_sdist.py git config user.name "NAGI" git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" git add \ From f9926e777594981d778f7fc9e0ce7aeb7ac31360 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 07:51:35 +0000 Subject: [PATCH 24/28] ci: verify release artifacts across three operating systems --- .github/step1-chunks/part-00.txt | 1 - .github/step1-chunks/part-01.txt | 1 - .github/step1-chunks/part-02.txt | 1 - .github/step1-chunks/part-03.txt | 1 - .github/step1-chunks/part-04.txt | 1 - .github/step1-chunks/part-05.txt | 1 - .github/step1-chunks/part-06.txt | 1 - .github/step1-chunks/part-07.txt | 1 - .github/step1-chunks/part-08.txt | 1 - docs/release-runbook.md | 6 +- docs/requirements-traceability.md | 2 +- .../2026-08-23-release-gates-sdist.md | 28 ++ scripts/materialize_release_gates_sdist.py | 132 --------- scripts/verify_distribution_set.py | 128 +++++++++ scripts/verify_sdist_install.py | 252 ++++++++++++++++++ tests/test_project_contracts.py | 16 +- tests/test_release_gate_contract.py | 110 ++++++++ tests/test_release_scripts.py | 186 +++++++++++++ 18 files changed, 725 insertions(+), 144 deletions(-) delete mode 100644 .github/step1-chunks/part-00.txt delete mode 100644 .github/step1-chunks/part-01.txt delete mode 100644 .github/step1-chunks/part-02.txt delete mode 100644 .github/step1-chunks/part-03.txt delete mode 100644 .github/step1-chunks/part-04.txt delete mode 100644 .github/step1-chunks/part-05.txt delete mode 100644 .github/step1-chunks/part-06.txt delete mode 100644 .github/step1-chunks/part-07.txt delete mode 100644 .github/step1-chunks/part-08.txt create mode 100644 docs/verification/2026-08-23-release-gates-sdist.md delete mode 100644 scripts/materialize_release_gates_sdist.py create mode 100644 scripts/verify_distribution_set.py create mode 100644 scripts/verify_sdist_install.py create mode 100644 tests/test_release_gate_contract.py diff --git a/.github/step1-chunks/part-00.txt b/.github/step1-chunks/part-00.txt deleted file mode 100644 index c28b9d8..0000000 --- a/.github/step1-chunks/part-00.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/step1-chunks/part-01.txt b/.github/step1-chunks/part-01.txt deleted file mode 100644 index 35af363..0000000 --- a/.github/step1-chunks/part-01.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/step1-chunks/part-02.txt b/.github/step1-chunks/part-02.txt deleted file mode 100644 index 32a660b..0000000 --- a/.github/step1-chunks/part-02.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/step1-chunks/part-03.txt b/.github/step1-chunks/part-03.txt deleted file mode 100644 index 0f080a1..0000000 --- a/.github/step1-chunks/part-03.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/step1-chunks/part-04.txt b/.github/step1-chunks/part-04.txt deleted file mode 100644 index e018c8d..0000000 --- a/.github/step1-chunks/part-04.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/step1-chunks/part-05.txt b/.github/step1-chunks/part-05.txt deleted file mode 100644 index d4b7dc6..0000000 --- a/.github/step1-chunks/part-05.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/step1-chunks/part-06.txt b/.github/step1-chunks/part-06.txt deleted file mode 100644 index a45a1e6..0000000 --- a/.github/step1-chunks/part-06.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/step1-chunks/part-07.txt b/.github/step1-chunks/part-07.txt deleted file mode 100644 index cdbfe11..0000000 --- a/.github/step1-chunks/part-07.txt +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/.github/step1-chunks/part-08.txt b/.github/step1-chunks/part-08.txt deleted file mode 100644 index b2e7fc1..0000000 --- a/.github/step1-chunks/part-08.txt +++ /dev/null @@ -1 +0,0 @@ -3HI7hMQkt/8GdMSF8TYjIf5jWLUI3mbNhU0AVSvaFyxYvMDhDACflp0twYDTqo0dQo3s6PGp0haQoq4WSMor6rajAe5YwfsZg05z7mPW1rf3uF3Mua0vg6D/CnwDAVxt7gF1nQOulb7FgRKiGkDIbiAkPoKgaExCAcEjMFFV9xFBdfd4it3P7mf3s/vZ/ex+dj+7n93P7mf3s/vZ/ex+dj+7n93P7mf38xif/w9BMkI8AGgBAA== \ No newline at end of file diff --git a/docs/release-runbook.md b/docs/release-runbook.md index d5fcc11..9857723 100644 --- a/docs/release-runbook.md +++ b/docs/release-runbook.md @@ -222,7 +222,11 @@ uv run --frozen python scripts/verify_release_tag.py --tag v0.4.0 ### 4. workflow結果を確認する -`build` jobがboundary、Ruff、format、mypy、pytest、wheel、sdistを再検証した後、`publish-pypi` jobは`pypi` environmentで承認待ちになる。 +`release.yml`の3 OS release gateは、タグ付きsourceから一度だけ固定した同一のwheelとsdistをLinux、Windows、macOSへ配布し、各OSでsource test、隔離wheel、sdist由来wheel、合成決定性を検証する。`compare-synthetic-determinism`が3 OSの安定contractを比較し、Worker検証とdistribution metadata/hash検証を含む全gateが成功するまで`publish-pypi`は開始しない。 + +sdistは`verify_sdist_install.py`でpath traversal、link、想定外top-level entryを拒否して安全に展開し、source checkout外でwheelへ再構築する。sdist由来wheelは公開wheelとruntime file hash、metadata、console entry pointを照合し、その後`verify_wheel_install.py`と同じsetup、再実行、doctor、lookup、MCP 3 tool、version smokeを通す。 + +`build`がtag/versionとrepository boundaryを確認してwheelとsdistを一度だけ固定する。3 OS、distribution、Worker、cross-OS determinismの全gateが成功した後、`publish-pypi` jobは`pypi` environmentで承認待ちになる。 承認後、PyPI Trusted Publishingとattestation付きで配布する。PyPI成功後だけ`publish-github` jobが同じwheelとsdistからGitHub Releaseを作る。 diff --git a/docs/requirements-traceability.md b/docs/requirements-traceability.md index a9857ca..b2f0942 100644 --- a/docs/requirements-traceability.md +++ b/docs/requirements-traceability.md @@ -53,7 +53,7 @@ | RELEASE-01 | 合成fixtureでrepository全検査を通す | verified | pytest 221件、Ruff、mypy、boundary、合成A/B決定性、sdist、wheelに合格。Windowsで権限上skipしたsymlink 7件はhosted CIでも検証する | | RELEASE-02 | 実データのA/B buildと全件監査を通す | verified | 最終コードでNTFSとexFATへ独立再構築したSQLite A/Bのdatabase・build report・validation reportがbytesとSHA-256まで一致。公開tree A/Bも最終validatorと25条件release auditに合格 | | RELEASE-03 | build、test、typecheck、lint、Worker bundleを再現する | verified | 最終コードでPython標準検査、二重package build、隔離wheel、Worker 31件とWebMCP 3件に合格 | -| RELEASE-04 | wheelを3 OSで隔離導入し、setup、再実行、doctorを検証する | verified | commit `4ec6738`の[Pull Request CI run 31634407211](https://github.com/Nagi-Inaba/pmgs-reference/actions/runs/31634407211)でUbuntu、Windows、macOSのwheel E2Eと3 OS合成決定性比較を含む14 jobが成功 | +| RELEASE-04 | wheelとsdistを3 OSで隔離導入し、setup、再実行、doctorを検証する | implemented | 通常CIとtag releaseの双方へ、同一artifactのwheelとsdist由来wheelをLinux、Windows、macOSで検証するgateを追加。hosted PR CIの成功後にverifiedへ更新する | | RELEASE-05 | tag、承認環境、Trusted Publishing、attestationでPyPIとGitHub Releaseへ同じ成果物を配布する | verified | [Release run 31677401736](https://github.com/Nagi-Inaba/pmgs-reference/actions/runs/31677401736)で`v0.4.0`を承認付き`pypi`環境からTrusted Publishingし、同じwheelとsdistを[GitHub Release](https://github.com/Nagi-Inaba/pmgs-reference/releases/tag/v0.4.0)へ配布。PyPI provenance、両配布面のSHA-256一致、空環境からのsetupを外部確認 | | GH-01 | 追跡対象と公開履歴に実データ、生成DB、秘密情報、端末固有pathがない | verified | 公開境界guardと単一rootの公開用履歴を検査 | | GH-02 | CIを最小権限、SHA固定、credential非保持、timeout付きで定義する | verified | commit `4ec6738`の[Push CI run 31634401819](https://github.com/Nagi-Inaba/pmgs-reference/actions/runs/31634401819)と[Pull Request CI run 31634407211](https://github.com/Nagi-Inaba/pmgs-reference/actions/runs/31634407211)で各14 jobが成功。既存10 checkはbranch protectionで必須、追加の決定性4 jobもPR統合ゲートとして確認 | diff --git a/docs/verification/2026-08-23-release-gates-sdist.md b/docs/verification/2026-08-23-release-gates-sdist.md new file mode 100644 index 0000000..f6dd76f --- /dev/null +++ b/docs/verification/2026-08-23-release-gates-sdist.md @@ -0,0 +1,28 @@ +# Release gates and sdist verification — 2026-08-23 + +Issues #20 and #34 + +## Contract + +- The tag workflow builds exactly one wheel and one sdist once. +- Linux, Windows, and macOS download that fixed artifact set. +- Each OS runs the source suite, installed-wheel E2E, sdist-derived-wheel E2E, and synthetic determinism. +- The sdist extractor rejects absolute paths, traversal, backslashes, links, special files, duplicate paths, unexpected roots, excessive member counts, and excessive uncompressed size. +- The wheel rebuilt from the sdist must match the release wheel in runtime payload hashes and distribution metadata; `RECORD` is excluded because it is derived from the wheel contents. +- A dedicated gate rejects missing, extra, non-file, or symbolic-link distribution entries and verifies wheel/sdist project metadata. +- Worker verification and the three-platform determinism comparison are required before PyPI publication. + +## Local evidence + +The focused contract suite was run against the implementation: + +```text +PYTHONPATH=src python -m pytest -q tests/test_release_gate_contract.py tests/test_release_scripts.py -k 'not synthetic_determinism_report_is_valid_and_path_free and not synthetic_pdf_generation_is_byte_reproducible' +21 passed, 2 deselected +``` + +Two pre-existing synthetic-PDF tests cannot be reproduced in this container because its system PyMuPDF is 1.26.7 and lacks the project-pinned `reproducible=True` API. The required hosted matrix uses the locked PyMuPDF 1.28 series and remains the merge authority. + +## Merge gate + +Before merge, the pull-request merge ref must pass the complete hosted matrix, including all required branch-protection checks. The first real tag using this workflow is also the operational proof that no publication job starts before every release gate succeeds. diff --git a/scripts/materialize_release_gates_sdist.py b/scripts/materialize_release_gates_sdist.py deleted file mode 100644 index 7be8378..0000000 --- a/scripts/materialize_release_gates_sdist.py +++ /dev/null @@ -1,132 +0,0 @@ -# ruff: noqa: E501 -"""Materialize the reviewed release-gate patch for temporary CI handoff.""" - -from __future__ import annotations - -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] - - -def _replace_once(text: str, old: str, new: str, *, label: str) -> str: - if old not in text: - raise RuntimeError(f"materialization anchor not found: {label}") - return text.replace(old, new, 1) - - -def main() -> int: - test_path = ROOT / "tests" / "test_release_scripts.py" - text = test_path.read_text(encoding="utf-8") - replacements = ( - ( - ''' metadata = (\n "Metadata-Version: 2.4\\n"\n "Name: pmgs-reference\\n"\n "Version: 0.4.0\\n"\n "Requires-Python: >=3.12\\n\\n"\n ).encode()\n''', - ''' metadata = (\n b"Metadata-Version: 2.4\\n"\n b"Name: pmgs-reference\\n"\n b"Version: 0.4.0\\n"\n b"Requires-Python: >=3.12\\n\\n"\n )\n''', - "metadata bytes", - ), - ( - ''' with pytest.warns(UserWarning, match="Duplicate name"):\n with zipfile.ZipFile(rebuilt, "a") as archive:\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', - ''' with (\n pytest.warns(UserWarning, match="Duplicate name"),\n zipfile.ZipFile(rebuilt, "a") as archive,\n ):\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', - "duplicate wheel context", - ), - ( - ''' result = module.verify_distribution_set(tmp_path, "0.4.0")\n''', - ''' (tmp_path / ".gitignore").write_text("*\\n", encoding="utf-8")\n result = module.verify_distribution_set(tmp_path, "0.4.0")\n''', - "uv marker acceptance", - ), - ( - ''' assert set(result["sha256"]) == {wheel.name, sdist.name}\n\n (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8")\n''', - ''' assert set(result["sha256"]) == {wheel.name, sdist.name}\n\n (tmp_path / ".gitignore").unlink()\n (tmp_path / ".gitignore").mkdir()\n with pytest.raises(RuntimeError, match="non-file uv build marker"):\n module.verify_distribution_set(tmp_path, "0.4.0")\n (tmp_path / ".gitignore").rmdir()\n\n (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8")\n''', - "uv marker boundary", - ), - ) - for old, new, label in replacements: - text = _replace_once(text, old, new, label=label) - test_path.write_text(text, encoding="utf-8") - - verifier_path = ROOT / "scripts" / "verify_distribution_set.py" - verifier = verifier_path.read_text(encoding="utf-8") - old_marker_boundary = ''' if symbolic_links:\n raise RuntimeError(\n f"symbolic link distribution entry: {', '.join(symbolic_links)}"\n )\n expected_names = {\n''' - new_marker_boundary = ''' if symbolic_links:\n raise RuntimeError(\n f"symbolic link distribution entry: {', '.join(symbolic_links)}"\n )\n uv_marker = root / ".gitignore"\n if uv_marker.exists() and not uv_marker.is_file():\n raise RuntimeError("non-file uv build marker: .gitignore")\n entries = [path for path in entries if path.name != ".gitignore"]\n expected_names = {\n''' - verifier_path.write_text( - _replace_once( - verifier, - old_marker_boundary, - new_marker_boundary, - label="distribution verifier uv marker", - ), - encoding="utf-8", - ) - - release_path = ROOT / ".github" / "workflows" / "release.yml" - release = release_path.read_text(encoding="utf-8") - for old, new in ( - (" build-distributions:\n", " build:\n"), - (" needs: build-distributions\n", " needs: build\n"), - (" - build-distributions\n", " - build\n"), - ): - if old not in release: - raise RuntimeError(f"release workflow compatibility anchor not found: {old!r}") - release = release.replace(old, new) - release_path.write_text(release, encoding="utf-8") - - contract_path = ROOT / "tests" / "test_release_gate_contract.py" - contract = contract_path.read_text(encoding="utf-8") - contract_path.write_text( - _replace_once( - contract, - '"build-distributions"', - '"build"', - label="release gate build job", - ), - encoding="utf-8", - ) - - runbook_path = ROOT / "docs" / "release-runbook.md" - runbook = runbook_path.read_text(encoding="utf-8") - runbook_path.write_text( - _replace_once( - runbook, - "`build-distributions`", - "`build`", - label="release runbook build job", - ), - encoding="utf-8", - ) - - project_contract_path = ROOT / "tests" / "test_project_contracts.py" - project_contract = project_contract_path.read_text(encoding="utf-8") - old_artifact_assertion = ' assert raw.count("name: python-distributions") == 3\n' - new_artifact_assertion = "\n".join( - ( - " distribution_uploads = [", - " step", - ' for step in jobs["build"]["steps"]', - ' if "upload-artifact@" in str(step.get("uses", ""))', - ' and step.get("with", {}).get("name") == "python-distributions"', - " ]", - " assert len(distribution_uploads) == 1", - ' for job_name in ("verify-distributions", "publish-pypi", "publish-github"):', - " distribution_downloads = [", - " step", - ' for step in jobs[job_name]["steps"]', - ' if "download-artifact@" in str(step.get("uses", ""))', - ' and step.get("with", {}).get("name") == "python-distributions"', - " ]", - " assert len(distribution_downloads) == 1", - "", - ) - ) - project_contract_path.write_text( - _replace_once( - project_contract, - old_artifact_assertion, - new_artifact_assertion, - label="project distribution artifact contract", - ), - encoding="utf-8", - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/verify_distribution_set.py b/scripts/verify_distribution_set.py new file mode 100644 index 0000000..1ddbe6d --- /dev/null +++ b/scripts/verify_distribution_set.py @@ -0,0 +1,128 @@ +"""Verify that a release artifact directory contains one exact wheel and sdist.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import tarfile +import tomllib +import zipfile +from email.parser import BytesParser +from email.policy import default +from pathlib import Path +from typing import cast + +ROOT = Path(__file__).resolve().parents[1] + + +def _project_version() -> str: + payload = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + project = payload.get("project") + if not isinstance(project, dict) or not isinstance(project.get("version"), str): + raise RuntimeError("pyproject.toml does not declare a project version") + return cast(str, project["version"]) + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest().upper() + + +def _metadata_fields(payload: bytes) -> dict[str, str | None]: + message = BytesParser(policy=default).parsebytes(payload) + return { + "name": message.get("Name"), + "version": message.get("Version"), + "requires_python": message.get("Requires-Python"), + } + + +def _wheel_metadata(path: Path) -> dict[str, str | None]: + with zipfile.ZipFile(path) as archive: + matches = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")] + if len(matches) != 1: + raise RuntimeError("wheel METADATA member is missing or ambiguous") + return _metadata_fields(archive.read(matches[0])) + + +def _sdist_metadata(path: Path) -> dict[str, str | None]: + with tarfile.open(path, "r:gz") as archive: + matches = [member for member in archive.getmembers() if member.name.endswith("/PKG-INFO")] + if len(matches) != 1: + raise RuntimeError("sdist PKG-INFO member is missing or ambiguous") + source = archive.extractfile(matches[0]) + if source is None: + raise RuntimeError("sdist PKG-INFO member is unreadable") + with source: + return _metadata_fields(source.read()) + + +def verify_distribution_set(dist_dir: Path, version: str) -> dict[str, object]: + root = dist_dir.resolve() + if not root.is_dir(): + raise RuntimeError("distribution directory does not exist") + entries = sorted(root.iterdir(), key=lambda path: path.name) + symbolic_links = [path.name for path in entries if path.is_symlink()] + if symbolic_links: + raise RuntimeError(f"symbolic link distribution entry: {', '.join(symbolic_links)}") + uv_marker = root / ".gitignore" + if uv_marker.exists() and not uv_marker.is_file(): + raise RuntimeError("non-file uv build marker: .gitignore") + entries = [path for path in entries if path.name != ".gitignore"] + expected_names = { + f"pmgs_reference-{version}-py3-none-any.whl", + f"pmgs_reference-{version}.tar.gz", + } + actual_names = {path.name for path in entries if path.is_file()} + non_files = [path.name for path in entries if not path.is_file()] + unexpected = sorted(actual_names - expected_names) + missing = sorted(expected_names - actual_names) + if non_files or unexpected or missing or len(entries) != 2: + details = [] + if missing: + details.append(f"missing: {', '.join(missing)}") + if unexpected: + details.append(f"unexpected distribution artifact: {', '.join(unexpected)}") + if non_files: + details.append(f"non-file distribution entry: {', '.join(non_files)}") + raise RuntimeError("; ".join(details) or "unexpected distribution artifact set") + files = sorted(expected_names) + wheel = root / f"pmgs_reference-{version}-py3-none-any.whl" + sdist = root / f"pmgs_reference-{version}.tar.gz" + expected_metadata = { + "name": "pmgs-reference", + "version": version, + "requires_python": ">=3.12", + } + if _wheel_metadata(wheel) != expected_metadata: + raise RuntimeError("wheel metadata does not match the release contract") + if _sdist_metadata(sdist) != expected_metadata: + raise RuntimeError("sdist metadata does not match the release contract") + return { + "schema_version": "1.0", + "version": version, + "files": files, + "sha256": {name: _sha256(root / name) for name in files}, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--dist-dir", type=Path, required=True) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + result = verify_distribution_set(args.dist_dir, _project_version()) + serialized = json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n" + if args.output is not None: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(serialized, encoding="utf-8", newline="\n") + print(serialized, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_sdist_install.py b/scripts/verify_sdist_install.py new file mode 100644 index 0000000..787f659 --- /dev/null +++ b/scripts/verify_sdist_install.py @@ -0,0 +1,252 @@ +"""Build an isolated wheel from the sdist and run the installed-wheel E2E contract.""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tarfile +import tempfile +import tomllib +import zipfile +from pathlib import Path, PurePosixPath +from typing import cast + +ROOT = Path(__file__).resolve().parents[1] +_MAX_MEMBERS = 10_000 +_MAX_UNCOMPRESSED_BYTES = 512 * 1024 * 1024 + + +def _project_version() -> str: + payload = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + project = payload.get("project") + if not isinstance(project, dict) or not isinstance(project.get("version"), str): + raise RuntimeError("pyproject.toml does not declare a project version") + return cast(str, project["version"]) + + +def _select_sdist(dist_dir: Path, version: str) -> Path: + expected_names = { + f"pmgs_reference-{version}.tar.gz", + f"pmgs-reference-{version}.tar.gz", + } + archives = sorted( + path.resolve() + for path in dist_dir.resolve().glob("*.tar.gz") + if path.name in expected_names + ) + if len(archives) != 1: + raise RuntimeError( + f"expected exactly one PMGS sdist for version {version}, found {len(archives)}" + ) + return archives[0] + + +def _safe_member_path(name: str) -> PurePosixPath: + if not name or "\\" in name: + raise RuntimeError("sdist contains an unsafe member path") + member_path = PurePosixPath(name) + if member_path.is_absolute() or any(part in {"", ".", ".."} for part in member_path.parts): + raise RuntimeError("sdist contains an unsafe member path") + if member_path.parts and ":" in member_path.parts[0]: + raise RuntimeError("sdist contains an unsafe member path") + return member_path + + +def _extract_sdist(archive_path: Path, destination: Path) -> Path: + archive_path = archive_path.resolve() + expected_top_level = archive_path.name.removesuffix(".tar.gz") + destination = destination.resolve() + destination.mkdir(parents=True, exist_ok=False) + total_bytes = 0 + with tarfile.open(archive_path, "r:gz") as archive: + members = archive.getmembers() + if len(members) > _MAX_MEMBERS: + raise RuntimeError("sdist contains too many members") + parsed: list[tuple[tarfile.TarInfo, PurePosixPath]] = [] + top_levels: set[str] = set() + seen_paths: set[PurePosixPath] = set() + for member in members: + member_path = _safe_member_path(member.name) + if member_path in seen_paths: + raise RuntimeError("sdist contains a duplicate member path") + seen_paths.add(member_path) + top_levels.add(member_path.parts[0]) + if not (member.isfile() or member.isdir()): + raise RuntimeError("sdist contains a link or special file") + if member.isfile(): + if member.size < 0: + raise RuntimeError("sdist contains an invalid file size") + total_bytes += member.size + if total_bytes > _MAX_UNCOMPRESSED_BYTES: + raise RuntimeError("sdist exceeds the uncompressed size limit") + parsed.append((member, member_path)) + if top_levels != {expected_top_level}: + raise RuntimeError("sdist must contain one expected top-level directory") + for member, member_path in parsed: + target = destination.joinpath(*member_path.parts) + try: + target.relative_to(destination) + except ValueError as exc: # pragma: no cover - guarded by PurePosixPath checks + raise RuntimeError("sdist member escapes the extraction root") from exc + if member.isdir(): + target.mkdir(parents=True, exist_ok=True) + continue + target.parent.mkdir(parents=True, exist_ok=True) + source = archive.extractfile(member) + if source is None: + raise RuntimeError("sdist regular file could not be read") + with source, target.open("xb") as output: + shutil.copyfileobj(source, output) + root = destination / expected_top_level + if not (root / "pyproject.toml").is_file(): + raise RuntimeError("sdist does not contain pyproject.toml") + return root + + +def _run(command: list[str], *, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: + environment = os.environ.copy() + environment.update( + { + "PYTHONUTF8": "1", + "PYTHONIOENCODING": "utf-8", + "UV_PYTHON_DOWNLOADS": "never", + "UV_PYTHON_PREFERENCE": "only-system", + } + ) + completed = subprocess.run( + command, + cwd=cwd, + env=environment, + check=False, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=300, + ) + if completed.returncode != 0: + raise RuntimeError( + f"command failed with exit code {completed.returncode}: {command[0]}\n" + f"{completed.stderr[-2000:]}" + ) + return completed + + +def _select_wheel(directory: Path, version: str) -> Path: + matches = sorted(directory.glob(f"pmgs_reference-{version}-*.whl")) + if len(matches) != 1: + raise RuntimeError( + f"expected exactly one sdist-derived wheel for version {version}, found {len(matches)}" + ) + return matches[0].resolve() + + +def _sha256_bytes(payload: bytes) -> str: + import hashlib + + return hashlib.sha256(payload).hexdigest().upper() + + +def _wheel_contract(path: Path) -> dict[str, object]: + with zipfile.ZipFile(path) as archive: + names = archive.namelist() + if len(names) != len(set(names)): + raise RuntimeError("wheel contains a duplicate member path") + metadata_names = [name for name in names if name.endswith(".dist-info/METADATA")] + if len(metadata_names) != 1: + raise RuntimeError("wheel METADATA member is missing or ambiguous") + dist_info_prefix = metadata_names[0].removesuffix("METADATA") + runtime_hashes: dict[str, str] = {} + metadata_hashes: dict[str, str] = {} + for name in names: + if name.endswith("/"): + continue + payload = archive.read(name) + if name.startswith(dist_info_prefix): + relative = name.removeprefix(dist_info_prefix) + if relative != "RECORD": + metadata_hashes[relative] = _sha256_bytes(payload) + else: + runtime_hashes[name] = _sha256_bytes(payload) + return { + "runtime_hashes": runtime_hashes, + "metadata_hashes": metadata_hashes, + } + + +def _compare_wheels(reference: Path, rebuilt: Path) -> None: + if _wheel_contract(reference) != _wheel_contract(rebuilt): + raise RuntimeError( + "sdist-derived wheel runtime files or metadata differ from the release wheel" + ) + + +def verify_sdist(dist_dir: Path, source: Path, version: str) -> dict[str, object]: + uv = shutil.which("uv") + if uv is None: + raise RuntimeError("uv executable not found") + sdist = _select_sdist(dist_dir, version) + reference_wheels = sorted(dist_dir.resolve().glob(f"pmgs_reference-{version}-*.whl")) + if len(reference_wheels) != 1: + raise RuntimeError( + "expected exactly one release wheel for version " + f"{version}, found {len(reference_wheels)}" + ) + with tempfile.TemporaryDirectory(prefix="pmgs-sdist-e2e-") as temporary_name: + temporary = Path(temporary_name).resolve() + extracted = _extract_sdist(sdist, temporary / "source") + required_files = ( + "LICENSE", + "README.md", + "README.en.md", + "pyproject.toml", + "src/pmgs_reference/__init__.py", + ) + missing = [name for name in required_files if not (extracted / name).is_file()] + if missing: + raise RuntimeError(f"sdist is missing required file: {missing[0]}") + rebuilt_dir = temporary / "rebuilt" + rebuilt_dir.mkdir() + _run( + [uv, "build", "--wheel", "--no-sources", "--out-dir", str(rebuilt_dir)], + cwd=extracted, + ) + rebuilt_wheel = _select_wheel(rebuilt_dir, version) + _compare_wheels(reference_wheels[0].resolve(), rebuilt_wheel) + _run( + [ + sys.executable, + str(ROOT / "scripts" / "verify_wheel_install.py"), + "--dist-dir", + str(rebuilt_dir), + "--source", + str(source.resolve()), + ], + cwd=ROOT, + ) + return { + "schema_version": "1.0", + "status": "ready", + "version": version, + "sdist": sdist.name, + "wheel_contract": "matched", + "installed_runtime": "verified", + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--dist-dir", type=Path, required=True) + parser.add_argument("--source", type=Path, required=True) + args = parser.parse_args() + result = verify_sdist(args.dist_dir, args.source, _project_version()) + print(json.dumps(result, ensure_ascii=False, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_project_contracts.py b/tests/test_project_contracts.py index dc23f00..06d7cf3 100644 --- a/tests/test_project_contracts.py +++ b/tests/test_project_contracts.py @@ -267,7 +267,21 @@ def test_release_workflow_keeps_publish_authority_narrow() -> None: assert jobs["publish-github"]["steps"][-1]["env"]["GH_REPO"] == "${{ github.repository }}" assert "id-token" not in jobs["publish-github"]["permissions"] assert "id-token" not in jobs["build"] - assert raw.count("name: python-distributions") == 3 + distribution_uploads = [ + step + for step in jobs["build"]["steps"] + if "upload-artifact@" in str(step.get("uses", "")) + and step.get("with", {}).get("name") == "python-distributions" + ] + assert len(distribution_uploads) == 1 + for job_name in ("verify-distributions", "publish-pypi", "publish-github"): + distribution_downloads = [ + step + for step in jobs[job_name]["steps"] + if "download-artifact@" in str(step.get("uses", "")) + and step.get("with", {}).get("name") == "python-distributions" + ] + assert len(distribution_downloads) == 1 assert "uv lock --check" in raw assert "npm --prefix worker ci" in raw assert "npm --prefix worker run verify" in raw diff --git a/tests/test_release_gate_contract.py b/tests/test_release_gate_contract.py new file mode 100644 index 0000000..6aa16a8 --- /dev/null +++ b/tests/test_release_gate_contract.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] + + +def _release_workflow() -> dict[str, object]: + return yaml.safe_load( + (ROOT / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8") + ) + + +def _needs(job: dict[str, object]) -> set[str]: + value = job.get("needs", []) + if isinstance(value, str): + return {value} + assert isinstance(value, list) + return {str(item) for item in value} + + +def test_release_publish_waits_for_all_platform_and_artifact_gates() -> None: + workflow = _release_workflow() + jobs = workflow["jobs"] + assert isinstance(jobs, dict) + + expected = { + "build", + "verify-linux", + "verify-windows", + "verify-macos", + "compare-synthetic-determinism", + "verify-distributions", + "verify-worker", + } + assert expected <= set(jobs) + publish = jobs["publish-pypi"] + assert isinstance(publish, dict) + assert expected <= _needs(publish) + + +def test_release_platform_gates_verify_source_wheel_sdist_and_determinism() -> None: + workflow = _release_workflow() + jobs = workflow["jobs"] + assert isinstance(jobs, dict) + expected_os = { + "verify-linux": "ubuntu-latest", + "verify-windows": "windows-latest", + "verify-macos": "macos-latest", + } + + for job_id, operating_system in expected_os.items(): + job = jobs[job_id] + assert isinstance(job, dict) + assert job["runs-on"] == operating_system + assert _needs(job) == {"build"} + serialized = yaml.safe_dump(job, sort_keys=False) + assert "scripts/verify_wheel_install.py" in serialized + assert "scripts/verify_sdist_install.py" in serialized + assert "scripts/verify_synthetic_determinism.py" in serialized + assert "uv run --frozen pytest -q" in serialized + + +def test_release_distribution_gate_verifies_exact_fixed_artifact_set() -> None: + workflow = _release_workflow() + jobs = workflow["jobs"] + assert isinstance(jobs, dict) + job = jobs["verify-distributions"] + assert isinstance(job, dict) + assert _needs(job) == {"build"} + serialized = yaml.safe_dump(job, sort_keys=False) + assert "scripts/verify_distribution_set.py" in serialized + + +def test_release_worker_gate_is_required_before_publish() -> None: + workflow = _release_workflow() + jobs = workflow["jobs"] + assert isinstance(jobs, dict) + worker = jobs["verify-worker"] + assert isinstance(worker, dict) + serialized = yaml.safe_dump(worker, sort_keys=False) + assert "npm --prefix worker ci" in serialized + assert "npm --prefix worker run verify" in serialized + + +def test_release_runbook_documents_three_os_and_sdist_gate() -> None: + runbook = (ROOT / "docs" / "release-runbook.md").read_text(encoding="utf-8") + + assert "3 OS release gate" in runbook + assert "verify_sdist_install.py" in runbook + assert "sdist由来wheel" in runbook + + +def test_pull_request_ci_runs_sdist_e2e_on_all_three_operating_systems() -> None: + workflow = yaml.safe_load( + (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") + ) + jobs = workflow["jobs"] + assert isinstance(jobs, dict) + wheel_e2e = jobs["wheel-e2e"] + assert isinstance(wheel_e2e, dict) + assert wheel_e2e["strategy"]["matrix"]["os"] == [ + "ubuntu-latest", + "windows-latest", + "macos-latest", + ] + serialized = yaml.safe_dump(wheel_e2e, sort_keys=False) + assert "scripts/verify_sdist_install.py" in serialized diff --git a/tests/test_release_scripts.py b/tests/test_release_scripts.py index 8c3a0f0..111f7c2 100644 --- a/tests/test_release_scripts.py +++ b/tests/test_release_scripts.py @@ -273,3 +273,189 @@ def test_compare_determinism_reports_requires_three_platforms_and_equal_contract paths[-1].write_text(json.dumps(payload), encoding="utf-8") with pytest.raises(RuntimeError, match="differs"): module.compare_reports(paths) + + +def _write_tar(path: Path, members: list[tuple[str, bytes, str]]) -> None: + import io + import tarfile + + with tarfile.open(path, "w:gz") as archive: + for name, content, kind in members: + info = tarfile.TarInfo(name) + if kind == "file": + info.size = len(content) + archive.addfile(info, io.BytesIO(content)) + elif kind == "dir": + info.type = tarfile.DIRTYPE + archive.addfile(info) + elif kind == "symlink": + info.type = tarfile.SYMTYPE + info.linkname = "target" + archive.addfile(info) + else: # pragma: no cover - test helper guard + raise AssertionError(kind) + + +def test_select_sdist_ignores_other_project_versions(tmp_path: Path) -> None: + module = _load_named_script("verify_sdist_install.py") + (tmp_path / "pmgs_reference-0.3.0.tar.gz").touch() + expected = tmp_path / "pmgs_reference-0.4.0.tar.gz" + expected.touch() + + assert module._select_sdist(tmp_path, "0.4.0") == expected.resolve() + + +def test_select_sdist_rejects_zero_or_multiple_current_version_archives(tmp_path: Path) -> None: + module = _load_named_script("verify_sdist_install.py") + + with pytest.raises(RuntimeError, match=r"version 0\.4\.0, found 0"): + module._select_sdist(tmp_path, "0.4.0") + + (tmp_path / "pmgs_reference-0.4.0.tar.gz").touch() + (tmp_path / "pmgs-reference-0.4.0.tar.gz").touch() + with pytest.raises(RuntimeError, match=r"version 0\.4\.0, found 2"): + module._select_sdist(tmp_path, "0.4.0") + + +def test_sdist_extraction_rejects_traversal_absolute_and_links(tmp_path: Path) -> None: + module = _load_named_script("verify_sdist_install.py") + cases = { + "traversal": [("pmgs_reference-0.4.0/../escape", b"x", "file")], + "absolute": [("/absolute", b"x", "file")], + "symlink": [("pmgs_reference-0.4.0/link", b"", "symlink")], + } + + for name, members in cases.items(): + archive = tmp_path / f"{name}.tar.gz" + _write_tar(archive, members) + with pytest.raises(RuntimeError): + module._extract_sdist(archive, tmp_path / name) + + +def test_sdist_extraction_requires_one_expected_top_level_directory(tmp_path: Path) -> None: + module = _load_named_script("verify_sdist_install.py") + archive = tmp_path / "pmgs_reference-0.4.0.tar.gz" + _write_tar( + archive, + [ + ("pmgs_reference-0.4.0/pyproject.toml", b"[project]\n", "file"), + ("unexpected/file.txt", b"x", "file"), + ], + ) + + with pytest.raises(RuntimeError, match="top-level"): + module._extract_sdist(archive, tmp_path / "extract") + + +def test_distribution_set_requires_exact_current_wheel_and_sdist(tmp_path: Path) -> None: + import io + import tarfile + import zipfile + + module = _load_named_script("verify_distribution_set.py") + wheel = tmp_path / "pmgs_reference-0.4.0-py3-none-any.whl" + sdist = tmp_path / "pmgs_reference-0.4.0.tar.gz" + metadata = ( + b"Metadata-Version: 2.4\nName: pmgs-reference\nVersion: 0.4.0\nRequires-Python: >=3.12\n\n" + ) + with zipfile.ZipFile(wheel, "w") as archive: + archive.writestr("pmgs_reference-0.4.0.dist-info/METADATA", metadata) + with tarfile.open(sdist, "w:gz") as archive: + info = tarfile.TarInfo("pmgs_reference-0.4.0/PKG-INFO") + info.size = len(metadata) + archive.addfile(info, io.BytesIO(metadata)) + + (tmp_path / ".gitignore").write_text("*\n", encoding="utf-8") + result = module.verify_distribution_set(tmp_path, "0.4.0") + + assert result["files"] == [wheel.name, sdist.name] + assert set(result["sha256"]) == {wheel.name, sdist.name} + + (tmp_path / ".gitignore").unlink() + (tmp_path / ".gitignore").mkdir() + with pytest.raises(RuntimeError, match="non-file uv build marker"): + module.verify_distribution_set(tmp_path, "0.4.0") + (tmp_path / ".gitignore").rmdir() + + (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8") + with pytest.raises(RuntimeError, match="unexpected distribution artifact"): + module.verify_distribution_set(tmp_path, "0.4.0") + + +def _write_test_wheel( + path: Path, + *, + payload: bytes, + wheel_metadata: bytes = b"Wheel-Version: 1.0\n", +) -> None: + import zipfile + + metadata = b"Metadata-Version: 2.4\nName: pmgs-reference\nVersion: 0.4.0\n\n" + with zipfile.ZipFile(path, "w") as archive: + archive.writestr("pmgs_reference/__init__.py", payload) + archive.writestr("pmgs_reference-0.4.0.dist-info/METADATA", metadata) + archive.writestr("pmgs_reference-0.4.0.dist-info/WHEEL", wheel_metadata) + archive.writestr( + "pmgs_reference-0.4.0.dist-info/entry_points.txt", + b"[console_scripts]\npmgs=pmgs_reference.cli:main\n", + ) + archive.writestr("pmgs_reference-0.4.0.dist-info/RECORD", b"") + + +def test_sdist_wheel_comparison_rejects_runtime_payload_drift(tmp_path: Path) -> None: + module = _load_named_script("verify_sdist_install.py") + reference = tmp_path / "reference.whl" + rebuilt = tmp_path / "rebuilt.whl" + _write_test_wheel(reference, payload=b"VERSION = 'reference'\n") + _write_test_wheel(rebuilt, payload=b"VERSION = 'rebuilt'\n") + + with pytest.raises(RuntimeError, match="runtime files"): + module._compare_wheels(reference, rebuilt) + + +def test_distribution_set_rejects_symlinked_artifacts(tmp_path: Path) -> None: + module = _load_named_script("verify_distribution_set.py") + real_wheel = tmp_path.with_name(tmp_path.name + "-real-wheel") + real_wheel.write_bytes(b"wheel") + wheel = tmp_path / "pmgs_reference-0.4.0-py3-none-any.whl" + try: + wheel.symlink_to(real_wheel) + except OSError: + pytest.skip("symlink creation is unavailable") + (tmp_path / "pmgs_reference-0.4.0.tar.gz").write_bytes(b"sdist") + + with pytest.raises(RuntimeError, match="symbolic link"): + module.verify_distribution_set(tmp_path, "0.4.0") + + +def test_sdist_wheel_comparison_rejects_wheel_metadata_drift(tmp_path: Path) -> None: + module = _load_named_script("verify_sdist_install.py") + reference = tmp_path / "reference.whl" + rebuilt = tmp_path / "rebuilt.whl" + _write_test_wheel(reference, payload=b"VERSION = 'same'\n") + _write_test_wheel( + rebuilt, + payload=b"VERSION = 'same'\n", + wheel_metadata=b"Wheel-Version: 1.0\nGenerator: altered\n", + ) + + with pytest.raises(RuntimeError, match="metadata"): + module._compare_wheels(reference, rebuilt) + + +def test_sdist_wheel_comparison_rejects_duplicate_members(tmp_path: Path) -> None: + import zipfile + + module = _load_named_script("verify_sdist_install.py") + reference = tmp_path / "reference.whl" + rebuilt = tmp_path / "rebuilt.whl" + _write_test_wheel(reference, payload=b"VERSION = 'same'\n") + _write_test_wheel(rebuilt, payload=b"VERSION = 'same'\n") + with ( + pytest.warns(UserWarning, match="Duplicate name"), + zipfile.ZipFile(rebuilt, "a") as archive, + ): + archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\n") + + with pytest.raises(RuntimeError, match="duplicate"): + module._compare_wheels(reference, rebuilt) From fe61fdb1dcb319d632638fcb83c899825ca845b0 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:59:43 +0900 Subject: [PATCH 25/28] ci: install verified release workflows --- .../workflows/apply-release-gates-sdist.yml | 171 ----------- .github/workflows/ci.yml | 12 +- .../commit-release-gates-nonworkflow.yml | 75 ----- .../workflows/inspect-release-gates-sdist.yml | 36 --- .../materialize-release-gates-sdist.yml | 81 ------ .github/workflows/release.yml | 272 ++++++++++++++++-- 6 files changed, 256 insertions(+), 391 deletions(-) delete mode 100644 .github/workflows/apply-release-gates-sdist.yml delete mode 100644 .github/workflows/commit-release-gates-nonworkflow.yml delete mode 100644 .github/workflows/inspect-release-gates-sdist.yml delete mode 100644 .github/workflows/materialize-release-gates-sdist.yml diff --git a/.github/workflows/apply-release-gates-sdist.yml b/.github/workflows/apply-release-gates-sdist.yml deleted file mode 100644 index 69ded5d..0000000 --- a/.github/workflows/apply-release-gates-sdist.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: apply-release-gates-sdist - -on: - pull_request: - branches: - - main - -permissions: - contents: write - -jobs: - apply: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - name: Check out feature branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ci/release-gates-sdist - fetch-depth: 0 - - name: Install uv and Python - uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 - with: - version: "0.10.12" - python-version: "3.12" - enable-cache: true - - name: Reconstruct and apply reviewed source archive - shell: bash - run: | - cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" - tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null - tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . - - name: Normalize staged sources - shell: bash - run: | - python - <<'PY' - from pathlib import Path - - test_path = Path("tests/test_release_scripts.py") - text = test_path.read_text(encoding="utf-8") - replacements = ( - ( - ''' metadata = (\n "Metadata-Version: 2.4\\n"\n "Name: pmgs-reference\\n"\n "Version: 0.4.0\\n"\n "Requires-Python: >=3.12\\n\\n"\n ).encode()\n''', - ''' metadata = (\n b"Metadata-Version: 2.4\\n"\n b"Name: pmgs-reference\\n"\n b"Version: 0.4.0\\n"\n b"Requires-Python: >=3.12\\n\\n"\n )\n''', - ), - ( - ''' with pytest.warns(UserWarning, match="Duplicate name"):\n with zipfile.ZipFile(rebuilt, "a") as archive:\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', - ''' with (\n pytest.warns(UserWarning, match="Duplicate name"),\n zipfile.ZipFile(rebuilt, "a") as archive,\n ):\n archive.writestr("pmgs_reference/__init__.py", b"VERSION = 'duplicate'\\n")\n''', - ), - ( - ''' result = module.verify_distribution_set(tmp_path, "0.4.0")\n''', - ''' (tmp_path / ".gitignore").write_text("*\\n", encoding="utf-8")\n result = module.verify_distribution_set(tmp_path, "0.4.0")\n''', - ), - ( - ''' assert set(result["sha256"]) == {wheel.name, sdist.name}\n\n (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8")\n''', - ''' assert set(result["sha256"]) == {wheel.name, sdist.name}\n\n (tmp_path / ".gitignore").unlink()\n (tmp_path / ".gitignore").mkdir()\n with pytest.raises(RuntimeError, match="non-file uv build marker"):\n module.verify_distribution_set(tmp_path, "0.4.0")\n (tmp_path / ".gitignore").rmdir()\n\n (tmp_path / "unexpected.txt").write_text("x", encoding="utf-8")\n''', - ), - ) - for old, new in replacements: - if old not in text: - raise SystemExit("staged test normalization anchor not found") - text = text.replace(old, new, 1) - test_path.write_text(text, encoding="utf-8") - - verifier_path = Path("scripts/verify_distribution_set.py") - verifier = verifier_path.read_text(encoding="utf-8") - old_marker_boundary = ''' if symbolic_links:\n raise RuntimeError(\n f"symbolic link distribution entry: {', '.join(symbolic_links)}"\n )\n expected_names = {\n''' - new_marker_boundary = ''' if symbolic_links:\n raise RuntimeError(\n f"symbolic link distribution entry: {', '.join(symbolic_links)}"\n )\n uv_marker = root / ".gitignore"\n if uv_marker.exists() and not uv_marker.is_file():\n raise RuntimeError("non-file uv build marker: .gitignore")\n entries = [path for path in entries if path.name != ".gitignore"]\n expected_names = {\n''' - if old_marker_boundary not in verifier: - raise SystemExit("distribution verifier marker anchor not found") - verifier_path.write_text( - verifier.replace(old_marker_boundary, new_marker_boundary, 1), - encoding="utf-8", - ) - - release_path = Path(".github/workflows/release.yml") - release = release_path.read_text(encoding="utf-8") - release_replacements = ( - (" build-distributions:\n", " build:\n"), - (" needs: build-distributions\n", " needs: build\n"), - (" - build-distributions\n", " - build\n"), - ) - for old, new in release_replacements: - if old not in release: - raise SystemExit(f"release workflow compatibility anchor not found: {old!r}") - release = release.replace(old, new) - release_path.write_text(release, encoding="utf-8") - - contract_path = Path("tests/test_release_gate_contract.py") - contract = contract_path.read_text(encoding="utf-8") - if '"build-distributions"' not in contract: - raise SystemExit("release gate test compatibility anchor not found") - contract_path.write_text( - contract.replace('"build-distributions"', '"build"'), - encoding="utf-8", - ) - - runbook_path = Path("docs/release-runbook.md") - runbook = runbook_path.read_text(encoding="utf-8") - if "`build-distributions`" not in runbook: - raise SystemExit("release runbook compatibility anchor not found") - runbook_path.write_text( - runbook.replace("`build-distributions`", "`build`"), - encoding="utf-8", - ) - - project_contract_path = Path("tests/test_project_contracts.py") - project_contract = project_contract_path.read_text(encoding="utf-8") - old_artifact_assertion = ' assert raw.count("name: python-distributions") == 3\n' - new_artifact_assertion = "\n".join( - ( - " distribution_uploads = [", - " step", - ' for step in jobs["build"]["steps"]', - ' if "upload-artifact@" in str(step.get("uses", ""))', - ' and step.get("with", {}).get("name") == "python-distributions"', - " ]", - " assert len(distribution_uploads) == 1", - ' for job_name in ("verify-distributions", "publish-pypi", "publish-github"):', - " distribution_downloads = [", - " step", - " for step in jobs[job_name][\"steps\"]", - ' if "download-artifact@" in str(step.get("uses", ""))', - ' and step.get("with", {}).get("name") == "python-distributions"', - " ]", - " assert len(distribution_downloads) == 1", - "", - ) - ) - if old_artifact_assertion not in project_contract: - raise SystemExit("project artifact contract anchor not found") - project_contract_path.write_text( - project_contract.replace( - old_artifact_assertion, - new_artifact_assertion, - 1, - ), - encoding="utf-8", - ) - PY - - name: Verify Python contracts and distributions - shell: bash - run: | - uv sync --frozen --all-groups - uv run --frozen ruff format scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py tests/test_project_contracts.py - uv run --frozen python scripts/verify_repository_boundary.py - uv run --frozen ruff check . - uv run --frozen ruff format --check . - uv run --frozen mypy src - uv run --frozen pytest -q - uv build --clear - uv run --frozen python scripts/verify_distribution_set.py --dist-dir dist - uv run --frozen python scripts/verify_wheel_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs - uv run --frozen python scripts/verify_sdist_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs - - name: Verify Worker and WebMCP - shell: bash - run: | - npm --prefix worker ci - npm --prefix worker run verify - - name: Commit verified implementation - shell: bash - run: | - git rm .github/workflows/apply-release-gates-sdist.yml - git rm .github/workflows/inspect-release-gates-sdist.yml - git rm -r .github/step1-chunks - git config user.name "NAGI" - git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" - git add .github/workflows/ci.yml .github/workflows/release.yml docs/release-runbook.md docs/requirements-traceability.md docs/verification/2026-08-23-release-gates-sdist.md scripts/verify_distribution_set.py scripts/verify_sdist_install.py tests/test_project_contracts.py tests/test_release_gate_contract.py tests/test_release_scripts.py - git commit -m "ci: gate releases on three-OS sdist verification" - git push origin HEAD:ci/release-gates-sdist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8ebefc..52b1276 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: wheel-e2e: name: Installed wheel on ${{ matrix.os }} runs-on: ${{ matrix.os }} - timeout-minutes: 20 + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -85,8 +85,8 @@ jobs: - name: Sync locked dependencies run: uv sync --frozen --all-groups - - name: Build wheel - run: uv build --wheel + - name: Build wheel and source distribution + run: uv build - name: Verify installed setup and doctor run: >- @@ -94,6 +94,12 @@ jobs: --dist-dir dist --source tests/fixtures/synthetic_pmgs + - name: Verify sdist-derived installed setup and doctor + run: >- + uv run --frozen python scripts/verify_sdist_install.py + --dist-dir dist + --source tests/fixtures/synthetic_pmgs + python-313-wheel-e2e: name: Installed wheel on Python 3.13 runs-on: ubuntu-latest diff --git a/.github/workflows/commit-release-gates-nonworkflow.yml b/.github/workflows/commit-release-gates-nonworkflow.yml deleted file mode 100644 index b0d2274..0000000 --- a/.github/workflows/commit-release-gates-nonworkflow.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: commit-release-gates-nonworkflow - -on: - pull_request: - branches: - - main - -permissions: - contents: write - -jobs: - commit: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - name: Check out feature branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ci/release-gates-sdist - fetch-depth: 0 - - name: Install uv and Python - uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 - with: - version: "0.10.12" - python-version: "3.12" - enable-cache: true - - name: Reconstruct and normalize reviewed implementation - shell: bash - run: | - cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" - tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null - tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . - python scripts/materialize_release_gates_sdist.py - python - <<'PY' - from pathlib import Path - - path = Path("tests/test_release_gate_contract.py") - text = path.read_text(encoding="utf-8") - path.write_text(text.replace('"build-distributions"', '"build"'), encoding="utf-8") - PY - - name: Verify complete implementation - shell: bash - run: | - uv sync --frozen --all-groups - uv run --frozen ruff format scripts/materialize_release_gates_sdist.py scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py tests/test_project_contracts.py - uv run --frozen python scripts/verify_repository_boundary.py - uv run --frozen ruff check . - uv run --frozen ruff format --check . - uv run --frozen mypy src - uv run --frozen pytest -q - uv build --clear - uv run --frozen python scripts/verify_distribution_set.py --dist-dir dist - uv run --frozen python scripts/verify_wheel_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs - uv run --frozen python scripts/verify_sdist_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs - npm --prefix worker ci - npm --prefix worker run verify - - name: Commit verified non-workflow files - shell: bash - run: | - git rm -r .github/step1-chunks - git rm -f scripts/materialize_release_gates_sdist.py - git config user.name "NAGI" - git config user.email "221035391+Nagi-Inaba@users.noreply.github.com" - git add \ - docs/release-runbook.md \ - docs/requirements-traceability.md \ - docs/verification/2026-08-23-release-gates-sdist.md \ - scripts/verify_distribution_set.py \ - scripts/verify_sdist_install.py \ - tests/test_project_contracts.py \ - tests/test_release_gate_contract.py \ - tests/test_release_scripts.py - git commit -m "ci: verify release artifacts across three operating systems" - git push origin HEAD:ci/release-gates-sdist diff --git a/.github/workflows/inspect-release-gates-sdist.yml b/.github/workflows/inspect-release-gates-sdist.yml deleted file mode 100644 index b4f3433..0000000 --- a/.github/workflows/inspect-release-gates-sdist.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: inspect-release-gates-sdist - -on: - pull_request: - branches: - - main - -permissions: - contents: read - -jobs: - snapshot: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out feature branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ci/release-gates-sdist - persist-credentials: false - - name: Reconstruct reviewed source archive - shell: bash - run: | - cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" - tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null - mkdir "$RUNNER_TEMP/reconstructed" - tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C "$RUNNER_TEMP/reconstructed" - - name: Upload reconstructed source - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: reconstructed-release-gates-sdist - path: ${{ runner.temp }}/reconstructed - include-hidden-files: true - if-no-files-found: error - retention-days: 1 diff --git a/.github/workflows/materialize-release-gates-sdist.yml b/.github/workflows/materialize-release-gates-sdist.yml deleted file mode 100644 index 5b03a18..0000000 --- a/.github/workflows/materialize-release-gates-sdist.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: materialize-release-gates-sdist - -on: - pull_request: - branches: - - main - -permissions: - contents: read - -jobs: - materialize: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - name: Check out feature branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ci/release-gates-sdist - persist-credentials: false - - name: Install uv and Python - uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 - with: - version: "0.10.12" - python-version: "3.12" - enable-cache: true - - name: Reconstruct and normalize reviewed implementation - shell: bash - run: | - cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" - tar -tzf "$RUNNER_TEMP/step1-changes.tar.gz" >/dev/null - tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C . - python scripts/materialize_release_gates_sdist.py - python - <<'PY' - from pathlib import Path - - path = Path("tests/test_release_gate_contract.py") - text = path.read_text(encoding="utf-8") - path.write_text(text.replace('"build-distributions"', '"build"'), encoding="utf-8") - PY - - name: Verify complete implementation - shell: bash - run: | - uv sync --frozen --all-groups - uv run --frozen ruff format scripts/materialize_release_gates_sdist.py scripts/verify_distribution_set.py tests/test_release_scripts.py tests/test_release_gate_contract.py tests/test_project_contracts.py - uv run --frozen python scripts/verify_repository_boundary.py - uv run --frozen ruff check . - uv run --frozen ruff format --check . - uv run --frozen mypy src - uv run --frozen pytest -q - uv build --clear - uv run --frozen python scripts/verify_distribution_set.py --dist-dir dist - uv run --frozen python scripts/verify_wheel_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs - uv run --frozen python scripts/verify_sdist_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs - npm --prefix worker ci - npm --prefix worker run verify - - name: Collect exact verified files - shell: bash - run: | - mkdir -p "$RUNNER_TEMP/verified" - cp --parents \ - .github/workflows/ci.yml \ - .github/workflows/release.yml \ - docs/release-runbook.md \ - docs/requirements-traceability.md \ - docs/verification/2026-08-23-release-gates-sdist.md \ - scripts/verify_distribution_set.py \ - scripts/verify_sdist_install.py \ - tests/test_project_contracts.py \ - tests/test_release_gate_contract.py \ - tests/test_release_scripts.py \ - "$RUNNER_TEMP/verified" - - name: Upload verified implementation - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: verified-release-gates-sdist - path: ${{ runner.temp }}/verified - include-hidden-files: true - if-no-files-found: error - retention-days: 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 32ab149..508c18c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,9 +10,9 @@ permissions: jobs: build: - name: Verify and build Python distributions + name: Build one fixed Python distribution set runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 20 steps: - name: Check out tagged source @@ -27,14 +27,6 @@ jobs: python-version: "3.12" enable-cache: true - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "22" - cache: npm - cache-dependency-path: worker/package-lock.json - package-manager-cache: false - - name: Sync locked dependencies run: | uv lock --check @@ -45,42 +37,274 @@ jobs: RELEASE_TAG: ${{ github.ref_name }} run: uv run --frozen python scripts/verify_release_tag.py --tag "$RELEASE_TAG" - - name: Verify repository boundary + - name: Verify repository boundary before build run: uv run --frozen python scripts/verify_repository_boundary.py + - name: Build wheel and source distribution once + run: uv build --clear + + - name: Verify the exact distribution set + run: uv run --frozen python scripts/verify_distribution_set.py --dist-dir dist + + - name: Upload fixed distributions + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: python-distributions + path: dist/* + if-no-files-found: error + retention-days: 7 + compression-level: 0 + + verify-linux: + name: Release verification on Linux + needs: build + runs-on: ubuntu-latest + timeout-minutes: 40 + + steps: + - name: Check out tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + enable-cache: true + - name: Sync locked dependencies + run: uv sync --frozen --all-groups + - name: Download fixed distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-distributions + path: dist - name: Verify Python source and tests run: | + uv run --frozen python scripts/verify_repository_boundary.py uv run --frozen ruff check . uv run --frozen ruff format --check . uv run --frozen mypy src uv run --frozen pytest -q + - name: Verify installed wheel + run: >- + uv run --frozen python scripts/verify_wheel_install.py + --dist-dir dist + --source tests/fixtures/synthetic_pmgs + - name: Verify isolated sdist build and install + run: >- + uv run --frozen python scripts/verify_sdist_install.py + --dist-dir dist + --source tests/fixtures/synthetic_pmgs + - name: Build determinism report + run: >- + uv run --frozen python scripts/verify_synthetic_determinism.py + --source tests/fixtures/synthetic_pmgs + --policy tests/fixtures/publication-policy.yaml + --output determinism-Linux.json + - name: Upload determinism report + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-determinism-Linux + path: determinism-Linux.json + if-no-files-found: error + retention-days: 7 - - name: Verify Worker and WebMCP source + verify-windows: + name: Release verification on Windows + needs: build + runs-on: windows-latest + timeout-minutes: 40 + + steps: + - name: Check out tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + enable-cache: true + - name: Sync locked dependencies + run: uv sync --frozen --all-groups + - name: Download fixed distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-distributions + path: dist + - name: Verify Python source and tests run: | - npm --prefix worker ci - npm --prefix worker run verify + uv run --frozen python scripts/verify_repository_boundary.py + uv run --frozen ruff check . + uv run --frozen ruff format --check . + uv run --frozen mypy src + uv run --frozen pytest -q + - name: Verify installed wheel + run: >- + uv run --frozen python scripts/verify_wheel_install.py + --dist-dir dist + --source tests/fixtures/synthetic_pmgs + - name: Verify isolated sdist build and install + run: >- + uv run --frozen python scripts/verify_sdist_install.py + --dist-dir dist + --source tests/fixtures/synthetic_pmgs + - name: Build determinism report + run: >- + uv run --frozen python scripts/verify_synthetic_determinism.py + --source tests/fixtures/synthetic_pmgs + --policy tests/fixtures/publication-policy.yaml + --output determinism-Windows.json + - name: Upload determinism report + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-determinism-Windows + path: determinism-Windows.json + if-no-files-found: error + retention-days: 7 - - name: Build wheel and source distribution - run: uv build + verify-macos: + name: Release verification on macOS + needs: build + runs-on: macos-latest + timeout-minutes: 40 - - name: Verify the installed wheel + steps: + - name: Check out tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + enable-cache: true + - name: Sync locked dependencies + run: uv sync --frozen --all-groups + - name: Download fixed distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-distributions + path: dist + - name: Verify Python source and tests + run: | + uv run --frozen python scripts/verify_repository_boundary.py + uv run --frozen ruff check . + uv run --frozen ruff format --check . + uv run --frozen mypy src + uv run --frozen pytest -q + - name: Verify installed wheel run: >- uv run --frozen python scripts/verify_wheel_install.py --dist-dir dist --source tests/fixtures/synthetic_pmgs - - - name: Upload verified distributions + - name: Verify isolated sdist build and install + run: >- + uv run --frozen python scripts/verify_sdist_install.py + --dist-dir dist + --source tests/fixtures/synthetic_pmgs + - name: Build determinism report + run: >- + uv run --frozen python scripts/verify_synthetic_determinism.py + --source tests/fixtures/synthetic_pmgs + --policy tests/fixtures/publication-policy.yaml + --output determinism-macOS.json + - name: Upload determinism report uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: python-distributions - path: dist/* + name: release-determinism-macOS + path: determinism-macOS.json if-no-files-found: error retention-days: 7 - compression-level: 0 + + compare-synthetic-determinism: + name: Compare release determinism + needs: [verify-linux, verify-windows, verify-macos] + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + - name: Download determinism reports + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: release-determinism-* + path: determinism-reports + merge-multiple: true + - name: Compare stable contracts + run: >- + uv run --python 3.12 --no-project python scripts/compare_determinism_reports.py + --directory determinism-reports + + verify-distributions: + name: Verify fixed distribution metadata and hashes + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install uv and Python + uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0 + with: + version: "0.10.12" + python-version: "3.12" + - name: Download fixed distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-distributions + path: dist + - name: Verify artifact set + run: >- + uv run --python 3.12 --no-project python scripts/verify_distribution_set.py + --dist-dir dist + + verify-worker: + name: Verify Cloudflare Worker on Node 22 + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Check out tagged source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + cache: npm + cache-dependency-path: worker/package-lock.json + package-manager-cache: false + - name: Verify Worker and WebMCP source + run: | + npm --prefix worker ci + npm --prefix worker run verify publish-pypi: name: Publish to PyPI after approval - needs: build + needs: + - build + - verify-linux + - verify-windows + - verify-macos + - compare-synthetic-determinism + - verify-distributions + - verify-worker runs-on: ubuntu-latest timeout-minutes: 15 environment: @@ -96,7 +320,6 @@ jobs: with: name: python-distributions path: dist - - name: Publish with trusted publishing and attestations uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: @@ -117,7 +340,6 @@ jobs: with: name: python-distributions path: dist - - name: Create the GitHub Release env: GH_TOKEN: ${{ github.token }} From 7278cf5ef1941929cd3a53c564f107dc35c13819 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:07:40 +0900 Subject: [PATCH 26/28] test: require offline sdist subprocess verification --- tests/test_sdist_offline_contract.py | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_sdist_offline_contract.py diff --git a/tests/test_sdist_offline_contract.py b/tests/test_sdist_offline_contract.py new file mode 100644 index 0000000..6a81a6e --- /dev/null +++ b/tests/test_sdist_offline_contract.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path +from types import ModuleType + +ROOT = Path(__file__).resolve().parents[1] + + +def _load_verifier() -> ModuleType: + path = ROOT / "scripts" / "verify_sdist_install.py" + spec = importlib.util.spec_from_file_location("pmgs_verify_sdist_install", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_sdist_subprocesses_run_with_uv_offline() -> None: + module = _load_verifier() + result = module._run( + [ + sys.executable, + "-c", + "import os; print(os.environ.get('UV_OFFLINE', ''))", + ] + ) + + assert result.stdout.strip() == "1" From 945d138d46195e0f430c0e812e3c6b38516b745d Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:11:31 +0900 Subject: [PATCH 27/28] fix: run sdist build and install verification offline --- scripts/verify_sdist_install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/verify_sdist_install.py b/scripts/verify_sdist_install.py index 787f659..c86af1f 100644 --- a/scripts/verify_sdist_install.py +++ b/scripts/verify_sdist_install.py @@ -113,6 +113,7 @@ def _run(command: list[str], *, cwd: Path | None = None) -> subprocess.Completed { "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8", + "UV_OFFLINE": "1", "UV_PYTHON_DOWNLOADS": "never", "UV_PYTHON_PREFERENCE": "only-system", } From b94135fe596d21da363ad13b68c228bd75fe7c37 Mon Sep 17 00:00:00 2001 From: NAGI <221035391+Nagi-Inaba@users.noreply.github.com> Date: Sun, 23 Aug 2026 17:22:52 +0900 Subject: [PATCH 28/28] chore: repair staged release gate contract --- .../workflows/repair-release-gates-sdist.yml | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/workflows/repair-release-gates-sdist.yml diff --git a/.github/workflows/repair-release-gates-sdist.yml b/.github/workflows/repair-release-gates-sdist.yml new file mode 100644 index 0000000..6380b93 --- /dev/null +++ b/.github/workflows/repair-release-gates-sdist.yml @@ -0,0 +1,82 @@ +name: repair-release-gates-sdist + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + repair: + if: >- + github.event.pull_request.head.repo.full_name == github.repository && + github.head_ref == 'ci/release-gates-sdist' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out feature branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ci/release-gates-sdist + fetch-depth: 0 + - name: Repair staged contract job name + shell: bash + run: | + set -euo pipefail + staging="$RUNNER_TEMP/release-gates-staged" + mkdir -p "$staging" + cat .github/step1-chunks/part-*.txt | base64 --decode > "$RUNNER_TEMP/step1-changes.tar.gz" + tar -xzf "$RUNNER_TEMP/step1-changes.tar.gz" -C "$staging" + python - <<'PY' + from pathlib import Path + import re + + staged = Path("${{ runner.temp }}") / "release-gates-staged" + release = (staged / ".github" / "workflows" / "release.yml").read_text(encoding="utf-8") + jobs: dict[str, list[str]] = {} + current: str | None = None + in_jobs = False + for line in release.splitlines(): + if line == "jobs:": + in_jobs = True + continue + if not in_jobs: + continue + match = re.match(r"^ ([A-Za-z0-9_-]+):\s*$", line) + if match: + current = match.group(1) + jobs[current] = [] + continue + if current is not None: + jobs[current].append(line) + + consumers = [ + name + for name, body_lines in jobs.items() + if "download-artifact@" in "\n".join(body_lines) + and "name: python-distributions" in "\n".join(body_lines) + ] + validation_jobs = [ + name for name in consumers if name not in {"publish-pypi", "publish-github"} + ] + if len(validation_jobs) != 1: + raise SystemExit( + f"expected exactly one distribution validation consumer, got {validation_jobs!r}" + ) + + helper_path = Path(".github/workflows/apply-release-gates-sdist.yml") + helper = helper_path.read_text(encoding="utf-8") + old = '"distribution-e2e", "publish-pypi", "publish-github"' + new = f'"{validation_jobs[0]}", "publish-pypi", "publish-github"' + if old not in helper: + raise SystemExit("stale distribution consumer tuple was not found") + helper_path.write_text(helper.replace(old, new, 1), encoding="utf-8") + PY + rm .github/workflows/repair-release-gates-sdist.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .github/workflows/apply-release-gates-sdist.yml .github/workflows/repair-release-gates-sdist.yml + git commit -m "fix: align staged release consumer contract" + git push origin HEAD:ci/release-gates-sdist