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
4 changes: 2 additions & 2 deletions .claude/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
| 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 |
| 25129799 "branches keep their history" | default branch, `release/*`, `gh-pages` | deletion, non-fast-forward | none |

- `feeds` takes no required checks: CI pushes it with `GITHUB_TOKEN`, and the signed feed proves its content.
- `gh-pages` takes no required checks (`RELEASING.md`, Keys and environments).
- 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`).
Expand Down
2 changes: 1 addition & 1 deletion .claude/rules/plugin-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ TableProPluginKit is built with Library Evolution, so an already-built plugin ke
## Structure

- PluginKit-free logic a plugin shares with the iOS app lives in a TableProCore product, such as `TableProMySQLCore`, `TableProPostgreSQLCore` or `TableProRedisCore`.
- A new driver needs: its `project.yml` target and a `RegistryPlugins` entry (`BundledPlugins` and the app's `copy: { destination: plugins }` dependency for a bundled one), a `DatabaseType` constant, for a registry one an entry in `.github/plugin-registry.json` keyed by its target, with `name` and `iconName` (a bundled one stays out of it), a curated snapshot in `PluginMetadataRegistry` (without it the type never appears in the New Connection picker), its `TableProProvides*` keys (below), a row in `../tablepro-docs/databases/index.mdx` through a paired docs PR, and a changelog fragment.
- A new driver needs: its `project.yml` target and a `RegistryPlugins` entry (`BundledPlugins` and the app's `copy: { destination: plugins }` dependency for a bundled one), a `DatabaseType` constant, for a registry one an entry in `.github/plugin-registry.json` keyed by its target, with `name` (a bundled one stays out of it), a curated snapshot in `PluginMetadataRegistry` (without it the type never appears in the New Connection picker), its `TableProProvides*` keys (below), a row in `../tablepro-docs/databases/index.mdx` through a paired docs PR, and a changelog fragment.
- A new engine ships as a registry plugin (`AGENTS.md`, Scope).
- `PluginManager` registers a plugin from the `TableProProvidesDatabaseTypeIds`, `TableProProvidesExportFormatIds` and `TableProProvidesImportFormatIds` arrays in its `Info.plist` and loads its code on first use. A plugin with none of them loads at launch. The first database type id is the primary one.
- `scripts/ci/check-plugin-provides.py` checks those arrays against the class's `databaseTypeId`, `additionalDatabaseTypeIds` and `formatId` in Repo Hygiene; `--write` stamps them.
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Before any decision, gather:
- the tags, newest first: `git -c versionsort.suffix=-beta tag -l 'v*' --sort=-v:refname | head`;
- the live lines: `git ls-remote --heads origin 'release/*'`;
- open regressions: `gh issue list --label regression --state open`;
- open Dependabot alerts: `gh api repos/TableProApp/TablePro/dependabot/alerts -q '.[] | select(.state == "open") | .security_advisory.summary'`;
- open Dependabot alerts: `gh api --paginate 'repos/TableProApp/TablePro/dependabot/alerts?state=open&per_page=100' -q '.[].security_advisory.summary'`;
- main's push runs: `gh run list --branch main --event push --limit 10`, with macOS Tests, iOS and Package Tests and Release Build green on the commit you would cut;
- whether `main` has the last stable's `## [X.Y.Z]` heading. If not, the forward-port is missing and the next tag will refuse to build: do that first (section 9).

Expand Down Expand Up @@ -70,7 +70,7 @@ Run: `RELEASING.md`, Notes, then Cutting a stable or patch.

