Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
19adc31
refactor(plugins): stamp the PluginKit version from one build setting
datlechin Oct 17, 2026
ae54f22
build(release): add the feed, registry and GitHub release tooling
datlechin Oct 17, 2026
79d553c
build(release): add the feed, registry and GitHub release tooling
datlechin Oct 17, 2026
7bd2f8d
Merge remote-tracking branch 'origin/main' into refactor/pluginkit-ve…
datlechin Oct 18, 2026
29afd77
Merge remote-tracking branch 'origin/build/release-feed-tooling' into…
datlechin Oct 18, 2026
4f8108e
Merge remote-tracking branch 'origin/build/release-feed-tooling' into…
datlechin Oct 18, 2026
e790126
Merge remote-tracking branch 'origin/build/release-feed-tooling' into…
datlechin Oct 18, 2026
3768be3
ci(release): build, notarize and publish from v* tags in release.yml
datlechin Oct 18, 2026
44b2d54
ci(release): switch the changelog to towncrier fragments
datlechin Oct 18, 2026
cc8a140
ci(release): take versions from the tag, write What's New at build time
datlechin Oct 18, 2026
8d46b49
Merge remote-tracking branch 'origin/main' into ci/release-from-tags-…
datlechin Oct 18, 2026
ddd2002
Merge remote-tracking branch 'origin/main' into ci/release-from-tags-…
datlechin Oct 18, 2026
c4152eb
Merge remote-tracking branch 'origin/ci/release-from-tags-build' into…
datlechin Oct 18, 2026
d204a74
Merge remote-tracking branch 'origin/ci/release-from-tags-changelog' …
datlechin Oct 18, 2026
aee588f
ci(release): release from tags through release.yml
datlechin Oct 18, 2026
7e16e6d
ci(release): release from tags through release.yml
datlechin Oct 18, 2026
e6248a9
Merge remote-tracking branch 'origin/main' into ci/release-from-tags
datlechin Oct 18, 2026
7834321
ci(release): release from tags through release.yml
datlechin Oct 18, 2026
aac254b
ci(release): release from tags through release.yml
datlechin Oct 18, 2026
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
20 changes: 12 additions & 8 deletions .claude/rules/changelog.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
---
paths:
- "CHANGELOG.md"
- "changelog.d/**/*"
- "towncrier.toml"
---

# CHANGELOG
# Changelog

[Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Entries go under `[Unreleased]`.
Unreleased changes are towncrier fragments in `changelog.d/`. A release turns them into a `CHANGELOG.md` section with `scripts/release/notes.py assemble` (`RELEASING.md`, Notes).

- **Sections**, each at most once per version and in this order: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
- **An entry is one line naming the change**, not a sentence explaining it, under about 120 characters. Added and Changed name the new thing (`Open in Window for row inspector text fields.`); Fixed names the bug, not the fix (`Empty context menu when right-clicking a read-only inspector field.`). No file paths or type names; an issue reference goes at the end, `(#1234)`.
- **One file per user-visible change**, named `changelog.d/<PR>.<type>.md`: `<PR>` is the pull request that adds it, `<type>` is `added`, `changed`, `deprecated`, `removed`, `fixed` or `security`. A second entry of the same type in one PR adds `.<n>`: `3452.fixed.2.md`.
- **One line, at most 120 characters, ending with a period.** Added and Changed name the new thing (`Open in Window for row inspector text fields.`); Fixed names the bug, not the fix (`Empty context menu when right-clicking a read-only inspector field.`). No file paths or type names.
- **No refs, no credit, no `- ` prefix.** towncrier links the PR named by the file and adds the bullet; `notes.py` adds ` by @login` when the PR's author is not a maintainer.
- **One entry per thing a user would notice.** Defects with the same visible symptom share an entry.
- **No entry** for a fix to something still unreleased (fold it into its entry) or for a docs-only change.
- **Credits are added at release** by `scripts/ci/changelog_credits.py`: `(#PR by @user)` for outside contributors, `(#PR)` for maintainers. Never write the credit by hand, and reword a contributor's entry in their own PR so the credit stays theirs.
- **A version may open with a lead block** of at most six lines before its first `###`, naming what a reader would notice. The update window and the Sparkle feed show only that block.
- After any edit, `grep -n '^## \[' CHANGELOG.md` must still list every released heading.
- **No fragment** for a fix to something still unreleased (edit that fragment instead) or for a docs-only change.
- **Only a `chore(release): vX.Y.Z` PR edits `CHANGELOG.md`.**
- **The lead block**: at most 6 lines, X.Y.0 only, passed as `notes.py assemble --lead`. The update window and the Sparkle feed show only that block.
- **`[CRITICAL]`** on a heading only for a security fix or data loss, never on a beta.
- `scripts/ci/check-changelog.py` checks the fragments and the headings in Repo Hygiene, then `towncrier build --draft --version 0.0.0` renders them. In PR title, `--pr` checks that a PR into main names each new fragment after itself and that only a release PR edits `CHANGELOG.md`.
46 changes: 29 additions & 17 deletions .claude/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,40 @@
paths:
- ".github/workflows/**/*"
- ".github/actions/**/*"
- ".github/scripts/**/*"
- ".github/actionlint.yaml"
- ".github/*baseline*"
- "scripts/ci/**/*"
- "scripts/release/**/*"
- ".swiftlint.yml"
- "**/.swiftlint.yml"
---

# CI

The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/skills/fix-issue/references/verification-apple.md`, Reading CI. The release and plugin builds: `RELEASING.md`.
The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/skills/fix-issue/references/verification-apple.md`, Reading CI. Release tags: `RELEASING.md`.

## Rulesets and required checks
## Rulesets

| Ruleset | Refs | Rules | Bypass |
|---|---|---|---|
| 25308039 "tags never move" | tags `v*`, `libs-*` | update, deletion | none |
| 23959123 "release tags are created by admins" | tags `v*`, `libs-*`, `plugin-*` | creation | admins |
| 25308049 "release branches are created by admins" | `release/*` | creation | admins |
| 23957966 "main and release branches require green gates" | default branch, `release/*` | the five required checks, `do_not_enforce_on_create` | none |
| 25129799 "branches keep their history" | default branch, `release/*`, `feeds` | deletion, non-fast-forward | none |

- `feeds` takes no required checks: CI pushes it with `GITHUB_TOKEN`, and the signed feed proves its content.
- 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)`, `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.

## Runners and toolchain

- Every Xcode job in the test and release workflows runs on the `xcode-27` runner label, a GitHub preview image. actionlint learns the label from `.github/actionlint.yaml`. `build-plugin.yml` picks its runner from the ref it builds (`RELEASING.md`).
- Every Xcode job in the test and release workflows runs on the `xcode-27` runner label, a GitHub preview image. actionlint learns the label from `.github/actionlint.yaml`. A stable tag builds only on a GA Xcode: `setup-xcode` with `require-release` fails on a beta or Release Candidate.
- The Xcode version is written once, in `.github/actions/setup-xcode/action.yml`, which runs `xcode-select --switch` and prints `xcodebuild -version`. A workflow never selects Xcode another way.
- XcodeGen comes from `.github/actions/setup-xcodegen`, pinned by version and SHA-256.
- `scripts/release/feed.py` runs Sparkle's `generate_appcast` and `sign_update` from `SPARKLE_BIN`, which `.github/actions/setup-sparkle` sets. The action reads the version and revision from the app's `Package.resolved` (`project.yml` pins Sparkle with `exactVersion`) and checks the archive against the checksum in Sparkle's `Package.swift` at that revision.
- The tag release (`build.yml`) signs the feed with the copy `scripts/ci/sign-and-appcast.sh` downloads, pinned by its own `SPARKLE_VERSION` and `SPARKLE_SHA256`. No check compares them with `project.yml`: a Sparkle bump changes all three.
- The compilation cache is off on CI (`COMPILATION_CACHE_ENABLE_CACHING=NO`); `Configs/Base.xcconfig` turns it on locally.

## macOS Tests (`macos-tests.yml`)
Expand All @@ -55,10 +62,16 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
- 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. They run `lipo -verify_arch` once per arch (arm64, x86_64), since Xcode 27's `lipo` takes one arch per call, then `verify-plugin-loads.sh`.
- `scripts/release/build.sh app --unsigned` archives the `TablePro` scheme; `build.sh plugins --unsigned` builds `RegistryPlugins` and copies the publish set, the targets `scripts/release/registry.py targets` reads from `.github/plugin-registry.json`, to `build/release/plugins`. Both build universal. They run `lipo -verify_arch` once per arch (arm64, x86_64), since Xcode 27's `lipo` takes one arch per call, then `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.

## Release, Withdraw and Pages

- A `v*` tag runs only `release.yml`; its jobs: `RELEASING.md`, What a tag runs. `withdraw.yml` runs only by dispatch, and `pages.yml` runs from both or by dispatch. Their environments allow only `v*` tags, so a dispatch names one with `--ref`.
- Jobs read the tag, version, build and channel only from `verify` outputs passed through `env:`, never through `${{ }}` inside `run:`.
- The default permission is `contents: read`, and each job lists every scope it needs. A job that calls `pages.yml` grants `pages: write` and `id-token: write`, since a called workflow can only lower what it receives.

## 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 @@ -77,22 +90,23 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
## PR title (`pr-title.yml`)

- Runs on opened, edited, reopened and synchronize, so every head commit gets the check.
- Runs `scripts/ci/check-pr-title.py`, then `scripts/check-banned-words.sh` on the PR's added lines and on its body. The banned-words step runs both and exits with the worse result.
- Runs `scripts/ci/check-pr-title.py`, then `scripts/ci/check-changelog.py --pr` with `GH_TOKEN` (`pull-requests: read`), then `scripts/check-banned-words.sh` on the PR's added lines and on its body. The banned-words step runs both and exits with the worse result.

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

- 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`;
- the tooling tests: `scripts/ci/test_*.py`, `scripts/lib/test_*.py`, `scripts/test_localization.py`, `.github/scripts/test_update_registry.py` and two shell tests;
- the tooling tests: `scripts/ci/test_*.py`, `scripts/lib/test_*.py`, `scripts/test_localization.py` and `scripts/ci/test_libs_release.sh`;
- `scripts/release/test_*.py` on Python 3.9 from `actions/setup-python`, in a venv that installs `scripts/release/requirements.txt` with `pip --require-hashes`;
- in the same venv, `check-changelog.py` and `towncrier build --draft --version 0.0.0`, which fails on a fragment name it cannot parse;
- `check-lib-sourcing.py`, `check-plugin-manifest.py`, `check-plugin-provides.py`, `check-deployment-target.sh`, `check-ios-shared-isolation.py`;
- `localization.py plugins` and `plugins --target ios`;
- `check-string-catalogs.py`, `check-license-inventory.py`, `check-entitlements.py`, `check-test-plans.py`;
- `check-pluginkit-bump-cadence.py`;
- `scripts/audit-refactor-health.sh --check`, `check-hub-ratchet.py`;
- the Go checks on the HANA helper, when the `hana` detector is true.
- the Go checks on the HANA helper, `govulncheck` among them, when the `hana` detector is true. New advisories between changes arrive as Dependabot alerts.
- `audit-refactor-health.sh --check` fails a file defining a production `DatabaseType` outside `Packages/TableProCore/Sources/TableProCoreTypes/DatabaseType.swift`, unless `.github/duplicate-contract-baseline.txt` lists its path. Moving a listed file moves its baseline path in the same commit. Never add an entry to let a new copy pass.
- `check-hub-ratchet.py` fails when an app hub grows past `.github/hub-ratchet-baseline.json`. `--update` only lowers a value, so growing one is a hand edit a reviewer sees.

Expand All @@ -106,22 +120,20 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
- `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.
- A release push that touches anything else runs the suites, and the tag waits for them.
- `CHANGELOG.md` and `changelog.d/` are on no suite's pattern: the What's New phase writes nothing at `0.0.0-dev`, and Repo Hygiene renders every fragment and section instead.

