Skip to content

Update appcast.xml for v0.80.0 #420

Update appcast.xml for v0.80.0

Update appcast.xml for v0.80.0 #420

Workflow file for this run

name: Build Plugin
on:
push:
tags: ["plugin-*-v*"]
workflow_dispatch:
inputs:
tags:
description: >
Comma-separated plugin tag:pluginKitVersion pairs.
pluginKitVersion defaults to currentPluginKitVersion from PluginManager.swift.
Examples: plugin-mongodb-v1.0.25, plugin-mongodb-v1.0.25:13
required: true
type: string
baseRef:
description: >
Build the plugin from this ref instead of the default branch, so its binary links against
that release's PluginKit and declares that release's TableProPluginKitVersion. Use a
shipped app tag (v0.73.0) to publish a driver fix users on that release can install.
Leave empty to build from the branch the workflow runs on.
required: false
type: string
# Declared per job rather than here. resolve-tags reads two integers out of a Swift file and needs
# nothing, and a workflow-level grant hands it a write token it has no use for.
permissions:
contents: read
# Without this, two runs for the same tag race each other through a delete-and-recreate of the same
# GitHub Release. Never cancel one in flight: it holds a notarization submission open with Apple and
# publishes release assets.
concurrency:
group: build-plugin-${{ github.ref }}
cancel-in-progress: false
jobs:
resolve-tags:
name: Resolve Plugin Tags
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.tags.outputs.matrix }}
currentPluginKitVersion: ${{ steps.pkv.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Validate registry update script
run: python3 .github/scripts/test_update_registry.py
- name: Check the plugin manifest against the plugin classes
run: python3 scripts/ci/check-plugin-manifest.py
- name: Read currentPluginKitVersion
id: pkv
run: |
VERSION=$(grep -E 'static let currentPluginKitVersion\s*=\s*[0-9]+' \
TablePro/Core/Plugins/PluginManager.swift \
| grep -oE '[0-9]+$' | head -1)
if [ -z "$VERSION" ]; then
echo "::error::Could not parse currentPluginKitVersion from PluginManager.swift"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "currentPluginKitVersion=$VERSION"
- name: Build matrix
id: tags
env:
DEFAULT_PKV: ${{ steps.pkv.outputs.version }}
INPUT_TAGS: ${{ inputs.tags }}
REF_NAME: ${{ github.ref_name }}
run: |
if [ -n "$INPUT_TAGS" ]; then
IFS=',' read -ra RAW_TAGS <<< "$INPUT_TAGS"
else
RAW_TAGS=("$REF_NAME")
fi
JSON='{"include":['
FIRST=true
for ITEM in "${RAW_TAGS[@]}"; do
ITEM="${ITEM#"${ITEM%%[![:space:]]*}"}"
ITEM="${ITEM%"${ITEM##*[![:space:]]}"}"
TAG=$(echo "$ITEM" | cut -d: -f1)
PKV=$(echo "$ITEM" | cut -d: -f2 -s)
if [ -z "$PKV" ]; then
PKV="$DEFAULT_PKV"
fi
# Both values are checked here, on ubuntu, before any macOS job exists to import a
# signing certificate. TAG reaches a shell and PKV is spliced unquoted into the JSON
# below, and neither had ever been validated: nothing downstream checks the version
# format either, and update-registry.py takes --version as an arbitrary string, so a
# malformed tag publishes a garbage version into the plugins.json every app reads.
# Checked against all 366 existing plugin-* tags: every one matches.
if [[ ! "$TAG" =~ ^plugin-[a-z0-9-]+-v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Malformed plugin tag: $TAG"
exit 1
fi
if [[ ! "$PKV" =~ ^[0-9]+$ ]]; then
echo "::error::Non-numeric pluginKitVersion for $TAG: $PKV"
exit 1
fi
if [ "$FIRST" = true ]; then FIRST=false; else JSON+=','; fi
JSON+="{\"tag\":\"$TAG\",\"pluginKitVersion\":$PKV}"
done
JSON+=']}'
echo "matrix=$JSON" >> "$GITHUB_OUTPUT"
echo "Matrix: $JSON"
build-plugin:
name: "Build ${{ matrix.tag }} (PluginKit ${{ matrix.pluginKitVersion }})"
needs: resolve-tags
# A dispatch runs under main's workflow group, so a tag push for the same plugin tag would
# otherwise stage its release at the same time.
concurrency:
group: build-plugin-release-${{ matrix.tag }}
cancel-in-progress: false
permissions:
contents: write
runs-on: macos-26
timeout-minutes: 30
strategy:
matrix: ${{ fromJson(needs.resolve-tags.outputs.matrix) }}
fail-fast: false
steps:
# A plugin's declared TableProPluginKitVersion and the symbols it links against both come
# from the tree it is built in, so they can only agree by building at the ref whose kit the
# binary should carry. Relabelling a binary built from main to an older kit would make an
# older app accept it and then fail Bundle.loadAndReturnError, which is #1917 and 0.49.0.
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
lfs: true
ref: ${{ inputs.baseRef || github.ref }}
- name: Pull LFS files
run: git lfs pull
- name: Select Xcode
uses: ./.github/actions/setup-xcode
- name: Setup XcodeGen
uses: ./.github/actions/setup-xcodegen
- name: Download static libraries
env:
GH_TOKEN: ${{ github.token }}
run: scripts/download-libs.sh
# No provisioning profile: a .tableplugin is signed with Developer ID and notarized, and
# never carries an embedded profile the way the app does.
- name: Set up Apple signing
uses: ./.github/actions/apple-signing
with:
certificate-p12: ${{ secrets.CERTIFICATES_P12 }}
certificate-password: ${{ secrets.CERTIFICATES_PASSWORD }}
apple-id: ${{ secrets.APPLE_ID }}
team-id: ${{ secrets.APPLE_TEAM_ID }}
notary-password: ${{ secrets.NOTARY_PASSWORD }}
- name: Resolve plugin info
id: plugin
env:
MATRIX_TAG: ${{ matrix.tag }}
# The metadata lives in .github/plugin-registry.json. It used to be a 120-line `case` block
# here, which is shell holding data, and seven of its icon values had drifted away from the
# plugin classes they describe with nothing to notice.
run: python3 scripts/ci/plugin-info.py "$MATRIX_TAG" >> "$GITHUB_OUTPUT"
- name: Build Dameng native bridge
if: ${{ contains(matrix.tag, 'plugin-dameng-') }}
run: ./scripts/build-dameng.sh both
- name: Set up Go for the SAP HANA helper
if: ${{ contains(matrix.tag, 'plugin-hana-') }}
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: Native/HanaBridge/go.mod
cache-dependency-path: Native/HanaBridge/go.sum
# Universal, because both architecture builds below embed the same helper. build-plugin.sh
# signs it with Developer ID, the hardened runtime and a timestamp, and checks all three.
- name: Build the SAP HANA helper
if: ${{ contains(matrix.tag, 'plugin-hana-') }}
run: ./scripts/build-hana.sh both
# NOTARIZE is not optional. Gatekeeper refuses to load an unnotarized plugin bundle
# into TablePro, so a build that skips it publishes a driver no user can open.
- name: Build plugin binaries
env:
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NOTARIZE: "true"
TARGET: ${{ steps.plugin.outputs.target }}
VERSION: ${{ steps.plugin.outputs.version }}
# One invocation, not one per architecture. The script has always taken "both", and
# each invocation regenerates both Xcode projects and wipes build/DerivedData, so calling
# it twice threw away everything the first architecture had compiled.
run: ./scripts/build-plugin.sh "$TARGET" both "$VERSION"
# Assert the result rather than trusting the flag. The previous notarization step was
# gated on an environment variable nothing defined, so it never ran and nothing noticed
# for every plugin release the registry has ever published.
# Before anything is published. The registry serves these to users directly, so a bundle
# that will not load is a plugin nobody can install.
- name: Verify the built bundles load
env:
BUNDLE_NAME: ${{ steps.plugin.outputs.bundleName }}
run: |
set -euo pipefail
for ARCH in arm64 x86_64; do
WORK="$(mktemp -d)"
unzip -oq "build/Plugins/${BUNDLE_NAME}-${ARCH}.zip" -d "$WORK"
# The distribution zip carries the plugin alone, so dyld has nothing to resolve
# @rpath/TableProPluginKit.framework against and every plugin fails for the wrong
# reason. build-plugin.sh builds both into one flat CONFIGURATION_BUILD_DIR, so the
# framework is put beside the unzipped bundle rather than added as a second search
# directory, which would make the script scan the build dir as another bundle.
cp -R "build/Plugins/${ARCH}/TableProPluginKit.framework" "$WORK/"
# Only the native slice can be dlopened on this runner; the other is checked by
# `lipo -info` having produced it and by Gatekeeper below.
if [ "$ARCH" = "arm64" ]; then
scripts/ci/verify-plugin-loads.sh "$WORK"
fi
rm -rf "$WORK"
done
- name: Verify the built bundles are notarized
run: |
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
for ARCH in arm64 x86_64; do
WORK=$(mktemp -d)
unzip -oq "build/Plugins/${BUNDLE_NAME}-${ARCH}.zip" -d "$WORK"
PLUGIN="$WORK/${BUNDLE_NAME}.tableplugin"
if ! xcrun stapler validate "$PLUGIN"; then
echo "::error::${BUNDLE_NAME}-${ARCH} carries no stapled notarization ticket. Gatekeeper refuses to load such a bundle into TablePro."
exit 1
fi
if ! spctl -a -vvv -t open --context context:primary-signature "$PLUGIN" 2>&1 | grep -q "source=Notarized Developer ID"; then
echo "::error::Gatekeeper rejects ${BUNDLE_NAME}-${ARCH}:"
spctl -a -vvv -t open --context context:primary-signature "$PLUGIN" || true
exit 1
fi
echo "Verified ${BUNDLE_NAME}-${ARCH}: notarized, stapled and accepted by Gatekeeper."
done
- name: Verify built PluginKit version matches the release label
env:
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
run: |
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
EXPECTED="$MATRIX_PKV"
for ARCH in arm64 x86_64; do
WORK=$(mktemp -d)
unzip -oq "build/Plugins/${BUNDLE_NAME}-${ARCH}.zip" -d "$WORK"
PLIST=$(find "$WORK" -path '*.tableplugin/Contents/Info.plist' | head -1)
if [ -z "$PLIST" ]; then
echo "::error::Could not find Info.plist in the built ${BUNDLE_NAME}-${ARCH} bundle."
exit 1
fi
ACTUAL=$(plutil -extract TableProPluginKitVersion raw "$PLIST")
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "::error::${BUNDLE_NAME}-${ARCH} was built for PluginKit $ACTUAL but this release is labeled PluginKit $EXPECTED. Refusing to publish a mislabeled binary. Re-release from a commit whose plugin Info.plist matches the target PluginKit version."
exit 1
fi
echo "Verified ${BUNDLE_NAME}-${ARCH}: built PluginKit $ACTUAL matches the release label."
done
- name: Read checksums
id: sha
run: |
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
ARM64_SHA=$(cat "build/Plugins/${BUNDLE_NAME}-arm64.zip.sha256")
X86_SHA=$(cat "build/Plugins/${BUNDLE_NAME}-x86_64.zip.sha256")
{
echo "arm64=$ARM64_SHA"
echo "x86_64=$X86_SHA"
} >> "$GITHUB_OUTPUT"
# The checkout above is `baseRef` when one is given, and a shipped app tag can predate
# draft-release.sh.
- name: Check out the release script
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
path: .release-tooling
sparse-checkout: scripts/ci
persist-credentials: false
# A published release is immutable, so both zips go on the draft first.
- name: Create the draft release
id: draft
env:
GH_TOKEN: ${{ github.token }}
MATRIX_TAG: ${{ matrix.tag }}
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
PLUGIN_VERSION: ${{ steps.plugin.outputs.version }}
run: |
TAG="$MATRIX_TAG"
DISPLAY_NAME="${{ steps.plugin.outputs.displayName }}"
VERSION="$PLUGIN_VERSION"
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
ARM64_SHA="${{ steps.sha.outputs.arm64 }}"
X86_SHA="${{ steps.sha.outputs.x86_64 }}"
PKV="$MATRIX_PKV"
RELEASE_BODY="## $DISPLAY_NAME v$VERSION
Plugin release for TablePro (PluginKit $PKV).
### Installation
TablePro will prompt you to install this plugin automatically when you select the database type. You can also install manually via Settings > Plugins > Browse.
### SHA-256
- ARM64: \`$ARM64_SHA\`
- x86_64: \`$X86_SHA\`"
EXISTING_PKV=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" --jq .body 2>/dev/null \
| grep -oiE 'PluginKit [0-9]+' | grep -oE '[0-9]+' | head -1 || true)
if [ -n "$EXISTING_PKV" ] && [ "$EXISTING_PKV" != "$PKV" ]; then
echo "::error::Release $TAG already exists for PluginKit $EXISTING_PKV. Refusing to overwrite it with PluginKit $PKV; publish the new ABI under a new plugin version."
exit 1
fi
# Tagged at the commit this job built, which is `baseRef` when one was given. Without
# --target GitHub tags the default branch's head instead, so a plugin built against a
# shipped release recorded whatever main had moved on to: the v0.75.0 plugins point at a
# commit carrying PluginKit 33, while their binaries are 32.
NOTES="$(mktemp)"
printf '%s' "$RELEASE_BODY" > "$NOTES"
.release-tooling/scripts/ci/draft-release.sh --target "$(git rev-parse HEAD)" \
"$TAG" "$DISPLAY_NAME v$VERSION" "$NOTES" \
"build/Plugins/${BUNDLE_NAME}-arm64.zip" "build/Plugins/${BUNDLE_NAME}-x86_64.zip"
# Without make_latest=false, publishing marks the plugin release Latest instead of the app's.
- name: Publish the release
if: steps.draft.outputs.id != ''
env:
GH_TOKEN: ${{ github.token }}
RELEASE_ID: ${{ steps.draft.outputs.id }}
run: |
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" \
-F draft=false -f make_latest=false --jq '"Published \(.tag_name): \(.html_url)"'
- name: Verify published assets match the PluginKit label
env:
MATRIX_TAG: ${{ matrix.tag }}
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
run: |
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
TAG="$MATRIX_TAG"
PKV="$MATRIX_PKV"
REPO="${{ github.repository }}"
for ARCH in arm64 x86_64; do
URL="https://github.com/${REPO}/releases/download/${TAG}/${BUNDLE_NAME}-${ARCH}.zip"
WORK=$(mktemp -d)
DOWNLOADED=""
for attempt in 1 2 3 4 5; do
if curl -fsSL "$URL" -o "$WORK/asset.zip"; then
DOWNLOADED="yes"
break
fi
echo "Published asset not downloadable yet (attempt $attempt/5), retrying in 5s..."
sleep 5
done
if [ -z "$DOWNLOADED" ]; then
echo "::error::Could not download the published asset at $URL to verify its PluginKit version."
exit 1
fi
unzip -oq "$WORK/asset.zip" -d "$WORK"
PLIST=$(find "$WORK" -path '*.tableplugin/Contents/Info.plist' | head -1)
if [ -z "$PLIST" ]; then
echo "::error::No .tableplugin Info.plist in the published ${BUNDLE_NAME}-${ARCH} asset."
exit 1
fi
ACTUAL=$(plutil -extract TableProPluginKitVersion raw "$PLIST")
if [ "$ACTUAL" != "$PKV" ]; then
echo "::error::Published ${BUNDLE_NAME}-${ARCH} at $URL is PluginKit $ACTUAL but the registry will record $PKV. Refusing to record a mismatched binary."
exit 1
fi
echo "Verified published ${BUNDLE_NAME}-${ARCH}: served PluginKit $ACTUAL matches the registry label."
done
- name: Update plugin registry
env:
REGISTRY_DEPLOY_KEY: ${{ secrets.REGISTRY_DEPLOY_KEY }}
GH_TOKEN: ${{ github.token }}
MATRIX_TAG: ${{ matrix.tag }}
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
PLUGIN_VERSION: ${{ steps.plugin.outputs.version }}
run: |
TAG="$MATRIX_TAG"
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
BUNDLE_ID="${{ steps.plugin.outputs.bundleId }}"
DISPLAY_NAME="${{ steps.plugin.outputs.displayName }}"
VERSION="$PLUGIN_VERSION"
SUMMARY="${{ steps.plugin.outputs.summary }}"
DB_TYPE_IDS='${{ steps.plugin.outputs.dbTypeIds }}'
MIN_APP_VERSION="${{ steps.plugin.outputs.minAppVersion }}"
ICON="${{ steps.plugin.outputs.icon }}"
HOMEPAGE="${{ steps.plugin.outputs.homepage }}"
CATEGORY="${{ steps.plugin.outputs.category }}"
ARM64_SHA="${{ steps.sha.outputs.arm64 }}"
X86_SHA="${{ steps.sha.outputs.x86_64 }}"
PKV="$MATRIX_PKV"
REPO="${{ github.repository }}"
ARM64_URL="https://github.com/${REPO}/releases/download/${TAG}/${BUNDLE_NAME}-arm64.zip"
X86_64_URL="https://github.com/${REPO}/releases/download/${TAG}/${BUNDLE_NAME}-x86_64.zip"
SCRIPT_PATH="$(pwd)/.github/scripts/update-registry.py"
WORK=$(mktemp -d)
eval "$(ssh-agent -s)"
trap 'ssh-add -D 2>/dev/null || true; eval "$(ssh-agent -k)" 2>/dev/null || true' EXIT
echo "$REGISTRY_DEPLOY_KEY" | ssh-add -
git clone git@github.com:TableProApp/plugins.git "$WORK/registry"
cd "$WORK/registry"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
MAX_RETRIES=10
for attempt in $(seq 1 $MAX_RETRIES); do
echo "Registry update attempt $attempt/$MAX_RETRIES"
git reset --hard origin/main
git pull --rebase origin main
python3 "$SCRIPT_PATH" \
--manifest plugins.json \
--id "$BUNDLE_ID" \
--name "$DISPLAY_NAME" \
--version "$VERSION" \
--summary "$SUMMARY" \
--db-type-ids "$DB_TYPE_IDS" \
--arm64-url "$ARM64_URL" \
--arm64-sha "$ARM64_SHA" \
--x86_64-url "$X86_64_URL" \
--x86_64-sha "$X86_SHA" \
--min-app-version "$MIN_APP_VERSION" \
--icon "$ICON" \
--homepage "$HOMEPAGE" \
--category "$CATEGORY" \
--plugin-kit-version "$PKV" \
--keep-kit-versions 3
git add plugins.json
git commit -m "Update $DISPLAY_NAME to v$VERSION (PluginKit $PKV)"
if git push; then
echo "Registry updated on attempt $attempt"
curl -sf "https://purge.jsdelivr.net/gh/TableProApp/plugins@main/plugins.json" >/dev/null \
&& echo "Purged jsDelivr cache for plugins.json" \
|| echo "::warning::jsDelivr purge request failed; the cache will refresh on its own shortly"
break
fi
if [ "$attempt" -eq "$MAX_RETRIES" ]; then
echo "::error::Failed to push registry update after $MAX_RETRIES attempts"
exit 1
fi
DELAY=$((2 + RANDOM % 4))
echo "Push rejected (concurrent update), retrying in ${DELAY}s..."
sleep "$DELAY"
done
echo "$DISPLAY_NAME v$VERSION released (PluginKit $PKV)"