- Follow the run with `gh run list --workflow release.yml --limit 3` and `gh run watch <id>`.
- Sort a failure into infrastructure (runner, network, notary service, an expired certificate) or code (a build, test or check the tag's commit fails). Infrastructure is a re-run; code is a fix on `main`, a pick, and the next number.
- Confirm the result from the source, not the cached site: `gh api -H 'Accept: application/vnd.github.raw' 'repos/TableProApp/TablePro/contents/appcast.xml?ref=feeds' | grep -c '<item>'`, and `gh release view v<version> --json assets -q '.assets | length'`.
- Confirm the result from the source, not the cached site: `gh api -H 'Accept: application/vnd.github.raw' 'repos/TableProApp/TablePro/contents/appcast.xml?ref=gh-pages' | grep -c '<sparkle:shortVersionString><version></sparkle:shortVersionString>'` prints 1, and `gh release view v<version> --json assets -q '.assets | length'`.

Run: `RELEASING.md`, What a tag runs, and When a run fails.

Expand Down
5 changes: 5 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ self-hosted-runner:
# GitHub's Xcode 27 preview image, newer than the label list in actionlint 1.7.12.
labels:
- xcode-27
paths:
.github/workflows/*.yml:
ignore:
# actionlint 1.7.12 does not know concurrency.queue yet (rhysd/actionlint#657).
- 'unexpected key "queue" for "concurrency" section'
2 changes: 1 addition & 1 deletion .github/actions/setup-xcodegen/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ runs:
echo "XcodeGen $XCODEGEN_VERSION already installed"
exit 0
fi
curl -sSLo "$RUNNER_TEMP/xcodegen.zip" \
curl -fsSLo "$RUNNER_TEMP/xcodegen.zip" \
"https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip"
echo "${XCODEGEN_SHA256} $RUNNER_TEMP/xcodegen.zip" | shasum -a 256 -c -
unzip -q "$RUNNER_TEMP/xcodegen.zip" -d "$RUNNER_TEMP/xcodegen-dist"
Expand Down
78 changes: 26 additions & 52 deletions .github/plugin-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,108 +2,82 @@
"retainFromPluginKit": 39,
"plugins": {
"BeancountDriver": {
"name": "Beancount Driver",
"iconName": "beancount-icon"
"name": "Beancount Driver"
},
"BigQueryDriverPlugin": {
"name": "BigQuery Driver",
"iconName": "bigquery-icon"
"name": "BigQuery Driver"
},
"BigtableDriverPlugin": {
"name": "Bigtable Driver",
"iconName": "bigtable-icon"
"name": "Bigtable Driver"
},
"CassandraDriver": {
"name": "Cassandra Driver",
"iconName": "cassandra-icon"
"name": "Cassandra Driver"
},
"CloudflareD1DriverPlugin": {
"name": "Cloudflare D1 Driver",
"iconName": "cloudflare-d1-icon"
"name": "Cloudflare D1 Driver"
},
"CloudflareR2SQLDriverPlugin": {
"name": "Cloudflare R2 SQL Driver",
"iconName": "cloudflare-r2-sql-icon"
"name": "Cloudflare R2 SQL Driver"
},
"DamengDriver": {
"name": "Dameng Driver",
"iconName": "cylinder"
"name": "Dameng Driver"
},
"DuckDBDriver": {
"name": "DuckDB Driver",
"iconName": "duckdb-icon"
"name": "DuckDB Driver"
},
"DynamoDBDriverPlugin": {
"name": "DynamoDB Driver",
"iconName": "dynamodb-icon"
"name": "DynamoDB Driver"
},
"ElasticsearchDriverPlugin": {
"name": "Elasticsearch Driver",
"iconName": "elasticsearch-icon"
"name": "Elasticsearch Driver"
},
"EtcdDriverPlugin": {
"name": "etcd Driver",
"iconName": "etcd-icon"
"name": "etcd Driver"
},
"FirebaseDriverPlugin": {
"name": "Firebase Driver",
"iconName": "firebase-icon"
"name": "Firebase Driver"
},
"HanaDriver": {
"name": "SAP HANA Driver",
"iconName": "cylinder"
"name": "SAP HANA Driver"
},
"KafkaDriverPlugin": {
"name": "Kafka Driver",
"iconName": "kafka-icon"
"name": "Kafka Driver"
},
"LibSQLDriverPlugin": {
"name": "libSQL / Turso Driver",
"iconName": "libsql-icon"
"name": "libSQL / Turso Driver"
},
"MSSQLDriver": {
"name": "MSSQL Driver",
"iconName": "mssql-icon"
"name": "MSSQL Driver"
},
"MongoDBDriver": {
"name": "MongoDB Driver",
"iconName": "mongodb-icon"
"name": "MongoDB Driver"
},
"OracleDriver": {
"name": "Oracle Driver",
"iconName": "oracle-icon"
"name": "Oracle Driver"
},
"ParquetExport": {
"name": "Parquet Export",
"iconName": "square.grid.3x3"
"name": "Parquet Export"
},
"SnowflakeDriverPlugin": {
"name": "Snowflake Driver",
"iconName": "snowflake-icon"
"name": "Snowflake Driver"
},
"SpannerDriverPlugin": {
"name": "Spanner Driver",
"iconName": "spanner-icon"
"name": "Spanner Driver"
},
"SurrealDBDriverPlugin": {
"name": "SurrealDB Driver",
"iconName": "surrealdb-icon"
"name": "SurrealDB Driver"
},
"TeradataDriver": {
"name": "Teradata Driver",
"iconName": "teradata-icon"
"name": "Teradata Driver"
},
"TrinoDriverPlugin": {
"name": "Trino Driver",
"iconName": "trino-icon"
"name": "Trino Driver"
},
"TypesenseDriverPlugin": {
"name": "Typesense Driver",
"iconName": "typesense-icon"
"name": "Typesense Driver"
},
"WeaviateDriverPlugin": {
"name": "Weaviate Driver",
"iconName": "weaviate-icon"
"name": "Weaviate Driver"
}
}
}
7 changes: 4 additions & 3 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Pages

# Deploys the feeds branch to updates.tablepro.app. A branch-source Pages site would not do: a push
# Deploys the gh-pages branch to updates.tablepro.app. A branch-source Pages site would not do: a push
# made with GITHUB_TOKEN starts no Pages build. Dispatch it at a v* tag, which github-pages requires.

on:
Expand All @@ -18,18 +18,19 @@ jobs:
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
# Never cancelled. A newer pending deploy replaces an older pending one, and both deploy the feeds head.
# Deploys wait in turn instead of replacing each other, so a caller's later jobs (release.yml notify) always run.
concurrency:
group: pages
cancel-in-progress: false
queue: max
permissions:
contents: read
pages: write
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: feeds
ref: gh-pages
persist-credentials: false

- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
Expand Down
80 changes: 57 additions & 23 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
name: Release

# A v* tag builds, signs and notarizes the app and every registry plugin, publishes the GitHub
# release, then adds it to the feeds branch and deploys that to updates.tablepro.app.
# release, then adds it to the gh-pages branch and deploys that to updates.tablepro.app.

on:
push:
tags: ['v*']

# Never cancelled: the build jobs hold notary submissions, and every step after them is idempotent,
# so a re-run of the same tag finishes what the first run left.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

Expand Down Expand Up @@ -212,6 +206,8 @@ jobs:
attestations: write
env:
TAG: ${{ needs.verify.outputs.tag }}
BUILD: ${{ needs.verify.outputs.build }}
CHANNEL: ${{ needs.verify.outputs.channel }}
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -231,35 +227,68 @@ jobs:
name: notes
path: notes

# A published release is immutable, so every asset goes on the draft and is checked there first.
- name: Draft the release
id: draft
run: python3 scripts/release/publish.py draft "$TAG" --notes notes/body.md dist/* >> "$GITHUB_OUTPUT"
- name: Check for a published release
id: release
run: |
if gh api "repos/{owner}/{repo}/releases/tags/$TAG" --silent 2> /dev/null; then
echo published=true >> "$GITHUB_OUTPUT"
fi

- name: Attest the assets
if: steps.draft.outputs.state == 'draft'
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
if: steps.release.outputs.published != 'true'
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*

# gh release create drafts, attaches every asset, then publishes: the order an immutable release needs.
- name: Publish the release
run: python3 scripts/release/publish.py release "$TAG"
if: steps.release.outputs.published != 'true'
run: |
set -euo pipefail
# A draft a killed attempt left would shadow the release in gh's lookups by tag.
if [ "$(gh release view "$TAG" --json isDraft --jq .isDraft 2> /dev/null)" = true ]; then
gh release delete "$TAG" --yes
fi
flags=(--prerelease --latest=false)
if [ "$CHANNEL" = stable ]; then
latest="$(gh release view --json tagName --jq .tagName)"
latest_build="$(python3 scripts/release/version.py parse "$latest" 2> /dev/null | sed -n 's/^build=//p' || true)"
flags=(--latest="$([ "$BUILD" -gt "${latest_build:-0}" ] && echo true || echo false)")
fi
gh release create "$TAG" --verify-tag --title "$TAG" --notes-file notes/body.md "${flags[@]}" dist/*

- name: Verify the release
run: |
set -euo pipefail
gh release verify "$TAG"
for asset in dist/*; do
gh release verify-asset "$TAG" "$asset"
done

feed:
name: Feed
needs: [verify, github-release]
runs-on: macos-latest
environment: release
timeout-minutes: 20
concurrency:
group: gh-pages
queue: max
permissions:
contents: write
attestations: read
env:
TAG: ${{ needs.verify.outputs.tag }}
BUILD: ${{ needs.verify.outputs.build }}
CHANNEL: ${{ needs.verify.outputs.channel }}
CRITICAL: ${{ needs.verify.outputs.critical }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: gh-pages
path: build/gh-pages

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

Expand All @@ -276,8 +305,17 @@ jobs:
set -euo pipefail
flags=()
[ "$CRITICAL" != true ] || flags+=(--critical)
python3 scripts/release/feed.py add "$TAG" --build "$BUILD" --channel "$CHANNEL" \
--notes notes/feed.md ${flags[@]+"${flags[@]}"}
python3 scripts/release/feed.py add "$TAG" --site build/gh-pages --notes notes/feed.md \
${flags[@]+"${flags[@]}"}

- name: Push gh-pages
working-directory: build/gh-pages
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add appcast.xml plugins/v3.json
git diff --cached --quiet || { git commit -m "release: $TAG" && git push origin HEAD:gh-pages; }

pages:
name: Pages
Expand Down Expand Up @@ -312,7 +350,6 @@ jobs:
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
TELEGRAM_TOPIC_ID: ${{ secrets.TELEGRAM_TOPIC_ID }}
run: |
set -euo pipefail
{
Expand All @@ -323,8 +360,5 @@ jobs:
fi
printf '\n%s/%s/releases/tag/%s\n' "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$TAG"
} > msg.txt
topic=()
[ -z "$TELEGRAM_TOPIC_ID" ] || topic=(--data-urlencode "message_thread_id=$TELEGRAM_TOPIC_ID")
curl -fsS --retry 3 -o /dev/null --data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
--data-urlencode text@msg.txt ${topic[@]+"${topic[@]}"} \
"https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage"
--data-urlencode text@msg.txt "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage"
2 changes: 1 addition & 1 deletion .github/workflows/repo-hygiene.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
- name: Check every script sources the libraries it calls into
run: python3 scripts/ci/check-lib-sourcing.py

- name: Check the plugin manifest against the plugin classes
- name: Check plugin-registry.json against the project.yml plugin aggregates
run: python3 scripts/ci/check-plugin-manifest.py

# The app registers a plugin from these keys without loading it, and loads one without them at launch.
Expand Down
Loading
Loading