Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions .claude/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
## Rulesets and required checks

- There are no gate jobs. A test job runs on `!cancelled() && needs.changes.outputs.run != 'false'`. A job its `if:` skips reports success, and a failed changes job runs the suite rather than skipping it.
- `Changes (macOS)` and `Changes (iOS)` are not required checks.
- `Changes (macOS)`, `Changes (iOS)` and `Changes (Release)` are not required checks.
- A PR need not be up to date with main to merge (`strict_required_status_checks_policy: false`).
- A second ruleset blocks force-push and deletion of `main`, with no bypass.
- A required check's name is its job's `name:`. Renaming a job changes the ruleset in the same change, and every open PR then merges main in before its checks report the new name.
Expand All @@ -31,7 +31,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/

## macOS Tests (`macos-tests.yml`)

- Triggers: pull request, push to main, dispatch. A PR keeps only its newest run; each main commit has its own concurrency group and is never cancelled.
- Triggers: pull request, push to main or `release/**`, dispatch. A PR keeps only its newest run; each pushed commit has its own concurrency group and is never cancelled.
- `Changes (macOS)` outputs `run` (detector `macos`) and `plugins` (detector `plugins`). The test job has a 120-minute limit and sets `NSUnbufferedIO=YES`.
- Setup, in order: checkout, setup-xcode, setup-xcodegen, `scripts/download-libs.sh`, `generate-project.sh macos`, SPM restore, resolve, SPM save.
- One `build-for-testing` with no `-testPlan` builds both plans and writes one `.xctestrun` per plan, with signing, the index store and the compilation cache off.
Expand All @@ -48,6 +48,15 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
- `swift test --package-path Packages/TableProCore --parallel`, then TableProOracle with `--force-resolved-versions` when its detector is not false. Each step has `if: !cancelled()`, so one red step never hides the next.
- The summary step always runs. Evidence uploads only on failure.

## Release Build (`release-build.yml`)

- Push to main or `release/**`, and dispatch; never a PR. Each commit has its own concurrency group and is never cancelled. Not a required check.
- `Changes (Release)` outputs `run` (detector `release`). `Release Build` runs on `xcode-27` with a 150-minute limit.
- Setup as macOS Tests up to the SPM restore, which it never saves; then Go, `build-dameng.sh both`, `build-hana.sh both` and create-dmg.
- `scripts/release/build.sh app --unsigned` archives the `TablePro` scheme; `build.sh plugins --unsigned` builds `RegistryPlugins` and copies the publish set (the `.github/plugin-registry.json` entries not bundled) to `build/release/plugins`. Both build universal and run `lipo -verify_arch arm64 x86_64` and `verify-plugin-loads.sh`.
- `scripts/release/package.sh app 0.0.0-dev --unsigned` writes the release assets: the DMG, the `.tar.xz` (extracted and compared with the app) and the dSYMs zip; `package.sh plugins` writes one zip per plugin and the plugin dSYMs zip.
- The plugins build runs after an app failure too. The xcodebuild logs in `build/release/logs` upload only on failure.

## Summaries (`scripts/ci/summarize-xcresult.sh`)

- Runs `scripts/lib/xcresult-cases.py summary`. `verify.sh` reads its verdicts through the same parser, and `scripts/lib/test_xcresult_cases.py` tests it in Repo Hygiene.
Expand All @@ -59,7 +68,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/

## SwiftLint (`swiftlint.yml`)

- PR, push to main, dispatch; Ubuntu, 10 minutes.
- PR, push to main or `release/**`, dispatch; Ubuntu, 10 minutes.
- Runs the digest-pinned `ghcr.io/realm/swiftlint` image over the whole repo with `lint --strict`. The image's entrypoint is `swiftlint`, so the command never repeats it.
- Fails when stderr has `Invalid configuration` or `is not a valid rule identifier`, because SwiftLint drops a custom rule whose regex does not compile and still exits 0.

Expand All @@ -70,7 +79,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/

## Repo Hygiene (`repo-hygiene.yml`)

- PR, push to main, dispatch, no path filter; Ubuntu, 15 minutes.
- PR, push to main or `release/**`, dispatch, no path filter; Ubuntu, 15 minutes.
- Steps, in order:
- actionlint, which also shellchecks every inline `run:` block;
- `shellcheck --severity=warning` on every `*.sh` under `scripts` and `.claude/skills`;
Expand All @@ -86,12 +95,13 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/

