Skip to content

fix(datagrid): carry identity columns through so a new row pre-fills DEFAULT #2085

fix(datagrid): carry identity columns through so a new row pre-fills DEFAULT

fix(datagrid): carry identity columns through so a new row pre-fills DEFAULT #2085

Workflow file for this run

name: macOS Tests
on:
# No paths filter here on purpose. A required status check has to report on every pull
# request, and a workflow skipped by a paths filter reports nothing at all, which leaves
# the check pending forever and blocks docs-only work. The filtering moved into the
# changes job below, so the gate job always runs and always reports.
pull_request:
push:
branches: [main]
paths:
- "TablePro/**"
- "Plugins/**"
- "Packages/**"
- "LocalPackages/**"
- "TableProTests/**"
- "TableProUITests/**"
- "Native/**"
- "project.yml"
- "TableProMobile/project.yml"
- "Configs/**"
- "Libs/**"
- "scripts/**"
- "TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
- ".github/actions/**"
- ".github/macos-test-quarantine.txt"
- ".github/macos-ui-test-quarantine.txt"
- ".github/workflows/macos-tests.yml"
workflow_dispatch:
workflow_call:
# One run per PR/branch at a time. On a pull request a new push cancels the run in flight, because
# only the newest commit's verdict is wanted and the older one is about to be replaced anyway.
#
# A push to main is different: its run is the only verdict that commit will ever get, and cancelling
# it leaves main untested. That is not hypothetical. Of the last 30 main runs of this workflow, 13
# were cancelled by the next merge, and the merges are frequent enough that whole days passed with
# no conclusive result on main at all.
#
# Queueing them does not pile up. With cancel-in-progress false GitHub still replaces the *pending*
# run in the group: "any existing pending job or workflow in the same concurrency group will be
# canceled and the new queued job or workflow will take its place." So a burst of merges holds at
# most one running plus one queued, and the one that ends up queued is the newest.
concurrency:
group: macos-tests-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
# A downgrade or an exact match is always allowed for a called workflow; only asking for more than
# the caller holds fails at run creation. Declaring read here therefore cannot break build.yml's
# workflow_call, and it stops every job in this file from inheriting a write token if the repository
# default is ever flipped.
permissions:
contents: read
env:
XCODE_VERSION: "26.4.1"
XCODE_PROJECT: TablePro.xcodeproj
XCODE_SCHEME: TablePro
TEST_DESTINATION: "platform=macOS"
# Inside the workspace rather than the default location, because the build job tars it and the
# test jobs unpack it. Every path inside the generated .xctestrun is written relative to
# __TESTROOT__, so the products relocate to any directory.
DERIVED_DATA: DerivedData
jobs:
# Reproduces the paths filter this workflow used to carry on `on: pull_request`. Keep this
# list and the `push:` paths above identical, so a pull request and a push to main run the
# same suites. `Native/` is on both because the Dameng steps below build and test the Rust
# bridge that lives there, and a bridge-only change would otherwise compile nowhere.
changes:
name: Detect relevant changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run: ${{ steps.decide.outputs.run }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Decide whether the macOS suites need to run
id: decide
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
REF: ${{ github.ref }}
# The reasoning lives in scripts/ci/detect-changed-paths.sh. It was inline here and in
# ios-tests.yml, with a comment asking a human to keep the two copies in step and nothing
# enforcing it.
run: |
set -euo pipefail
RUN=$(scripts/ci/detect-changed-paths.sh \
--event "$EVENT_NAME" \
--base "$BASE_SHA" \
--ref "$REF" \
--skip-release-commit \
'TablePro/' 'Plugins/' 'Packages/' 'LocalPackages/' \
'TableProTests/' 'TableProUITests/' 'Native/' 'Configs/' 'Libs/' 'scripts/' \
'\.github/actions/' '\.github/macos-(ui-)?test-quarantine\.txt$' \
'TablePro\.xcodeproj/project\.xcworkspace/xcshareddata/swiftpm/Package\.resolved$' \
'TableProMobile/project\.yml$' 'project\.yml$' \
'\.github/workflows/macos-tests\.yml$')
echo "run=$RUN" >> "$GITHUB_OUTPUT"
packages:
name: Package Tests
needs: changes
if: needs.changes.outputs.run == 'true'
runs-on: macos-26
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Run TableProCore package tests
run: swift test --package-path Packages/TableProCore
# CodeEditSourceEditor is deliberately not run here. `swift test` cannot build it on this
# runner: its CodeEditSymbols dependency fails with "type 'Bundle' has no member 'module'",
# after a SwiftPM cache warning about a missing maintenance.lock. It builds fine inside the
# Xcode project, so the app target still compiles it; only the standalone package test run
# is broken. Its own suites also fail on main (HighlighterTests, TagEditingTests, plus one
# that aborts the runner), so this needs fixing at the package level before it can gate.
- name: Run CodeEditTextView package tests
run: swift test --package-path LocalPackages/CodeEditTextView
# Compiles the app, both test bundles and all 31 plugins once, then hands the products to the
# test jobs. That build used to happen inside the same job that ran the tests, which meant the
# unit step spent 12 of its 15.3 minutes compiling and the UI step could not start until the
# unit step had finished. All 12,662 unit cases execute in 206 seconds, so the build was almost
# all of the cost and none of it was parallel.
build:
name: Build for testing
needs: changes
if: needs.changes.outputs.run == 'true'
runs-on: macos-26
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Install xcbeautify
run: brew list xcbeautify &>/dev/null || brew install xcbeautify
- name: Cache static libraries
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: Libs
key: ${{ runner.os }}-libs-${{ hashFiles('Libs/checksums.sha256', 'Plugins/MSSQLDriverPlugin/CFreeTDS/include/sybdb.h') }}
- name: Download static libraries
env:
GH_TOKEN: ${{ github.token }}
run: scripts/download-libs.sh
# Compiles a C probe against Libs/libbson and parses every filter document the MongoDB
# query builder can emit. It guarded that invariant and ran nowhere; it needs clang and the
# vendored libraries, so this is the first job where it can run at all.
- name: Check the MongoDB filter shapes against the query builder
run: scripts/check-mongodb-filter-shapes.sh
# Cargo and rustup need access outside Xcode's user-script sandbox. Build the
# native bridge explicitly before compiling the driver target.
- name: Build Dameng native bridge
run: scripts/build-dameng.sh arm64
# Only the bridge's own FFI tests live here. The DM8 protocol crates are a pinned
# revision of TableProApp/rust-dameng and carry their own suite in that repository.
# Run from the bridge directory so rustup reads its rust-toolchain.toml: rustup resolves
# that file from the working directory, not from --manifest-path, and testing on the
# runner's default Rust would not be the toolchain the shipped staticlib is built with.
- name: Test Dameng native bridge
working-directory: Native/DamengBridge
run: cargo test --locked
# Secrets.xcconfig is gitignored. Tests do not need analytics keys, and the project only
# needs the variable to resolve, so an empty value is enough and no secret is read here.
- name: Create Secrets.xcconfig
run: echo "ANALYTICS_HMAC_SECRET = " > Configs/Secrets.xcconfig
- name: Setup XcodeGen
uses: ./.github/actions/setup-xcodegen
- name: Generate Xcode project
run: scripts/generate-project.sh
# Package.resolved pins every revision and is tracked, which is why it can key the cache.
# build.yml has cached these checkouts for a while; this workflow paid 1.5 minutes a run
# re-resolving them.
- name: Cache Swift package checkouts
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.spm-cache
key: ${{ runner.os }}-spm-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: ${{ runner.os }}-spm-
- name: Resolve Swift package dependencies
run: |
xcodebuild -resolvePackageDependencies \
-project "$XCODE_PROJECT" \
-scheme "$XCODE_SCHEME" \
-clonedSourcePackagesDirPath ~/.spm-cache \
-skipPackagePluginValidation
# The TablePro scheme only builds the 14 bundled plugins, so a change confined to one
# of the 17 registry-only plugins compiles nowhere else in CI. AllPlugins is the only
# target that covers them, and build-plugin.yml does not run until a release tag.
- name: Compile every plugin
run: |
set -o pipefail
xcodebuild build \
-project "$XCODE_PROJECT" \
-scheme AllPlugins \
-destination "$TEST_DESTINATION" \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath ~/.spm-cache \
-skipPackagePluginValidation \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions
# Builds and signs used to be the whole check: nothing ever tried to load one. "Bundle
# failed to load executable" has shipped twice, and dlopen is exactly the dyld path that
# fails in that case.
- name: Verify every plugin loads
run: scripts/ci/verify-plugin-loads.sh "$DERIVED_DATA/Build/Products/Debug"
- name: Build the app and both test bundles
run: |
set -o pipefail
xcodebuild build-for-testing \
-project "$XCODE_PROJECT" \
-scheme "$XCODE_SCHEME" \
-destination "$TEST_DESTINATION" \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath ~/.spm-cache \
-skipPackagePluginValidation \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions
# DamengDriverTests is the one plugin-owned unit bundle: its suites import CDameng, so
# they cannot move into TableProTests the way pure-logic plugin tests do. Without this
# step nothing runs them and they rot silently.
- name: Build the Dameng driver tests
run: |
set -o pipefail
xcodebuild build-for-testing \
-project "$XCODE_PROJECT" \
-scheme DamengDriverTests \
-destination "$TEST_DESTINATION" \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath ~/.spm-cache \
-skipPackagePluginValidation \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions
# Tarred rather than uploaded as a directory. actions/upload-artifact does not preserve
# the executable bit or symlinks, and both matter to an .app bundle: the app would arrive
# unable to run. The archive measures 144 MB against 559 MB on disk.
- name: Pack the built products
run: tar -czf products.tar.gz -C "$DERIVED_DATA/Build" Products
- name: Upload the built products
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-products
path: products.tar.gz
retention-days: 1
compression-level: 0
unit:
name: Unit tests
needs: [changes, build]
if: needs.changes.outputs.run == 'true'
runs-on: macos-26
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Install xcbeautify
run: brew list xcbeautify &>/dev/null || brew install xcbeautify
- uses: ./.github/actions/unpack-test-products
# Quarantined cases are listed one per line in .github/macos-test-quarantine.txt. Burn that
# list down. The script enumerates the target first and refuses to run if any entry would
# skip nothing, because xcodebuild accepts -skip-testing for a case that does not exist and
# says nothing: that left one line inert for 479 commits, and it is also what happens to a
# Swift Testing case written without its parentheses.
- 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=()
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 < "$SKIP_FILE"
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN" \
-destination "$TEST_DESTINATION" \
-only-testing:TableProTests \
${SKIP_ARGS[@]+"${SKIP_ARGS[@]}"} \
-parallel-testing-enabled NO \
-resultBundlePath TestResults.xcresult \
| xcbeautify --renderer github-actions
- name: Run Dameng driver tests
run: |
set -o pipefail
xcodebuild test-without-building \
-xctestrun "$DAMENG_XCTESTRUN" \
-destination "$TEST_DESTINATION" \
-parallel-testing-enabled NO \
| xcbeautify --renderer github-actions
# Always, including on failure: a red run is when the durations matter most.
- name: Summarise test durations
if: ${{ !cancelled() }}
run: scripts/ci/summarize-xcresult.sh TestResults.xcresult "Unit tests"
- name: Upload test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-test-results
path: TestResults.xcresult
retention-days: 7
# UI tests drive the real app, so they need the runner's GUI session and cannot run beside
# the unit run. Every case launches the app against a throwaway storage sandbox that
# UITestCase hands it, so nothing here touches a real store.
#
# Sharded across runners rather than parallelised on one. Three things in the suite are shared
# per machine and cannot be made per-worker: the single `com.TablePro.uitest` defaults domain,
# the preferences sweep in UITestCase's class setUp, and the menu bar itself, which belongs to
# whichever app is frontmost. A separate runner per shard has its own of all three.
ui:
name: UI tests ${{ matrix.shard }}/${{ matrix.of }}
needs: [changes, build]
if: needs.changes.outputs.run == 'true'
runs-on: macos-26
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2]
of: [3]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Install xcbeautify
run: brew list xcbeautify &>/dev/null || brew install xcbeautify
- uses: ./.github/actions/unpack-test-products
# One retry is allowed. A full local suite flaked once in three runs on a single
# waitForExistence, which without a retry would fail roughly a third of runs and train
# people to re-run until green. A case that fails twice is a real failure.
- name: Run UI tests
env:
SHARD: ${{ matrix.shard }}
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 < "$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 \
-xctestrun "$XCTESTRUN" \
-destination "$TEST_DESTINATION" \
"${ONLY_ARGS[@]}" \
-parallel-testing-enabled NO \
-test-iterations 2 \
-retry-tests-on-failure \
-resultBundlePath UITestResults.xcresult \
| xcbeautify --renderer github-actions
- name: Summarise UI test durations
if: ${{ !cancelled() }}
run: scripts/ci/summarize-xcresult.sh UITestResults.xcresult "UI tests ${{ matrix.shard }}/${{ matrix.of }}"
- name: Upload UI test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-ui-test-results-${{ matrix.shard }}
path: UITestResults.xcresult
retention-days: 7
# The one check to mark required on main. It reports on every pull request, so a docs-only
# change passes on skipped suites instead of hanging, and a red suite cannot be merged past.
gate:
name: macOS Tests Gate
if: always()
needs: [changes, packages, build, unit, ui]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require every suite to have passed or been skipped
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
set -euo pipefail
echo "suite results: $RESULTS"
for result in $RESULTS; do
case "$result" in
success|skipped) ;;
*)
echo "A macOS suite reported '$result'." >&2
exit 1
;;
esac
done