From 986dff52b71432644f792552a4ba2d4b0aed9a5b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 23:42:39 -0700 Subject: [PATCH 1/3] test: gh-merge-green merges a repo whose ci.yml has no ci-status job (red) Co-Authored-By: Claude Opus 5.5 --- tests/test_gh_merge_green.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/test_gh_merge_green.py b/tests/test_gh_merge_green.py index 8e37eb7babbd..5294e3d1e4b8 100644 --- a/tests/test_gh_merge_green.py +++ b/tests/test_gh_merge_green.py @@ -414,14 +414,15 @@ def test_large_file_and_patch_lists_do_not_trip_pipefail(self): class WorkflowPresenceRegression(unittest.TestCase): """Repositories without the aggregate workflow use all exact-head verdicts.""" - def run_case(self, *, workflow=False, probe_status=404, checks=None, statuses=None, app_workflow=False, files=None): + def run_case(self, *, workflow=False, probe_status=404, checks=None, statuses=None, app_workflow=False, files=None, workflow_body=None): with tempfile.TemporaryDirectory() as directory: directory = Path(directory) marker = directory / "merged" queries = directory / "queries" payload = {"head": HEAD, "workflow": workflow, "probe_status": probe_status, "checks": checks if checks is not None else [{"id": 1, "name": "tests", "status": "completed", "conclusion": "success"}], - "statuses": statuses or [], "app_workflow": app_workflow, "files": files or []} + "statuses": statuses or [], "app_workflow": app_workflow, "files": files or [], + "workflow_body": workflow_body} fixture = directory / "fixture.json" fixture.write_text(__import__("json").dumps(payload)) gh = directory / "gh" @@ -440,7 +441,8 @@ def run_case(self, *, workflow=False, probe_status=404, checks=None, statuses=No code = 200 if present else x['probe_status'] print('HTTP/2.0 ' + str(code)) print() - print('{}') + body = x['workflow_body'] + print(json.dumps({'content': __import__('base64').b64encode(body.encode()).decode()}) if body is not None else '{}') sys.exit(0 if code == 200 else 1) elif a[0] == 'api' and any('/check-runs' in arg for arg in a): print(json.dumps([{'check_runs': x['checks']}])) @@ -496,6 +498,23 @@ def test_present_ci_workflow_still_requires_ci_status(self): self.assertFalse(merged) self.assertIn('ci-status', result.stderr) + def test_ci_workflow_without_a_ci_status_job_merges_all_green_checks(self): + tests_only = "name: CI\non: pull_request\njobs:\n tests:\n runs-on: macos-15\n steps:\n - run: swift test\n" + result, merged, _ = self.run_case(workflow=True, workflow_body=tests_only) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(merged) + result, merged, _ = self.run_case(workflow=True, workflow_body=tests_only, + checks=[{'id': 1, 'name': 'tests', 'status': 'completed', 'conclusion': 'failure'}]) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(merged) + + def test_ci_workflow_with_a_ci_status_job_requires_it(self): + aggregate = "jobs:\n tests:\n runs-on: x\n # ci-status: in a comment does not count\n ci-status:\n needs: [tests]\n" + result, merged, _ = self.run_case(workflow=True, workflow_body=aggregate) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(merged) + self.assertIn('ci-status', result.stderr) + def test_absent_app_workflow_does_not_require_compile(self): result, merged, _ = self.run_case(workflow=True, files=['Sources/App.swift'], checks=[{'id': 1, 'name': 'ci-status', 'status': 'completed', 'conclusion': 'success'}]) self.assertEqual(result.returncode, 0, result.stderr) From 1171cecd34a43cd438869c0dad199199ed87c12a Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 23:42:39 -0700 Subject: [PATCH 2/3] fix: gh-merge-green requires ci-status only when a workflow defines it Co-Authored-By: Claude Opus 5.5 --- scripts/gh-merge-green | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/gh-merge-green b/scripts/gh-merge-green index 1e851ac67765..1427f1123cd2 100755 --- a/scripts/gh-merge-green +++ b/scripts/gh-merge-green @@ -186,9 +186,11 @@ fi # Require aggregate checks only when their workflow exists on the base. Otherwise # every reported exact-head check and status context must be completed successfully. +workflow_response='' workflow_on_base() { local file="$1" response if response=$(gh api --include "repos/$repo/contents/.github/workflows/$file" -X GET -f "ref=$base" 2>/dev/null); then + workflow_response="$response" return 0 fi if grep -qE '^HTTP/[^ ]+ 404([[:space:]]|$)' <<<"$response"; then @@ -199,10 +201,29 @@ workflow_on_base() { 'restore GitHub contents API access; only HTTP 404 means the workflow is absent' } +# ci-status is required only when ci.yml or ci-macos.yml defines that job; +# a ci.yml with only other jobs (bonsplit's `tests`) gets the all-checks path. Fails closed: a body that is missing, too large for the +# contents API, or unreadable still requires ci-status. +reports_ci_status() { + python3 -c ' +import base64, json, re, sys +raw = sys.stdin.read() +body = re.split(r"\r?\n\r?\n", raw, maxsplit=1) +try: + text = base64.b64decode(json.loads(body[1])["content"]).decode() +except Exception: + sys.exit(0) +sys.exit(0 if re.search(r"(?m)^ ci-status:\s*$", text) else 1) +' <<<"$workflow_response" +} + has_ci=0 has_app_compile=0 -if workflow_on_base ci.yml; then has_ci=1; fi -if workflow_on_base ci-macos.yml; then has_app_compile=1; fi +if workflow_on_base ci.yml && reports_ci_status; then has_ci=1; fi +if workflow_on_base ci-macos.yml; then + has_app_compile=1 + if reports_ci_status; then has_ci=1; fi +fi must=() if [ "$has_ci" -eq 1 ]; then must+=("ci-status"); fi From 8b6a13d8538708070ac1d111f896472ce4b924e4 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 4 Oct 2026 23:44:49 -0700 Subject: [PATCH 3/3] fix: gh-merge-green fails closed on empty workflow content and reads quoted or indented ci-status keys Co-Authored-By: Claude Opus 5.5 --- scripts/gh-merge-green | 7 +++++-- tests/test_gh_merge_green.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/scripts/gh-merge-green b/scripts/gh-merge-green index 1427f1123cd2..4721963f85f0 100755 --- a/scripts/gh-merge-green +++ b/scripts/gh-merge-green @@ -210,10 +210,13 @@ import base64, json, re, sys raw = sys.stdin.read() body = re.split(r"\r?\n\r?\n", raw, maxsplit=1) try: - text = base64.b64decode(json.loads(body[1])["content"]).decode() + data = json.loads(body[1]) + text = base64.b64decode(data["content"]).decode() if data.get("encoding") == "base64" else "" except Exception: sys.exit(0) -sys.exit(0 if re.search(r"(?m)^ ci-status:\s*$", text) else 1) +if not text.strip(): + sys.exit(0) +sys.exit(0 if re.search(r"(?m)^\s+[\x22\x27]?ci-status[\x22\x27]?\s*:", text) else 1) ' <<<"$workflow_response" } diff --git a/tests/test_gh_merge_green.py b/tests/test_gh_merge_green.py index 5294e3d1e4b8..568f06b89338 100644 --- a/tests/test_gh_merge_green.py +++ b/tests/test_gh_merge_green.py @@ -414,7 +414,8 @@ def test_large_file_and_patch_lists_do_not_trip_pipefail(self): class WorkflowPresenceRegression(unittest.TestCase): """Repositories without the aggregate workflow use all exact-head verdicts.""" - def run_case(self, *, workflow=False, probe_status=404, checks=None, statuses=None, app_workflow=False, files=None, workflow_body=None): + def run_case(self, *, workflow=False, probe_status=404, checks=None, statuses=None, app_workflow=False, files=None, workflow_body=None, + raw_content=None, app_workflow_body=None): with tempfile.TemporaryDirectory() as directory: directory = Path(directory) marker = directory / "merged" @@ -422,7 +423,7 @@ def run_case(self, *, workflow=False, probe_status=404, checks=None, statuses=No payload = {"head": HEAD, "workflow": workflow, "probe_status": probe_status, "checks": checks if checks is not None else [{"id": 1, "name": "tests", "status": "completed", "conclusion": "success"}], "statuses": statuses or [], "app_workflow": app_workflow, "files": files or [], - "workflow_body": workflow_body} + "workflow_body": workflow_body, "raw_content": raw_content, "app_workflow_body": app_workflow_body} fixture = directory / "fixture.json" fixture.write_text(__import__("json").dumps(payload)) gh = directory / "gh" @@ -441,8 +442,13 @@ def run_case(self, *, workflow=False, probe_status=404, checks=None, statuses=No code = 200 if present else x['probe_status'] print('HTTP/2.0 ' + str(code)) print() - body = x['workflow_body'] - print(json.dumps({'content': __import__('base64').b64encode(body.encode()).decode()}) if body is not None else '{}') + body = x['app_workflow_body'] if any('ci-macos.yml' in arg for arg in a) else x['workflow_body'] + if x['raw_content'] is not None and not any('ci-macos.yml' in arg for arg in a): + print(json.dumps(x['raw_content'])) + elif body is not None: + print(json.dumps({'encoding': 'base64', 'content': __import__('base64').b64encode(body.encode()).decode()})) + else: + print('{}') sys.exit(0 if code == 200 else 1) elif a[0] == 'api' and any('/check-runs' in arg for arg in a): print(json.dumps([{'check_runs': x['checks']}])) @@ -515,6 +521,23 @@ def test_ci_workflow_with_a_ci_status_job_requires_it(self): self.assertFalse(merged) self.assertIn('ci-status', result.stderr) + def test_unreadable_workflow_body_still_requires_ci_status(self): + # The contents API returns empty content with encoding "none" for files over 1 MB. + for raw in ({'encoding': 'none', 'content': ''}, {'content': None}, {'encoding': 'base64', 'content': ''}): + with self.subTest(raw=raw): + result, merged, _ = self.run_case(workflow=True, raw_content=raw) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(merged) + self.assertIn('ci-status', result.stderr) + + def test_ci_status_job_in_the_app_workflow_requires_it(self): + tests_only = "jobs:\n tests:\n runs-on: x\n" + result, merged, _ = self.run_case(workflow=True, workflow_body=tests_only, app_workflow=True, + app_workflow_body="jobs:\n 'ci-status': # aggregate\n needs: [tests]\n") + self.assertNotEqual(result.returncode, 0) + self.assertFalse(merged) + self.assertIn('ci-status', result.stderr) + def test_absent_app_workflow_does_not_require_compile(self): result, merged, _ = self.run_case(workflow=True, files=['Sources/App.swift'], checks=[{'id': 1, 'name': 'ci-status', 'status': 'completed', 'conclusion': 'success'}]) self.assertEqual(result.returncode, 0, result.stderr)