## Change detector (`scripts/ci/detect-changed-paths.sh`)

- `detect-changed-paths.sh [--base <sha>] macos|plugins|ios|oracle|hana` prints `true` or `false`. An unknown suite or flag exits 2.
- A PR passes its base SHA, a push `github.event.before`. No base, or the all-zero SHA of a new ref, prints true.
- `detect-changed-paths.sh [--base <sha>] macos|plugins|ios|oracle|hana|release` prints `true` or `false`. An unknown suite or flag exits 2.
- A PR passes its base SHA, a push `github.event.before`. No base prints true. The all-zero SHA of a new ref diffs against `git merge-base HEAD origin/main`, so a branch cut from main skips what main already ran; with no merge base it prints true.
- `macos` watches the app, test, plugin, package, native, config and libs folders, `TestPlans/`, `project.yml`, the app's `Package.resolved`, the build scripts and CI scripts it runs, both setup actions and its workflow. `Packages/` covers TableProMacCore.
- `plugins` watches `Plugins/`, `Packages/TableProCore/`, `Packages/TableProOracle/`, `Native/`, `Libs/`, `Configs/`, `project.yml`, the bridge scripts and `verify-plugin-loads.sh`.
- `ios` watches `TableProMobile/`, `Libs/`, `Plugins/TableProPluginKit/` (the package target is a symlink to it), and every `../` path it reads out of `TableProMobile/project.yml`.
- `oracle` watches `Packages/TableProOracle/`; `hana` watches `Native/HanaBridge/`. Each suite also watches its own workflow.
- `release` watches what the Release build reads: the app, plugin, package, native, config and libs folders, `project.yml`, the app's `Package.resolved`, `.github/plugin-registry.json`, `scripts/release/`, the build and CI scripts it runs and both setup actions. It skips `Plugins/*Tests/` and `Packages/*/Tests/`.
- A path a suite newly reads joins that suite's pattern in the same change, or the suite skips on it.
- Release-only files never trigger a suite: `CHANGELOG.md`, `appcast.xml`, `TablePro/Resources/WhatsNew.md`, `.github/release-flags.json`, and `Configs/Version.xcconfig` while its diff touches only the two version keys.
- Every file the release commit stages (`.claude/skills/release/SKILL.md`, Commit, tag, push) is on that list; `appcast.xml` is `build.yml`'s own push.
Expand All @@ -100,15 +110,15 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
## What runs where

- A pull request: both changes jobs; macOS Tests and iOS and Package Tests unless their detector says false (Oracle only when its detector is not false); SwiftLint; Repo Hygiene; PR title. `AllPlugins` only when the `plugins` detector is not false.
- A push to main: the same minus PR title, never cancelled, `AllPlugins` always, the SPM cache saved on a miss. Every code push runs both plans in full; a docs-only or release-only push skips the suites through the detector.
- A push to main or `release/**`: the same minus PR title, plus Release Build; never cancelled, `AllPlugins` always, the SPM cache saved on a miss. Every code push runs both plans in full; a docs-only or release-only push skips the suites through the detector.
- A `v*` tag runs only `build.yml`; a `plugin-*-v*` tag runs only `build-plugin.yml` (`RELEASING.md`). The test workflows never run on a tag.
- `workflow_dispatch` exists on every workflow except `pr-title.yml`.
- `build-linux.yml` is on `main` only for its Monday schedule, which GitHub runs from the default branch. Until the Linux app's directory lands on `main`, every build job skips and `weekly-dispatch` starts the workflow on the branch the `LINUX_BRANCH` variable names (the linux branch when unset), whose testing guide describes the jobs.

## Caches and runner budget

- Swift package checkouts live in `~/.spm-cache`, keyed `spm-<os>-<hash of the app's Package.resolved>` with prefix restore. The cache is saved only on a push to main and only on a miss: a cache saved on a PR ref is visible to that PR alone.
- `build.yml` restores that cache and never saves it, since a save in tag scope is unreadable from other tags.
- Swift package checkouts live in `~/.spm-cache`, keyed `spm-<os>-<hash of the app's Package.resolved>` with prefix restore. macOS Tests saves it only on a push and only on a miss: a cache saved on a PR ref is visible to that PR alone.
- `build.yml` and `release-build.yml` restore that cache and never save it; a save in tag scope is unreadable from other tags.
- There is no other cache: no libs or DerivedData cache, and none in the iOS, plugin or Ubuntu workflows.
- CI fits the GitHub Free and Pro plans: no paid larger runner, no self-hosted Mac. At most 5 macOS jobs run at once, and main runs queue behind PR runs for those slots.

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ios-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: iOS and Package Tests
on:
pull_request:
push:
branches: [main]
branches: [main, 'release/**']
workflow_dispatch:

concurrency:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/macos-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: macOS Tests
on:
pull_request:
push:
branches: [main]
branches: [main, 'release/**']
workflow_dispatch:

# A pull request keeps only its newest run. Each main commit gets a group of its own, so no main
Expand Down
111 changes: 111 additions & 0 deletions .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
name: Release Build

# The Release configuration of the app and every registry plugin, universal and unsigned, packaged
# the way a tag packages it, so a packaging failure shows on the commit instead of on the tag.

on:
push:
branches: [main, 'release/**']
workflow_dispatch:

# Each commit has a group of its own and is never cancelled, as in macos-tests.yml.
concurrency:
group: release-build-${{ github.sha }}
cancel-in-progress: false

permissions:
contents: read

jobs:
changes:
name: Changes (Release)
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 Release build runs
id: decide
env:
BASE: ${{ github.event.before }}
run: |
set -euo pipefail
run=$(scripts/ci/detect-changed-paths.sh --base "$BASE" release)
echo "run=$run" >> "$GITHUB_OUTPUT"

# Reads "not false" so a failed changes job builds instead of passing.
build:
name: Release Build
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.run != 'false' }}
runs-on: xcode-27
timeout-minutes: 150
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- uses: ./.github/actions/setup-xcode

- uses: ./.github/actions/setup-xcodegen

- name: Download static libraries
env:
GH_TOKEN: ${{ github.token }}
run: scripts/download-libs.sh

- name: Generate the Xcode project
run: scripts/generate-project.sh macos

# macos-tests.yml saves this cache.
- name: Restore Swift package checkouts
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.spm-cache
key: spm-${{ runner.os }}-${{ hashFiles('TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-${{ runner.os }}-

- name: Set up Go for the SAP HANA helper
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: Native/HanaBridge/go.mod
cache: false

# The Dameng and HANA targets copy these artifacts in and never build them.
- name: Build the native bridges
run: |
scripts/build-dameng.sh both
scripts/build-hana.sh both

- name: Install create-dmg
run: brew list create-dmg > /dev/null 2>&1 || brew install create-dmg

- name: Build the app
id: app
run: scripts/release/build.sh app --unsigned

# Runs after an app failure too, so one run reports both.
- name: Build the registry plugins
id: plugins
if: ${{ !cancelled() }}
run: scripts/release/build.sh plugins --unsigned

- name: Package the app
if: ${{ !cancelled() && steps.app.outcome == 'success' }}
run: scripts/release/package.sh app 0.0.0-dev --unsigned

- name: Package the plugins
if: ${{ !cancelled() && steps.plugins.outcome == 'success' }}
run: scripts/release/package.sh plugins 0.0.0-dev --unsigned

- name: Upload the build logs
if: ${{ failure() }}
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-build-logs-${{ github.run_attempt }}
path: build/release/logs
if-no-files-found: ignore
retention-days: 7
2 changes: 1 addition & 1 deletion .github/workflows/repo-hygiene.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: Repo Hygiene
on:
pull_request:
push:
branches: [main]
branches: [main, 'release/**']
workflow_dispatch:

concurrency:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: SwiftLint
on:
pull_request:
push:
branches: [main]
branches: [main, 'release/**']
workflow_dispatch:

concurrency:
Expand Down
55 changes: 34 additions & 21 deletions project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ targetTemplates:
# in a directory from mktemp: the user-script sandbox lets the phase write its declared output
# and nothing beside it, codesign writes a temporary file next to whatever it signs, and it
# reads every parent of that file, which the sandbox refuses inside the build folders.
# scripts/release/build.sh passes OTHER_CODE_SIGN_FLAGS=--timestamp; other builds skip the timestamp server.
HanaHelperEmbedding:
postBuildScripts:
- name: Embed tablepro-hana-helper
Expand All @@ -266,7 +267,7 @@ targetTemplates:
trap 'rm -rf "$staging"' EXIT
cp "$helper" "$staging/tablepro-hana-helper"
if [ "${CODE_SIGNING_ALLOWED:-NO}" = "YES" ]; then
/usr/bin/codesign --force --options runtime --timestamp=none --sign "${EXPANDED_CODE_SIGN_IDENTITY:--}" "$staging/tablepro-hana-helper"
/usr/bin/codesign --force --options runtime ${OTHER_CODE_SIGN_FLAGS:---timestamp=none} --sign "${EXPANDED_CODE_SIGN_IDENTITY:--}" "$staging/tablepro-hana-helper"
fi
cp -f "$staging/tablepro-hana-helper" "$SCRIPT_OUTPUT_FILE_0"

Expand Down Expand Up @@ -350,8 +351,7 @@ targets:
removeHeaders: false
copy:
destination: executables
# Plugins shipped inside the app bundle. Everything else under Plugins/ is
# published to the registry and is built by its own scheme or by AllPlugins.
# Plugins shipped inside the app bundle: the BundledPlugins aggregate lists the same set.
- target: MySQLDriver
embed: true
copy: { destination: plugins }
Expand Down Expand Up @@ -508,6 +508,8 @@ targets:
base:
ENABLE_HARDENED_RUNTIME: YES
PRODUCT_BUNDLE_IDENTIFIER: com.TablePro.tablepro-mcp
# The app embeds it; installed on its own it turns the app archive into a generic one.
SKIP_INSTALL: YES
PRODUCT_NAME: tablepro-mcp
SWIFT_APPROACHABLE_CONCURRENCY: YES
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY: YES
Expand All @@ -529,6 +531,8 @@ targets:
base:
ENABLE_HARDENED_RUNTIME: YES
PRODUCT_BUNDLE_IDENTIFIER: com.TablePro.tablepro-imagerender
# The app embeds it; installed on its own it turns the app archive into a generic one.
SKIP_INSTALL: YES
PRODUCT_NAME: tablepro-imagerender
SWIFT_APPROACHABLE_CONCURRENCY: YES
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY: YES
Expand Down Expand Up @@ -1203,18 +1207,33 @@ targets:
- package: TableProCore
product: TableProWeaviateCore

# Compile-checks every plugin, including the registry-only ones the app does not
# embed. CI builds this scheme so a registry plugin cannot rot between releases.
# Every plugin is in exactly one of BundledPlugins (embedded in the app) and RegistryPlugins (each
# release publishes it); scripts/ci/check-plugin-manifest.py enforces it. AllPlugins compiles both.
aggregateTargets:
AllPlugins:
BundledPlugins:
targets:
- CSVExport
- CSVImport
- ClickHouseDriver
- JSONExport
- JSONImport
- MQLExport
- MarkdownExport
- MySQLDriver
- PostgreSQLDriver
- RedisDriver
- SQLExport
- SQLImport
- SQLiteDriver
- XLSXExport
- XLSXImport

RegistryPlugins:
targets:
- BeancountDriver
- BigQueryDriverPlugin
- BigtableDriverPlugin
- CSVExport
- CSVImport
- CassandraDriver
- ClickHouseDriver
- CloudflareD1DriverPlugin
- CloudflareR2SQLDriverPlugin
- DamengDriver
Expand All @@ -1224,31 +1243,25 @@ aggregateTargets:
- EtcdDriverPlugin
- FirebaseDriverPlugin
- HanaDriver
- JSONExport
- JSONImport
- KafkaDriverPlugin
- LibSQLDriverPlugin
- MQLExport
- MSSQLDriver
- MongoDBDriver
- MySQLDriver
- OracleDriver
- PostgreSQLDriver
- RedisDriver
- MarkdownExport
- ParquetExport
- SQLExport
- XLSXImport
- SQLImport
- SQLiteDriver
- SnowflakeDriverPlugin
- SpannerDriverPlugin
- SurrealDBDriverPlugin
- TeradataDriver
- TrinoDriverPlugin
- TypesenseDriverPlugin
- WeaviateDriverPlugin
- XLSXExport
scheme: {}

AllPlugins:
targets:
- BundledPlugins
- RegistryPlugins
scheme: {}

schemes:
Expand Down
Loading
Loading