From ae9237ce7561d8c3f95219a43e535c7e9fb61da0 Mon Sep 17 00:00:00 2001 From: ShutenOishi Date: Fri, 23 Oct 2026 00:24:16 +0900 Subject: [PATCH 1/5] ci: reuse central verified main-push candidate audit --- .github/workflows/update-watch.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/update-watch.yml b/.github/workflows/update-watch.yml index 8b7e2da..6c1d8f2 100644 --- a/.github/workflows/update-watch.yml +++ b/.github/workflows/update-watch.yml @@ -4,6 +4,10 @@ name: Upstream candidate after verified central daily fleet scan on: schedule: - cron: "30 3 * * *" + push: + branches: [main] + paths: + - "config/extension.json" workflow_run: workflows: ["Windows CI/CD"] types: [completed] @@ -70,3 +74,16 @@ jobs: uses: pgextwin/build/.github/workflows/verify-native-pr-candidate.yml@cb06156e897a0b8f0609d4320ab619ac6c5c29a8 with: ci_run_id: ${{ github.event.workflow_run.id }} + + candidate_main_merge_observation: + name: Authenticate main merge and scheduled decision (NONPUBLISHING) + if: >- + ${{ github.event_name == 'push' && + github.ref == 'refs/heads/main' && + github.event.created == false && + github.event.deleted == false }} + permissions: + contents: read + actions: read + pull-requests: read + uses: pgextwin/build/.github/workflows/automatic-postmerge-audit-readonly.yml@8a41cedf34108bbb9b07a24ff4ddde42d9621b97 From c4cebde6b72a3f949e6f58c1a7507a85081aac32 Mon Sep 17 00:00:00 2001 From: ShutenOishi Date: Fri, 23 Oct 2026 00:32:53 +0900 Subject: [PATCH 2/5] test: admit exact read-only candidate main push, retain negative event/privilege checks --- tests/test_on_demand_update_workflow.py | 38 ++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/test_on_demand_update_workflow.py b/tests/test_on_demand_update_workflow.py index e7727ea..a3c492c 100644 --- a/tests/test_on_demand_update_workflow.py +++ b/tests/test_on_demand_update_workflow.py @@ -40,7 +40,7 @@ def verify(src): if src.count("needs.authorize.outputs.mode == 'candidate'") != 2: raise ValueError("both scoped candidate jobs must depend on candidate mode") for forbidden in ( - "\n workflow_dispatch:", "\n push:", "\n pull_request:", + "\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", @@ -49,6 +49,37 @@ def verify(src): ): if forbidden in src: raise ValueError("Unexpected human UPDATE, event, publication or credential path") + # Read-only candidate main observation is allowed ONLY for manifest merges. + # This must never change the original scheduled candidate/UPDATE contracts. + expected_push = """ push: + branches: [main] + paths: + - "config/extension.json" +""" + if expected_push not in src: + raise ValueError("exact protected-main manifest-only push filter missing") + required_observer = ( + " candidate_main_merge_observation:\n", + "github.event_name == 'push'", + "github.ref == 'refs/heads/main'", + "github.event.created == false", + "github.event.deleted == false", + " actions: read", + " pull-requests: read", + "uses: pgextwin/build/.github/workflows/automatic-postmerge-audit-readonly.yml@" + + "8a41cedf34108bbb9b07a24ff4ddde42d9621b97", + ) + 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] + for prohibited in ("contents: write", "actions: write", "issues: write", + "secrets.", "GH_TOKEN:", "workflow_dispatch", "PUBLISH", + "gh release ", "ref: main", "persist-credentials: true"): + if prohibited in observer: + raise ValueError("unsafe observer privilege or mutable Release path") 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): @@ -61,6 +92,11 @@ def main(): src = PATH.read_text(encoding="utf-8") verify(src) mutants = ( + (' branches: [main]', ' branches: [develop]'), + (' - "config/extension.json"', ' - ".github/workflows/update-watch.yml"'), + ('automatic-postmerge-audit-readonly.yml@8a41cedf34108bbb9b07a24ff4ddde42d9621b97', + 'automatic-postmerge-audit-readonly.yml@main'), + (' pull-requests: read', ' pull-requests: write'), ('cron: "30 3 * * *"', 'cron: "* * * * *"'), ('operator-update-auto.yml@' + SHARED, 'operator-update-auto.yml@main'), ('operator-update-auto.yml@' + SHARED, From 94a701368e0b6e40ed3069fc39c2d1edf2cc1487 Mon Sep 17 00:00:00 2001 From: ShutenOishi Date: Fri, 23 Oct 2026 00:53:53 +0900 Subject: [PATCH 3/5] test: prohibit actual PUBLISH input, not benign NONPUBLISHING audit title --- 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 a3c492c..25922a8 100644 --- a/tests/test_on_demand_update_workflow.py +++ b/tests/test_on_demand_update_workflow.py @@ -76,7 +76,7 @@ def verify(src): raise ValueError("missing pinned read-only main observer " + marker) observer = src.split(" candidate_main_merge_observation:\n", 1)[1] for prohibited in ("contents: write", "actions: write", "issues: write", - "secrets.", "GH_TOKEN:", "workflow_dispatch", "PUBLISH", + "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") From a6e66765bd32977a2114f44fcf7f31d7b2a47eef Mon Sep 17 00:00:00 2001 From: ShutenOishi Date: Fri, 23 Oct 2026 00:58:39 +0900 Subject: [PATCH 4/5] test: scope deny mutation to main-push observer instead of independent native PR audit --- tests/test_on_demand_update_workflow.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_on_demand_update_workflow.py b/tests/test_on_demand_update_workflow.py index 25922a8..8fabca4 100644 --- a/tests/test_on_demand_update_workflow.py +++ b/tests/test_on_demand_update_workflow.py @@ -120,7 +120,12 @@ def main(): if before not in src: raise AssertionError("missing hostile fixture " + before[:60]) try: - verify(src.replace(before, after, 1)) + # The native PR audit also legitimately has pull-requests: read. + # Mutate the final, exact main-push observer instead of that sibling. + altered = (src.rsplit(before, 1)[0] + after + src.rsplit(before, 1)[1] + if before == ' pull-requests: read' + else src.replace(before, after, 1)) + verify(altered) except ValueError: continue raise AssertionError("unsafe scheduled candidate mutation accepted: " + before[:60]) From 1022acc274546037a97755f0195725d4be604539 Mon Sep 17 00:00:00 2001 From: ShutenOishi Date: Fri, 23 Oct 2026 01:03:59 +0900 Subject: [PATCH 5/5] test: explicitly reject pull-request write permissions on main-push observer --- tests/test_on_demand_update_workflow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_on_demand_update_workflow.py b/tests/test_on_demand_update_workflow.py index 8fabca4..92b3bf8 100644 --- a/tests/test_on_demand_update_workflow.py +++ b/tests/test_on_demand_update_workflow.py @@ -76,6 +76,7 @@ def verify(src): raise ValueError("missing pinned read-only main observer " + marker) observer = src.split(" candidate_main_merge_observation:\n", 1)[1] 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: