From b69e605095fcd4a01f200c3ddf308f9f043368bc Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 26 Aug 2026 08:20:24 +0700 Subject: [PATCH 1/3] fix(tabs): restore the quinaryLabel spelling that Xcode 26.4 compiles --- TablePro/Views/Main/EditorTabStripSurfaces.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/TablePro/Views/Main/EditorTabStripSurfaces.swift b/TablePro/Views/Main/EditorTabStripSurfaces.swift index b4299d4d9..bae3c9363 100644 --- a/TablePro/Views/Main/EditorTabStripSurfaces.swift +++ b/TablePro/Views/Main/EditorTabStripSurfaces.swift @@ -23,7 +23,12 @@ internal enum EditorTabStripPalette { internal static var selectedFill: Color { Color(nsColor: .controlColor) } /// Half the weight of a separator. `separatorColor` was twice the measured edge and read as a /// drawn outline rather than the lit rim the system puts there. - internal static var trackEdge: Color { Color(nsColor: .quinaryLabelColor) } + /// + /// `quinaryLabel`, not `quinaryLabelColor`. The two spellings swap places between toolchains: + /// the Xcode 27 beta deprecates this one in favour of `quinaryLabelColor`, and Xcode 26.4, the + /// one CI builds with, rejects `quinaryLabelColor` outright as renamed to this. Take the + /// spelling CI accepts and ignore the beta's deprecation hint. + internal static var trackEdge: Color { Color(nsColor: .quinaryLabel) } internal static var hoverFill: Color { Color(nsColor: .tertiarySystemFill) } internal static var separator: Color { Color(nsColor: .separatorColor) } } From eba34d0391974ae4ecd6f6bb2d3e1da073555215 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 26 Aug 2026 08:20:28 +0700 Subject: [PATCH 2/3] ci: stop an empty test quarantine passing an empty argument to xcodebuild --- .github/workflows/macos-tests.yml | 4 ++ .github/workflows/repo-hygiene.yml | 7 ++ scripts/ci/quarantine_args.py | 9 ++- scripts/ci/test_quarantine_args.py | 106 +++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 scripts/ci/test_quarantine_args.py diff --git a/.github/workflows/macos-tests.yml b/.github/workflows/macos-tests.yml index 6598ef3b5..1cc4f950f 100644 --- a/.github/workflows/macos-tests.yml +++ b/.github/workflows/macos-tests.yml @@ -306,6 +306,10 @@ jobs: # mapfile nor readarray, and a `run:` block gets that bash. SKIP_ARGS=() while IFS= read -r arg; do + # A blank line would become a literal empty argument, and xcodebuild reads that as a + # build action and refuses the whole run. An empty array expands to nothing and is + # fine; one empty string is not. + [ -n "$arg" ] || continue SKIP_ARGS+=("$arg") done < <(scripts/ci/quarantine-args.sh .github/macos-test-quarantine.txt TableProTests "$XCTESTRUN" "$TEST_DESTINATION") xcodebuild test-without-building \ diff --git a/.github/workflows/repo-hygiene.yml b/.github/workflows/repo-hygiene.yml index 734b02850..42e8b2c68 100644 --- a/.github/workflows/repo-hygiene.yml +++ b/.github/workflows/repo-hygiene.yml @@ -83,6 +83,13 @@ jobs: - name: Validate the registry update script run: python3 .github/scripts/test_update_registry.py + # The macOS suite cannot catch this one: the script runs before any test does, and when it + # gets this wrong no test runs at all. An empty quarantine list made it print a blank line, + # which the caller turned into an empty xcodebuild argument, and main went red for three + # commits with `Unknown build action ''`. + - name: Validate the test quarantine script + run: python3 scripts/ci/test_quarantine_args.py + # A check that guarded a real invariant and ran nowhere. Pure grep over Swift sources, so it # belongs on the free Linux runner. The MongoDB filter-shape check is the other one that was # orphaned, but it compiles a C probe against Libs/libbson, so it lives in the macOS build diff --git a/scripts/ci/quarantine_args.py b/scripts/ci/quarantine_args.py index 158f2e8cd..c18062182 100755 --- a/scripts/ci/quarantine_args.py +++ b/scripts/ci/quarantine_args.py @@ -69,7 +69,14 @@ def main(): if not ok: sys.exit(1) - print("\n".join(args)) + + # Nothing at all when there is nothing to skip, because `print("")` is a blank line and the + # caller reads this stdout one line per argument. An empty list therefore reached xcodebuild as + # a literal empty argument and it stopped with `Unknown build action ''` before running a + # single test. An empty quarantine file is the goal, not an edge case: main went red the + # commit the last entry was removed and stayed red. + if args: + print("\n".join(args)) if __name__ == "__main__": diff --git a/scripts/ci/test_quarantine_args.py b/scripts/ci/test_quarantine_args.py new file mode 100644 index 000000000..965fe0f90 --- /dev/null +++ b/scripts/ci/test_quarantine_args.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Tests for quarantine_args.py, whose stdout is read one line per xcodebuild argument. + +Run: python3 scripts/ci/test_quarantine_args.py + +The first test is the one that matters. `print("\\n".join([]))` is a blank line, the caller turns +every line into an argument, and xcodebuild reads an empty argument as a build action: `Unknown +build action ''`, exit 65, before a single test runs. An empty quarantine file is the goal the file +itself states, so main went red the commit the last entry was removed and stayed red for three +commits. +""" +import importlib.util +import io +import json +import os +import sys +import tempfile +from contextlib import redirect_stdout + +_spec = importlib.util.spec_from_file_location( + "quarantine_args", + os.path.join(os.path.dirname(__file__), "quarantine_args.py"), +) +quarantine_args = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(quarantine_args) + +ENUMERATED = ["TableProTests/SomeSuite/someCase()", "TableProTests/OtherSuite/otherCase()"] + + +def _run(quarantine_text, identifiers=None): + """Returns (stdout, exit code or None), the way the shell caller sees it.""" + with tempfile.TemporaryDirectory() as work: + quarantine = os.path.join(work, "quarantine.txt") + with open(quarantine, "w", encoding="utf-8") as handle: + handle.write(quarantine_text) + + enumeration = os.path.join(work, "tests.json") + listed = ENUMERATED if identifiers is None else identifiers + with open(enumeration, "w", encoding="utf-8") as handle: + json.dump({"values": [{"enabledTests": [{"identifier": i} for i in listed]}]}, handle) + + os.environ["QUARANTINE"] = quarantine + os.environ["TARGET"] = "TableProTests" + argv = sys.argv + sys.argv = ["quarantine_args.py", enumeration] + captured = io.StringIO() + status = None + try: + with redirect_stdout(captured): + quarantine_args.main() + except SystemExit as error: + status = error.code + finally: + sys.argv = argv + return captured.getvalue(), status + + +def test_an_empty_list_prints_nothing_at_all(): + """Not even a newline. One blank line is one empty argument, and xcodebuild refuses the run.""" + out, status = _run("# every entry has been burned down\n#\n") + assert out == "", repr(out) + assert status is None, status + + +def test_a_file_of_only_whitespace_prints_nothing(): + out, status = _run("\n \n\t\n") + assert out == "", repr(out) + assert status is None, status + + +def test_no_line_is_ever_empty(): + """The caller reads one argument per line, so a blank one is unrepresentable by construction.""" + out, _ = _run("SomeSuite/someCase()\nOtherSuite/otherCase()\n") + assert out.splitlines() == [ + "-skip-testing:TableProTests/SomeSuite/someCase()", + "-skip-testing:TableProTests/OtherSuite/otherCase()", + ], out + assert all(line.strip() for line in out.splitlines()), repr(out) + + +def test_a_whole_suite_is_accepted_without_a_case(): + out, _ = _run("SomeSuite\n") + assert out.splitlines() == ["-skip-testing:TableProTests/SomeSuite"], out + + +def test_an_entry_that_would_skip_nothing_still_fails_the_job(): + """The guard the script exists for, unchanged by the empty-list fix.""" + out, status = _run("NoSuchSuite/nope()\n") + assert status == 1, status + assert out == "", repr(out) + + +def test_a_swift_testing_case_without_its_parentheses_still_fails(): + out, status = _run("SomeSuite/someCase\n") + assert status == 1, status + assert out == "", repr(out) + + +if __name__ == "__main__": + test_an_empty_list_prints_nothing_at_all() + test_a_file_of_only_whitespace_prints_nothing() + test_no_line_is_ever_empty() + test_a_whole_suite_is_accepted_without_a_case() + test_an_entry_that_would_skip_nothing_still_fails_the_job() + test_a_swift_testing_case_without_its_parentheses_still_fails() + print("All quarantine-args tests passed.") From cf3f1153594349338be822aacd72071642ef7c15 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Wed, 26 Aug 2026 08:30:28 +0700 Subject: [PATCH 3/3] ci: fail the test step when the quarantine or shard lister refuses to run --- .github/workflows/macos-tests.yml | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/macos-tests.yml b/.github/workflows/macos-tests.yml index 1cc4f950f..fe046ed19 100644 --- a/.github/workflows/macos-tests.yml +++ b/.github/workflows/macos-tests.yml @@ -302,6 +302,12 @@ jobs: - name: Run unit tests run: | set -o pipefail + # To a file on its own line, not through a process substitution. `< <(...)` discards the + # producer's exit status, so the script refusing an entry that would skip nothing printed + # its diagnostic, exited 1, and the step carried on to run the whole suite unskipped and + # report success. The refusal the comment above promises never once failed a job. + SKIP_FILE="${RUNNER_TEMP:-.}/quarantine-skip-args.txt" + scripts/ci/quarantine-args.sh .github/macos-test-quarantine.txt TableProTests "$XCTESTRUN" "$TEST_DESTINATION" > "$SKIP_FILE" # A read loop, not mapfile: the macOS runner's /bin/bash is 3.2, which has neither # mapfile nor readarray, and a `run:` block gets that bash. SKIP_ARGS=() @@ -311,12 +317,12 @@ jobs: # fine; one empty string is not. [ -n "$arg" ] || continue SKIP_ARGS+=("$arg") - done < <(scripts/ci/quarantine-args.sh .github/macos-test-quarantine.txt TableProTests "$XCTESTRUN" "$TEST_DESTINATION") + done < "$SKIP_FILE" xcodebuild test-without-building \ -xctestrun "$XCTESTRUN" \ -destination "$TEST_DESTINATION" \ -only-testing:TableProTests \ - "${SKIP_ARGS[@]}" \ + ${SKIP_ARGS[@]+"${SKIP_ARGS[@]}"} \ -parallel-testing-enabled NO \ -resultBundlePath TestResults.xcresult \ | xcbeautify --renderer github-actions @@ -384,17 +390,22 @@ jobs: SHARD_COUNT: ${{ matrix.of }} run: | set -o pipefail + # To a file on its own line, so a failure of the lister fails the step. A process + # substitution discards its exit status, which reads here as "the shard selected nothing". + ONLY_FILE="${RUNNER_TEMP:-.}/shard-only-args.txt" + scripts/ci/list-tests.sh \ + --xctestrun "$XCTESTRUN" \ + --target TableProUITests \ + --quarantine .github/macos-ui-test-quarantine.txt \ + --shard "$SHARD/$SHARD_COUNT" > "$ONLY_FILE" # A read loop, not mapfile: the macOS runner's /bin/bash is 3.2, which has neither. ONLY_ARGS=() while IFS= read -r arg; do + # The count check below cannot see an empty argument: one blank line makes the count 1 + # and xcodebuild then reads that empty argument as a build action. + [ -n "$arg" ] || continue ONLY_ARGS+=("$arg") - done < <( - scripts/ci/list-tests.sh \ - --xctestrun "$XCTESTRUN" \ - --target TableProUITests \ - --quarantine .github/macos-ui-test-quarantine.txt \ - --shard "$SHARD/$SHARD_COUNT" - ) + done < "$ONLY_FILE" [ "${#ONLY_ARGS[@]}" -gt 0 ] || { echo "::error::shard $SHARD selected no cases"; exit 1; } echo "shard $SHARD of $SHARD_COUNT runs ${#ONLY_ARGS[@]} cases" xcodebuild test-without-building \