From 292c0fe4cbf17b2bed2869dd67692898561a1ccd Mon Sep 17 00:00:00 2001 From: ShutenOishi Date: Fri, 23 Oct 2026 08:13:08 +0900 Subject: [PATCH 1/3] ci: follow true verified candidate intent into existing attested signer without publishing --- .github/workflows/update-watch.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-watch.yml b/.github/workflows/update-watch.yml index 6c1d8f2..ffba4fb 100644 --- a/.github/workflows/update-watch.yml +++ b/.github/workflows/update-watch.yml @@ -86,4 +86,22 @@ jobs: contents: read actions: read pull-requests: read - uses: pgextwin/build/.github/workflows/automatic-postmerge-audit-readonly.yml@8a41cedf34108bbb9b07a24ff4ddde42d9621b97 + uses: pgextwin/build/.github/workflows/automatic-postmerge-audit-readonly.yml@e81d9f7b38a949d5b220ab16228ff206a8020992 + + candidate_attested_build: + name: Attest original candidate after verified protected main merge (NO PUBLISH) + needs: candidate_main_merge_observation + if: >- + ${{ github.event_name == 'push' && + github.ref == 'refs/heads/main' && + needs.candidate_main_merge_observation.result == 'success' && + needs.candidate_main_merge_observation.outputs.verified_signed_build_intent == 'true' }} + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + uses: pgextwin/build/.github/workflows/build-extension-attested.yml@5756b3c5136a8bf4db710016cfe97555f0a71c6d + with: + extension_config_path: config/extension.json + test_contract_path: config/test-contract.json From c142bf25294c7159a0fc3711e73d419ecef7b21b Mon Sep 17 00:00:00 2001 From: ShutenOishi Date: Fri, 23 Oct 2026 08:13:10 +0900 Subject: [PATCH 2/3] test: bound authenticated attested builder without allowing publication --- tests/test_on_demand_update_workflow.py | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/test_on_demand_update_workflow.py b/tests/test_on_demand_update_workflow.py index 92b3bf8..175a828 100644 --- a/tests/test_on_demand_update_workflow.py +++ b/tests/test_on_demand_update_workflow.py @@ -67,20 +67,44 @@ def verify(src): " actions: read", " pull-requests: read", "uses: pgextwin/build/.github/workflows/automatic-postmerge-audit-readonly.yml@" - + "8a41cedf34108bbb9b07a24ff4ddde42d9621b97", + + "e81d9f7b38a949d5b220ab16228ff206a8020992", ) if src.count(" push:\n") != 1 or src.count(" candidate_main_merge_observation:\n") != 1: raise ValueError("duplicate or ambiguous read-only observer") for marker in required_observer: if marker not in src: raise ValueError("missing pinned read-only main observer " + marker) - observer = src.split(" candidate_main_merge_observation:\n", 1)[1] + observer = src.split(" candidate_main_merge_observation:\n", 1)[1].split("\n candidate_attested_build:\n", 1)[0] for prohibited in ("contents: write", "actions: write", "issues: write", "pull-requests: write", "secrets.", "GH_TOKEN:", "workflow_dispatch", "confirm_release", "gh release ", "ref: main", "persist-credentials: true"): if prohibited in observer: raise ValueError("unsafe observer privilege or mutable Release path") + # The attested job is distinct from the read-only observer. It may attest, + # but it can never mint a public Release, tag or privilege escalation. + if src.count("\n candidate_attested_build:\n") != 1: + raise ValueError("exactly one shared signer job required") + signer = src.split("\n candidate_attested_build:\n", 1)[1] + signed_required = ( + " needs: candidate_main_merge_observation", + "github.event_name == 'push'", + "github.ref == 'refs/heads/main'", + "needs.candidate_main_merge_observation.result == 'success'", + "needs.candidate_main_merge_observation.outputs.verified_signed_build_intent == 'true'", + " permissions:\n contents: read\n id-token: write\n attestations: write\n artifact-metadata: write", + "uses: pgextwin/build/.github/workflows/build-extension-attested.yml@5756b3c5136a8bf4db710016cfe97555f0a71c6d", + "extension_config_path: config/extension.json", + "test_contract_path: config/test-contract.json", + ) + for marker in signed_required: + if marker not in signer: + raise ValueError("signed-build gate or immutable source changed: " + marker) + for forbidden in ("contents: write", "actions: write", "pull-requests: write", + "secrets.", "workflow_dispatch", "gh release ", "confirm_release", + "publish-release", "catalog", "@main"): + if forbidden in signer: + raise ValueError("signed-build job gained publication or extraneous permissions") watch = src.split(" watch_candidate:\n", 1)[1].split(" candidate:\n", 1)[0] candidate = src.split(" candidate:\n", 1)[1] if not re.search(r"(?m)^ permissions:\n contents: read\n issues: write$", watch): @@ -93,9 +117,13 @@ def main(): src = PATH.read_text(encoding="utf-8") verify(src) mutants = ( + ("needs.candidate_main_merge_observation.outputs.verified_signed_build_intent == 'true'", + "needs.candidate_main_merge_observation.outputs.verified_signed_build_intent == 'false'"), + ("build-extension-attested.yml@5756b3c5136a8bf4db710016cfe97555f0a71c6d", "build-extension-attested.yml@main"), + (" attestations: write", " attestations: read"), (' branches: [main]', ' branches: [develop]'), (' - "config/extension.json"', ' - ".github/workflows/update-watch.yml"'), - ('automatic-postmerge-audit-readonly.yml@8a41cedf34108bbb9b07a24ff4ddde42d9621b97', + ('automatic-postmerge-audit-readonly.yml@e81d9f7b38a949d5b220ab16228ff206a8020992', 'automatic-postmerge-audit-readonly.yml@main'), (' pull-requests: read', ' pull-requests: write'), ('cron: "30 3 * * *"', 'cron: "* * * * *"'), From 1628603fa6dffa22f3556b77cf8bb6abd2fe1aae Mon Sep 17 00:00:00 2001 From: ShutenOishi Date: Fri, 23 Oct 2026 08:13:35 +0900 Subject: [PATCH 3/3] test: permit only guarded immutable signer, continue to prohibit PUBLISH --- tests/test_on_demand_update_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_on_demand_update_workflow.py b/tests/test_on_demand_update_workflow.py index 175a828..4d494ed 100644 --- a/tests/test_on_demand_update_workflow.py +++ b/tests/test_on_demand_update_workflow.py @@ -43,7 +43,7 @@ def verify(src): "\n workflow_dispatch:", "\n pull_request:", "\n repository_dispatch:", "\n workflow_call:", "confirm_update", "expected_main_sha", "ShutenOishi", - "gh release ", "confirm_release=PUBLISH", "build-extension-attested.yml", + "gh release ", "confirm_release=PUBLISH", "operator-update-readonly.yml", "GH_TOKEN:", "secrets.", "persist-credentials: true", "watch_readonly:", ):