## 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 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`.
- 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 changelog-only push skips the suites through the detector.
- A `v*` tag runs only `release.yml`. The test workflows never run on a tag.
- `workflow_dispatch` exists on every workflow except `pr-title.yml` and `release.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. 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` restores that cache and never saves it, since a save in tag scope is unreadable from other tags.
- `release.yml` restores that cache and never saves it, since a save in tag scope is unreadable from other refs.
- `release-build.yml` restores it and leaves the save to macOS Tests.
- 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
1 change: 1 addition & 0 deletions .claude/rules/data-sync-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ paths:
This path holds user data. A silent wrong answer here looks exactly like an empty database, so prove late-completion and cancellation behavior with a test.

- **A synced CKRecord type or field reaches CloudKit Production before anything writes it.** Both apps pin the container to Production, where nothing is auto-created, and CloudKit rejects a whole record that carries an undeclared field. Write fields only through `record.fields(SomeSyncField.self)`, never `record["..."]`. To ship one: add it in Development, deploy to Production in the Console, run `scripts/export-cloudkit-schema.sh`, commit `CloudKit/production-schema.ckdb`, then add it to the matching `verifiedInProduction`. `ProductionSchemaParityTests` checks code against the snapshot in both directions, not against the server, so never edit the snapshot by hand: `scripts/export-cloudkit-schema.sh --check` compares it with Production.
- **A storage format stays readable by the previous stable.** Sparkle never downgrades, and a beta user can turn betas off (`RELEASING.md`, Channels).
- **Persist, then notify.** `SyncChangeTracker.markDeleted()` runs after the storage write, because its notification can start a sync that re-uploads the record from the stale file.
- **Concurrent schema loads await the one in-flight `loadTask`** in `SQLSchemaProvider`; never guard with a flag that returns without data.
- **Every metadata read goes through `DatabaseManager.withMetadataDriver`**, never `MetadataConnectionPool.shared` directly, so an embedded engine with `supportsConnectionPooling = false` (DuckDB, PGlite) stays on its session driver; a second connection to it is a different, empty database.
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/native-bridges.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ paths:
- **First-party bridge code lives in `Native/<X>Bridge`**, built by `scripts/build-<x>.sh`, with output in a gitignored folder beside the source, never in `Libs/`.
- **Rust may run in the app process; Go may not.** The Dameng staticlib is built with `panic = "unwind"`, and every export that does driver work wraps it in `catch_unwind`; a new export that does work takes the same guard. Go's `recover()` cannot catch a panic on a goroutine go-hdb starts, so HANA runs out of process as `tablepro-hana-helper`, one per driver session.
- **Toolchains and upstream code are pinned**: `rust-toolchain.toml`, `Cargo.lock` with `--locked`, the `toolchain` line of `go.mod`, `go.sum`. A protocol change for Dameng lands on the [TableProApp/rust-dameng](https://github.com/TableProApp/rust-dameng) fork first, then the pinned `rev` moves.
- **Every nested Mach-O is signed with Developer ID, the hardened runtime and a timestamp** before the main binary; `scripts/build-plugin.sh` does it and checks it with `codesign -dvvv`.
- **Every nested Mach-O is signed with Developer ID, the hardened runtime and a timestamp** before the main binary. The `HanaHelperEmbedding` copy phase in `project.yml` signs the HANA helper with the plugin's identity, and `scripts/release/build.sh plugins` checks every executable with `codesign -dvvv`.
Loading
Loading