Skip to content

Build Plugin

Build Plugin #294

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
permissions:
contents: write
env:
XCODE_PROJECT: TablePro.xcodeproj
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@v4
- name: Validate registry update script
run: python3 .github/scripts/test_update_registry.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=$(echo "$ITEM" | xargs)
TAG=$(echo "$ITEM" | cut -d: -f1)
PKV=$(echo "$ITEM" | cut -d: -f2 -s)
if [ -z "$PKV" ]; then
PKV="$DEFAULT_PKV"
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
runs-on: macos-26
timeout-minutes: 30
strategy:
matrix: ${{ fromJson(needs.resolve-tags.outputs.matrix) }}
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
lfs: true
- name: Pull LFS files
run: git lfs pull
- name: Select Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.4.1"
- name: Download static libraries
env:
GH_TOKEN: ${{ github.token }}
run: scripts/download-libs.sh
- name: Import signing certificate
env:
CERTIFICATES_P12: ${{ secrets.CERTIFICATES_P12 }}
CERTIFICATES_PASSWORD: ${{ secrets.CERTIFICATES_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
security create-keychain -p "" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "" "$KEYCHAIN_PATH"
echo "$CERTIFICATES_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 -P "$CERTIFICATES_PASSWORD" \
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: -k "" "$KEYCHAIN_PATH"
security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain
- name: Configure notarization
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }}
run: |
xcrun notarytool store-credentials "TablePro" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$NOTARY_PASSWORD"
- name: Resolve plugin info
id: plugin
env:
MATRIX_TAG: ${{ matrix.tag }}
run: |
TAG="$MATRIX_TAG"
PLUGIN_NAME=$(echo "$TAG" | sed -E 's/^plugin-([a-z0-9-]+)-v([0-9].*)$/\1/')
VERSION=$(echo "$TAG" | sed -E 's/^plugin-([a-z0-9-]+)-v([0-9].*)$/\2/')
case "$PLUGIN_NAME" in
oracle)
TARGET="OracleDriver"; BUNDLE_ID="com.TablePro.OracleDriver"
DISPLAY_NAME="Oracle Driver"; SUMMARY="Oracle Database 12c+ driver via OracleNIO"
DB_TYPE_IDS='["Oracle"]'; ICON="server.rack"; BUNDLE_NAME="OracleDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/oracle" ;;
clickhouse)
TARGET="ClickHouseDriver"; BUNDLE_ID="com.TablePro.ClickHouseDriver"
DISPLAY_NAME="ClickHouse Driver"; SUMMARY="ClickHouse OLAP database driver via HTTP interface"
DB_TYPE_IDS='["ClickHouse"]'; ICON="chart.bar.xaxis"; BUNDLE_NAME="ClickHouseDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/clickhouse" ;;
sqlite)
TARGET="SQLiteDriver"; BUNDLE_ID="com.TablePro.SQLiteDriver"
DISPLAY_NAME="SQLite Driver"; SUMMARY="SQLite embedded database driver"
DB_TYPE_IDS='["SQLite"]'; ICON="internaldrive"; BUNDLE_NAME="SQLiteDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/sqlite" ;;
duckdb)
TARGET="DuckDBDriver"; BUNDLE_ID="com.TablePro.DuckDBDriver"
DISPLAY_NAME="DuckDB Driver"; SUMMARY="DuckDB analytical database driver"
DB_TYPE_IDS='["DuckDB"]'; ICON="bird"; BUNDLE_NAME="DuckDBDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/duckdb" ;;
cassandra)
TARGET="CassandraDriver"; BUNDLE_ID="com.TablePro.CassandraDriver"
DISPLAY_NAME="Cassandra Driver"; SUMMARY="Apache Cassandra and ScyllaDB driver via DataStax C driver"
DB_TYPE_IDS='["Cassandra","ScyllaDB"]'; ICON="cassandra-icon"; BUNDLE_NAME="CassandraDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/cassandra" ;;
etcd)
TARGET="EtcdDriverPlugin"; BUNDLE_ID="com.TablePro.EtcdDriverPlugin"
DISPLAY_NAME="etcd Driver"; SUMMARY="etcd v3 key-value store driver with prefix-tree browsing and lease management"
DB_TYPE_IDS='["etcd"]'; ICON="etcd-icon"; BUNDLE_NAME="EtcdDriverPlugin"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/etcd" ;;
mssql)
TARGET="MSSQLDriver"; BUNDLE_ID="com.TablePro.MSSQLDriver"
DISPLAY_NAME="MSSQL Driver"; SUMMARY="Microsoft SQL Server driver via FreeTDS"
DB_TYPE_IDS='["SQL Server"]'; ICON="mssql-icon"; BUNDLE_NAME="MSSQLDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/mssql" ;;
mongodb)
TARGET="MongoDBDriver"; BUNDLE_ID="com.TablePro.MongoDBDriver"
DISPLAY_NAME="MongoDB Driver"; SUMMARY="MongoDB document database driver via libmongoc"
DB_TYPE_IDS='["MongoDB"]'; ICON="mongodb-icon"; BUNDLE_NAME="MongoDBDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/mongodb" ;;
redis)
TARGET="RedisDriver"; BUNDLE_ID="com.TablePro.RedisDriver"
DISPLAY_NAME="Redis Driver"; SUMMARY="Redis in-memory data store driver via hiredis"
DB_TYPE_IDS='["Redis"]'; ICON="redis-icon"; BUNDLE_NAME="RedisDriver"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/redis" ;;
cloudflare-d1)
TARGET="CloudflareD1DriverPlugin"; BUNDLE_ID="com.TablePro.CloudflareD1DriverPlugin"
DISPLAY_NAME="Cloudflare D1 Driver"; SUMMARY="Cloudflare D1 serverless SQLite-compatible database driver via REST API"
DB_TYPE_IDS='["Cloudflare D1"]'; ICON="cloudflare-d1-icon"; BUNDLE_NAME="CloudflareD1DriverPlugin"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/cloudflare-d1" ;;
libsql)
TARGET="LibSQLDriverPlugin"; BUNDLE_ID="com.TablePro.LibSQLDriverPlugin"
DISPLAY_NAME="libSQL / Turso Driver"; SUMMARY="libSQL and Turso database support via Hrana HTTP protocol"
DB_TYPE_IDS='["libSQL","Turso"]'; ICON="libsql-icon"; BUNDLE_NAME="LibSQLDriverPlugin"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/libsql" ;;
dynamodb)
TARGET="DynamoDBDriverPlugin"; BUNDLE_ID="com.TablePro.DynamoDBDriverPlugin"
DISPLAY_NAME="DynamoDB Driver"; SUMMARY="Amazon DynamoDB driver with PartiQL queries and AWS IAM/Profile/SSO authentication"
DB_TYPE_IDS='["DynamoDB"]'; ICON="dynamodb-icon"; BUNDLE_NAME="DynamoDBDriverPlugin"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/dynamodb" ;;
bigquery)
TARGET="BigQueryDriverPlugin"; BUNDLE_ID="com.TablePro.BigQueryDriverPlugin"
DISPLAY_NAME="BigQuery Driver"; SUMMARY="Google BigQuery analytics database driver via REST API"
DB_TYPE_IDS='["BigQuery"]'; ICON="bigquery-icon"; BUNDLE_NAME="BigQueryDriverPlugin"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/bigquery" ;;
snowflake)
TARGET="SnowflakeDriverPlugin"; BUNDLE_ID="com.TablePro.SnowflakeDriverPlugin"
DISPLAY_NAME="Snowflake Driver"; SUMMARY="Snowflake cloud data warehouse driver via the connector REST protocol"
DB_TYPE_IDS='["Snowflake"]'; ICON="snowflake-icon"; BUNDLE_NAME="SnowflakeDriverPlugin"
CATEGORY="database-driver"; HOMEPAGE="https://docs.tablepro.app/databases/snowflake" ;;
xlsx)
TARGET="XLSXExport"; BUNDLE_ID="com.TablePro.XLSXExportPlugin"
DISPLAY_NAME="XLSX Export"; SUMMARY="Export data to Microsoft Excel XLSX format"
DB_TYPE_IDS='null'; ICON="doc.richtext"; BUNDLE_NAME="XLSXExport"
CATEGORY="export-format"; HOMEPAGE="https://docs.tablepro.app/features/export" ;;
mql)
TARGET="MQLExport"; BUNDLE_ID="com.TablePro.MQLExportPlugin"
DISPLAY_NAME="MQL Export"; SUMMARY="Export MongoDB data as MQL statements"
DB_TYPE_IDS='null'; ICON="doc.text"; BUNDLE_NAME="MQLExport"
CATEGORY="export-format"; HOMEPAGE="https://docs.tablepro.app/features/export" ;;
sqlimport)
TARGET="SQLImport"; BUNDLE_ID="com.TablePro.SQLImportPlugin"
DISPLAY_NAME="SQL Import"; SUMMARY="Import data from SQL dump files"
DB_TYPE_IDS='null'; ICON="square.and.arrow.down"; BUNDLE_NAME="SQLImport"
CATEGORY="import-format"; HOMEPAGE="https://docs.tablepro.app/features/import" ;;
*)
echo "::error::Unknown plugin name: $PLUGIN_NAME"
exit 1 ;;
esac
# The app target is the only one with a real version; every plugin,
# test, and framework target is pinned to MARKETING_VERSION = 1.0,
# and target order in the pbxproj is not stable.
MIN_APP_VERSION=$(grep -E 'MARKETING_VERSION\s*=\s*[0-9]' \
TablePro.xcodeproj/project.pbxproj \
| sed 's/.*MARKETING_VERSION = \(.*\);/\1/' | tr -d ' ' \
| grep -v '^1\.0$' | sort -u | head -1)
if [ -z "$MIN_APP_VERSION" ] || [ "$MIN_APP_VERSION" = "1.0" ]; then
echo "::error::Could not resolve the app MARKETING_VERSION (got '$MIN_APP_VERSION')"
exit 1
fi
DISTINCT_COUNT=$(grep -E 'MARKETING_VERSION\s*=\s*[0-9]' \
TablePro.xcodeproj/project.pbxproj \
| sed 's/.*MARKETING_VERSION = \(.*\);/\1/' | tr -d ' ' \
| grep -v '^1\.0$' | sort -u | wc -l | tr -d ' ')
if [ "$DISTINCT_COUNT" != "1" ]; then
echo "::error::Expected exactly one non-1.0 MARKETING_VERSION, found $DISTINCT_COUNT"
exit 1
fi
{
echo "target=$TARGET"
echo "bundleId=$BUNDLE_ID"
echo "displayName=$DISPLAY_NAME"
echo "summary=$SUMMARY"
echo "dbTypeIds=$DB_TYPE_IDS"
echo "icon=$ICON"
echo "bundleName=$BUNDLE_NAME"
echo "category=$CATEGORY"
echo "homepage=$HOMEPAGE"
echo "version=$VERSION"
echo "minAppVersion=$MIN_APP_VERSION"
} >> "$GITHUB_OUTPUT"
- name: Build Cassandra dependencies
if: ${{ contains(matrix.tag, 'plugin-cassandra-') }}
run: ./scripts/build-cassandra.sh both
- name: Build plugin binaries
env:
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
./scripts/build-plugin.sh "${{ steps.plugin.outputs.target }}" arm64 "${{ steps.plugin.outputs.version }}"
./scripts/build-plugin.sh "${{ steps.plugin.outputs.target }}" x86_64 "${{ steps.plugin.outputs.version }}"
- 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"
- name: Notarize
if: ${{ env.NOTARIZE_PLUGINS == 'true' }}
run: |
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
for zip in build/Plugins/${BUNDLE_NAME}-*.zip; do
xcrun notarytool submit "$zip" --keychain-profile "TablePro" --wait
done
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
MATRIX_TAG: ${{ matrix.tag }}
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
run: |
TAG="$MATRIX_TAG"
DISPLAY_NAME="${{ steps.plugin.outputs.displayName }}"
VERSION="${{ steps.plugin.outputs.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 release view "$TAG" --json body --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
gh release delete "$TAG" --yes 2>/dev/null || true
gh release create "$TAG" \
--title "$DISPLAY_NAME v$VERSION" \
--notes "$RELEASE_BODY" \
build/Plugins/${BUNDLE_NAME}-arm64.zip \
build/Plugins/${BUNDLE_NAME}-x86_64.zip
- 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
if: ${{ env.REGISTRY_DEPLOY_KEY != '' }}
env:
REGISTRY_DEPLOY_KEY: ${{ secrets.REGISTRY_DEPLOY_KEY }}
GH_TOKEN: ${{ github.token }}
MATRIX_TAG: ${{ matrix.tag }}
MATRIX_PKV: ${{ matrix.pluginKitVersion }}
run: |
TAG="$MATRIX_TAG"
BUNDLE_NAME="${{ steps.plugin.outputs.bundleName }}"
BUNDLE_ID="${{ steps.plugin.outputs.bundleId }}"
DISPLAY_NAME="${{ steps.plugin.outputs.displayName }}"
VERSION="${{ steps.plugin.outputs.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 2
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)"