fix: address code review issues across SSH parser, groups, and CI #99
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build TablePro | ||
| on: | ||
| push: | ||
| tags: ["v*"] | ||
| paths-ignore: | ||
| - "**.md" | ||
| - "docs/**" | ||
| - ".vscode/**" | ||
| env: | ||
| XCODE_PROJECT: TablePro.xcodeproj | ||
| XCODE_SCHEME: TablePro | ||
| BUILD_CONFIGURATION: Release | ||
| jobs: | ||
| lint: | ||
| name: SwiftLint | ||
| runs-on: self-hosted | ||
| timeout-minutes: 10 | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| - name: Install SwiftLint | ||
| run: brew list swiftlint &>/dev/null || brew install swiftlint | ||
| - name: Run SwiftLint | ||
| run: swiftlint lint --strict | ||
| build-arm64: | ||
| name: Build ARM64 | ||
| runs-on: self-hosted | ||
| needs: lint | ||
| timeout-minutes: 20 | ||
| steps: | ||
| - name: Install Git LFS | ||
| run: brew list git-lfs &>/dev/null || brew install git-lfs; git lfs install | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| lfs: true | ||
| - name: Pull LFS files | ||
| run: git lfs pull | ||
| - name: Install ARM64 dependencies | ||
| run: | | ||
| echo "Installing ARM64 dependencies..." | ||
| # Check and install only if needed | ||
| if ! brew list mariadb-connector-c &>/dev/null; then | ||
| echo "📦 Installing mariadb-connector-c..." | ||
| brew install mariadb-connector-c | ||
| else | ||
| echo "✅ mariadb-connector-c already installed" | ||
| fi | ||
| # Link packages with --force and --overwrite (needed for keg-only formulas) | ||
| brew link --force --overwrite mariadb-connector-c 2>/dev/null || true | ||
| # Verify installations | ||
| if ! brew list mariadb-connector-c >/dev/null 2>&1; then | ||
| echo "❌ ERROR: mariadb-connector-c installation failed" | ||
| exit 1 | ||
| fi | ||
| echo "✅ ARM64 dependencies installed" | ||
| - name: Prepare libmariadb | ||
| run: | | ||
| echo "📦 Preparing libmariadb.a for arm64..." | ||
| cp Libs/libmariadb_arm64.a Libs/libmariadb.a | ||
| echo "✅ libmariadb.a ready" | ||
| lipo -info Libs/libmariadb.a | ||
| ls -lh Libs/libmariadb.a | ||
| - name: Prepare libpq | ||
| run: | | ||
| echo "📦 Preparing libpq + OpenSSL static libraries for arm64..." | ||
| for lib in libpq libpgcommon libpgport libssl libcrypto; do | ||
| cp "Libs/${lib}_arm64.a" "Libs/${lib}.a" | ||
| done | ||
| echo "✅ libpq + OpenSSL libraries ready" | ||
| ls -lh Libs/lib{pq,pgcommon,pgport,ssl,crypto}.a | ||
| - name: Verify Xcode | ||
| run: | | ||
| echo "Active Xcode:" | ||
| xcode-select -p | ||
| xcodebuild -version | ||
| - name: Create Secrets.xcconfig | ||
| env: | ||
| ANALYTICS_HMAC_SECRET: ${{ secrets.ANALYTICS_HMAC_SECRET }} | ||
| run: echo "ANALYTICS_HMAC_SECRET = ${ANALYTICS_HMAC_SECRET}" > Secrets.xcconfig | ||
| - name: Build ARM64 | ||
| env: | ||
| ANALYTICS_HMAC_SECRET: ${{ secrets.ANALYTICS_HMAC_SECRET }} | ||
| run: | | ||
| chmod +x scripts/build-release.sh | ||
| scripts/build-release.sh arm64 | ||
| - name: Verify build | ||
| run: | | ||
| echo "Verifying build output..." | ||
| BINARY_PATH="build/Release/TablePro-arm64.app/Contents/MacOS/TablePro" | ||
| # Check binary exists | ||
| if [ ! -f "$BINARY_PATH" ]; then | ||
| echo "❌ ERROR: Built binary not found at: $BINARY_PATH" | ||
| echo "Build may have failed silently" | ||
| exit 1 | ||
| fi | ||
| # Check it's not empty | ||
| if [ ! -s "$BINARY_PATH" ]; then | ||
| echo "❌ ERROR: Binary file is empty" | ||
| exit 1 | ||
| fi | ||
| # Check architecture | ||
| ARCH_INFO=$(lipo -info "$BINARY_PATH") | ||
| echo "Architecture: $ARCH_INFO" | ||
| if ! echo "$ARCH_INFO" | grep -q "arm64"; then | ||
| echo "❌ ERROR: Binary does not contain arm64 architecture" | ||
| echo "Expected: arm64 only" | ||
| echo "Got: $ARCH_INFO" | ||
| exit 1 | ||
| fi | ||
| if echo "$ARCH_INFO" | grep -q "x86_64"; then | ||
| echo "❌ ERROR: Binary contains x86_64 but should be arm64 only" | ||
| exit 1 | ||
| fi | ||
| # Check it's executable | ||
| if [ ! -x "$BINARY_PATH" ]; then | ||
| echo "❌ ERROR: Binary is not executable" | ||
| exit 1 | ||
| fi | ||
| # Verify bundled dylibs | ||
| FRAMEWORKS_DIR="build/Release/TablePro-arm64.app/Contents/Frameworks" | ||
| if [ -d "$FRAMEWORKS_DIR" ]; then | ||
| echo "Bundled dynamic libraries:" | ||
| ls -lh "$FRAMEWORKS_DIR"/*.dylib 2>/dev/null || echo " (none)" | ||
| # Verify no Homebrew paths remain in the binary | ||
| if otool -L "$BINARY_PATH" | grep -q '/opt/homebrew/\|/usr/local/opt/'; then | ||
| echo "❌ ERROR: Binary still references Homebrew paths:" | ||
| otool -L "$BINARY_PATH" | grep '/opt/homebrew/\|/usr/local/opt/' | ||
| exit 1 | ||
| fi | ||
| echo "✅ No Homebrew path references in binary" | ||
| else | ||
| echo "⚠️ WARNING: No Frameworks directory found — dylibs may not be bundled" | ||
| fi | ||
| # Display info | ||
| echo "✅ Build verified successfully" | ||
| echo "Binary size: $(ls -lh "$BINARY_PATH" | awk '{print $5}')" | ||
| echo "App bundle size: $(du -sh build/Release/TablePro-arm64.app | awk '{print $1}')" | ||
| - name: Create DMG installer | ||
| run: | | ||
| echo "Creating DMG installer..." | ||
| # Install create-dmg tool for proper icon handling in CI | ||
| echo "📦 Installing create-dmg tool..." | ||
| brew list create-dmg &>/dev/null || brew install create-dmg | ||
| # Make DMG creation script executable | ||
| chmod +x scripts/create-dmg.sh | ||
| # Create DMG with version from git tag or use default | ||
| # The script handles app renaming internally | ||
| VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.1.13") | ||
| echo "📌 Using version: $VERSION" | ||
| scripts/create-dmg.sh "$VERSION" "arm64" "build/Release/TablePro-arm64.app" | ||
| # Verify DMG was created - check for the specific file or any arm64 DMG | ||
| DMG_FILE="build/Release/TablePro-${VERSION}-arm64.dmg" | ||
| if [ -f "$DMG_FILE" ]; then | ||
| echo "✅ DMG installer created successfully: $DMG_FILE" | ||
| else | ||
| echo "⚠️ Expected DMG not found at: $DMG_FILE" | ||
| echo "📂 Checking for any DMG files in build/Release/:" | ||
| ls -la build/Release/*.dmg 2>/dev/null || echo " No DMG files found" | ||
| # Check if any arm64 DMG was created (version might differ) | ||
| if ls build/Release/*-arm64.dmg 1>/dev/null 2>&1; then | ||
| echo "✅ Found arm64 DMG file(s):" | ||
| ls -lh build/Release/*-arm64.dmg | ||
| else | ||
| echo "❌ ERROR: No arm64 DMG file was created" | ||
| exit 1 | ||
| fi | ||
| fi | ||
| ls -lh build/Release/*.dmg | ||
| - name: Create ZIP archive | ||
| run: | | ||
| echo "Creating ZIP archive..." | ||
| cd build/Release | ||
| # Use ditto to preserve framework symlinks (zip -r resolves them, | ||
| # which breaks code signature validation and Sparkle updates) | ||
| if ! ditto -c -k --sequesterRsrc --keepParent TablePro-arm64.app TablePro-arm64.zip; then | ||
| echo "❌ ERROR: Failed to create ZIP archive" | ||
| exit 1 | ||
| fi | ||
| echo "✅ ZIP archive created" | ||
| ls -lh TablePro-arm64.zip | ||
| - name: Stage artifacts for release job | ||
| run: | | ||
| STAGING="/tmp/tablepro-artifacts-${{ github.sha }}" | ||
| mkdir -p "$STAGING" | ||
| cp build/Release/*.dmg "$STAGING/" 2>/dev/null || true | ||
| cp build/Release/TablePro-arm64.zip "$STAGING/" 2>/dev/null || true | ||
| echo "✅ ARM64 artifacts staged to $STAGING" | ||
| ls -lh "$STAGING" | ||
| build-x86_64: | ||
| name: Build x86_64 | ||
| runs-on: self-hosted | ||
| needs: lint | ||
| timeout-minutes: 20 | ||
| steps: | ||
| - name: Install Git LFS | ||
| run: brew list git-lfs &>/dev/null || brew install git-lfs; git lfs install | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| lfs: true | ||
| - name: Pull LFS files | ||
| run: git lfs pull | ||
| - name: Install Rosetta 2 | ||
| run: | | ||
| if ! arch -x86_64 /usr/bin/true 2>/dev/null; then | ||
| echo "Installing Rosetta 2..." | ||
| if ! softwareupdate --install-rosetta --agree-to-license; then | ||
| echo "❌ ERROR: Failed to install Rosetta 2" | ||
| exit 1 | ||
| fi | ||
| # Verify Rosetta 2 works | ||
| if ! arch -x86_64 /usr/bin/true 2>/dev/null; then | ||
| echo "❌ ERROR: Rosetta 2 installed but not functional" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Rosetta 2 installed" | ||
| else | ||
| echo "✅ Rosetta 2 already installed" | ||
| fi | ||
| - name: Install x86_64 Homebrew | ||
| run: | | ||
| if [ ! -f /usr/local/bin/brew ]; then | ||
| echo "Installing x86_64 Homebrew..." | ||
| if ! arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"; then | ||
| echo "❌ ERROR: Homebrew installation failed" | ||
| exit 1 | ||
| fi | ||
| if [ ! -f /usr/local/bin/brew ]; then | ||
| echo "❌ ERROR: Homebrew not found after installation" | ||
| exit 1 | ||
| fi | ||
| if ! /usr/local/bin/brew --version; then | ||
| echo "❌ ERROR: Homebrew not functional" | ||
| exit 1 | ||
| fi | ||
| echo "✅ x86_64 Homebrew installed" | ||
| else | ||
| echo "x86_64 Homebrew already installed" | ||
| if ! /usr/local/bin/brew --version; then | ||
| echo "❌ ERROR: Homebrew not functional" | ||
| exit 1 | ||
| fi | ||
| fi | ||
| - name: Install x86_64 dependencies | ||
| run: | | ||
| echo "Installing x86_64 dependencies..." | ||
| # Check and install only if needed | ||
| if ! arch -x86_64 /usr/local/bin/brew list mariadb-connector-c &>/dev/null; then | ||
| echo "📦 Installing mariadb-connector-c (x86_64)..." | ||
| arch -x86_64 /usr/local/bin/brew install mariadb-connector-c | ||
| else | ||
| echo "✅ mariadb-connector-c (x86_64) already installed" | ||
| fi | ||
| # Link packages with --force (needed for keg-only formulas) | ||
| arch -x86_64 /usr/local/bin/brew link --force --overwrite mariadb-connector-c 2>/dev/null || true | ||
| # Verify installations | ||
| if ! arch -x86_64 /usr/local/bin/brew list mariadb-connector-c >/dev/null 2>&1; then | ||
| echo "❌ ERROR: mariadb-connector-c installation failed" | ||
| exit 1 | ||
| fi | ||
| echo "✅ x86_64 dependencies installed" | ||
| - name: Prepare libmariadb | ||
| run: | | ||
| echo "📦 Preparing libmariadb.a for x86_64..." | ||
| cp Libs/libmariadb_x86_64.a Libs/libmariadb.a | ||
| echo "✅ libmariadb.a ready" | ||
| lipo -info Libs/libmariadb.a | ||
| ls -lh Libs/libmariadb.a | ||
| - name: Prepare libpq | ||
| run: | | ||
| echo "📦 Preparing libpq + OpenSSL static libraries for x86_64..." | ||
| for lib in libpq libpgcommon libpgport libssl libcrypto; do | ||
| cp "Libs/${lib}_x86_64.a" "Libs/${lib}.a" | ||
| done | ||
| echo "✅ libpq + OpenSSL libraries ready" | ||
| ls -lh Libs/lib{pq,pgcommon,pgport,ssl,crypto}.a | ||
| - name: Verify Xcode | ||
| run: | | ||
| echo "Active Xcode:" | ||
| xcode-select -p | ||
| xcodebuild -version | ||
| - name: Create Secrets.xcconfig | ||
| env: | ||
| ANALYTICS_HMAC_SECRET: ${{ secrets.ANALYTICS_HMAC_SECRET }} | ||
| run: echo "ANALYTICS_HMAC_SECRET = ${ANALYTICS_HMAC_SECRET}" > Secrets.xcconfig | ||
| - name: Build x86_64 | ||
| env: | ||
| ANALYTICS_HMAC_SECRET: ${{ secrets.ANALYTICS_HMAC_SECRET }} | ||
| run: | | ||
| chmod +x scripts/build-release.sh | ||
| scripts/build-release.sh x86_64 | ||
| - name: Verify build | ||
| run: | | ||
| echo "Verifying build output..." | ||
| BINARY_PATH="build/Release/TablePro-x86_64.app/Contents/MacOS/TablePro" | ||
| # Check binary exists | ||
| if [ ! -f "$BINARY_PATH" ]; then | ||
| echo "❌ ERROR: Built binary not found at: $BINARY_PATH" | ||
| exit 1 | ||
| fi | ||
| # Check it's not empty | ||
| if [ ! -s "$BINARY_PATH" ]; then | ||
| echo "❌ ERROR: Binary file is empty" | ||
| exit 1 | ||
| fi | ||
| # Check architecture | ||
| ARCH_INFO=$(lipo -info "$BINARY_PATH") | ||
| echo "Architecture: $ARCH_INFO" | ||
| if ! echo "$ARCH_INFO" | grep -q "x86_64"; then | ||
| echo "❌ ERROR: Binary does not contain x86_64 architecture" | ||
| echo "Expected: x86_64 only" | ||
| echo "Got: $ARCH_INFO" | ||
| exit 1 | ||
| fi | ||
| if echo "$ARCH_INFO" | grep -q "arm64"; then | ||
| echo "❌ ERROR: Binary contains arm64 but should be x86_64 only" | ||
| exit 1 | ||
| fi | ||
| # Check it's executable | ||
| if [ ! -x "$BINARY_PATH" ]; then | ||
| echo "❌ ERROR: Binary is not executable" | ||
| exit 1 | ||
| fi | ||
| # Verify bundled dylibs | ||
| FRAMEWORKS_DIR="build/Release/TablePro-x86_64.app/Contents/Frameworks" | ||
| if [ -d "$FRAMEWORKS_DIR" ]; then | ||
| echo "Bundled dynamic libraries:" | ||
| ls -lh "$FRAMEWORKS_DIR"/*.dylib 2>/dev/null || echo " (none)" | ||
| # Verify no Homebrew paths remain in the binary | ||
| if otool -L "$BINARY_PATH" | grep -q '/opt/homebrew/\|/usr/local/opt/'; then | ||
| echo "❌ ERROR: Binary still references Homebrew paths:" | ||
| otool -L "$BINARY_PATH" | grep '/opt/homebrew/\|/usr/local/opt/' | ||
| exit 1 | ||
| fi | ||
| echo "✅ No Homebrew path references in binary" | ||
| else | ||
| echo "⚠️ WARNING: No Frameworks directory found — dylibs may not be bundled" | ||
| fi | ||
| # Display info | ||
| echo "✅ Build verified successfully" | ||
| echo "Binary size: $(ls -lh "$BINARY_PATH" | awk '{print $5}')" | ||
| echo "App bundle size: $(du -sh build/Release/TablePro-x86_64.app | awk '{print $1}')" | ||
| - name: Create DMG installer | ||
| run: | | ||
| echo "Creating DMG installer..." | ||
| # Install create-dmg tool for proper icon handling in CI | ||
| echo "📦 Installing create-dmg tool..." | ||
| brew list create-dmg &>/dev/null || brew install create-dmg | ||
| # Make DMG creation script executable | ||
| chmod +x scripts/create-dmg.sh | ||
| # Create DMG with version from git tag or use default | ||
| # The script handles app renaming internally | ||
| VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.1.13") | ||
| echo "📌 Using version: $VERSION" | ||
| scripts/create-dmg.sh "$VERSION" "x86_64" "build/Release/TablePro-x86_64.app" | ||
| # Verify DMG was created - check for the specific file or any x86_64 DMG | ||
| DMG_FILE="build/Release/TablePro-${VERSION}-x86_64.dmg" | ||
| if [ -f "$DMG_FILE" ]; then | ||
| echo "✅ DMG installer created successfully: $DMG_FILE" | ||
| else | ||
| echo "⚠️ Expected DMG not found at: $DMG_FILE" | ||
| echo "📂 Checking for any DMG files in build/Release/:" | ||
| ls -la build/Release/*.dmg 2>/dev/null || echo " No DMG files found" | ||
| # Check if any x86_64 DMG was created (version might differ) | ||
| if ls build/Release/*-x86_64.dmg 1>/dev/null 2>&1; then | ||
| echo "✅ Found x86_64 DMG file(s):" | ||
| ls -lh build/Release/*-x86_64.dmg | ||
| else | ||
| echo "❌ ERROR: No x86_64 DMG file was created" | ||
| exit 1 | ||
| fi | ||
| fi | ||
| ls -lh build/Release/*.dmg | ||
| - name: Create ZIP archive | ||
| run: | | ||
| echo "Creating ZIP archive..." | ||
| cd build/Release | ||
| # Use ditto to preserve framework symlinks (zip -r resolves them, | ||
| # which breaks code signature validation and Sparkle updates) | ||
| if ! ditto -c -k --sequesterRsrc --keepParent TablePro-x86_64.app TablePro-x86_64.zip; then | ||
| echo "❌ ERROR: Failed to create ZIP archive" | ||
| exit 1 | ||
| fi | ||
| echo "✅ ZIP archive created" | ||
| ls -lh TablePro-x86_64.zip | ||
| - name: Stage artifacts for release job | ||
| run: | | ||
| STAGING="/tmp/tablepro-artifacts-${{ github.sha }}" | ||
| mkdir -p "$STAGING" | ||
| cp build/Release/*.dmg "$STAGING/" 2>/dev/null || true | ||
| cp build/Release/TablePro-x86_64.zip "$STAGING/" 2>/dev/null || true | ||
| echo "✅ x86_64 artifacts staged to $STAGING" | ||
| ls -lh "$STAGING" | ||
| release: | ||
| name: Create GitHub Release | ||
| runs-on: self-hosted | ||
| needs: [build-arm64, build-x86_64] | ||
| if: startsWith(github.ref, 'refs/tags/v') | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Collect staged artifacts | ||
| run: | | ||
| STAGING="/tmp/tablepro-artifacts-${{ github.sha }}" | ||
| mkdir -p artifacts/ | ||
| cp "$STAGING"/* artifacts/ | ||
| echo "✅ Artifacts collected from $STAGING" | ||
| ls -lh artifacts/ | ||
| - name: Verify and organize artifacts for release | ||
| run: | | ||
| VERSION=${GITHUB_REF#refs/tags/v} | ||
| if [ -z "$VERSION" ]; then | ||
| echo "❌ ERROR: Failed to extract version from ref: $GITHUB_REF" | ||
| exit 1 | ||
| fi | ||
| echo "Preparing artifacts for version: $VERSION" | ||
| echo "Contents of artifacts directory:" | ||
| ls -la artifacts/ | ||
| # Note: DMG files should already have correct names from build | ||
| # ZIP files need to be renamed | ||
| # Rename ZIP files if they exist | ||
| if [ -f "artifacts/TablePro-arm64.zip" ]; then | ||
| mv artifacts/TablePro-arm64.zip "artifacts/TablePro-${VERSION}-arm64.zip" | ||
| fi | ||
| if [ -f "artifacts/TablePro-x86_64.zip" ]; then | ||
| mv artifacts/TablePro-x86_64.zip "artifacts/TablePro-${VERSION}-x86_64.zip" | ||
| fi | ||
| echo "✅ Artifacts organized successfully" | ||
| echo "Final artifacts:" | ||
| ls -lh artifacts/ | ||
| - name: Sign update archives with Sparkle | ||
| if: env.SPARKLE_PRIVATE_KEY != '' | ||
| env: | ||
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | ||
| run: | | ||
| VERSION=${GITHUB_REF#refs/tags/v} | ||
| # Install Sparkle tools (Cask — binaries in Caskroom, not on PATH) | ||
| brew list --cask sparkle &>/dev/null || brew install --cask sparkle | ||
| SPARKLE_BIN="$(brew --caskroom)/sparkle/$(ls "$(brew --caskroom)/sparkle" | head -1)/bin" | ||
| ARM64_ZIP="artifacts/TablePro-${VERSION}-arm64.zip" | ||
| X86_64_ZIP="artifacts/TablePro-${VERSION}-x86_64.zip" | ||
| # Sign each ZIP with EdDSA using sign_update | ||
| KEY_FILE=$(mktemp) | ||
| echo "$SPARKLE_PRIVATE_KEY" > "$KEY_FILE" | ||
| ARM64_SIG=$("$SPARKLE_BIN/sign_update" "$ARM64_ZIP" -f "$KEY_FILE") | ||
| X86_64_SIG=$("$SPARKLE_BIN/sign_update" "$X86_64_ZIP" -f "$KEY_FILE") | ||
| rm -f "$KEY_FILE" | ||
| # Parse signature and length from sign_update output | ||
| # Output format: sparkle:edSignature="..." length="..." | ||
| ARM64_ED_SIG=$(echo "$ARM64_SIG" | sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p') | ||
| ARM64_LENGTH=$(echo "$ARM64_SIG" | sed -n 's/.*length="\([^"]*\)".*/\1/p') | ||
| X86_64_ED_SIG=$(echo "$X86_64_SIG" | sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p') | ||
| X86_64_LENGTH=$(echo "$X86_64_SIG" | sed -n 's/.*length="\([^"]*\)".*/\1/p') | ||
| # Extract version info from the top-level app's Info.plist inside the ZIP | ||
| # Use -maxdepth 3 to avoid nested framework plists (e.g. Sparkle.framework) | ||
| TEMP_DIR=$(mktemp -d) | ||
| unzip -q "$ARM64_ZIP" -d "$TEMP_DIR" | ||
| INFO_PLIST=$(find "$TEMP_DIR" -maxdepth 3 -path "*/Contents/Info.plist" | head -1) | ||
| if [ -n "$INFO_PLIST" ] && [ -f "$INFO_PLIST" ]; then | ||
| BUILD_NUMBER=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$INFO_PLIST" 2>/dev/null || echo "1") | ||
| SHORT_VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$INFO_PLIST" 2>/dev/null || echo "$VERSION") | ||
| MIN_OS=$(/usr/libexec/PlistBuddy -c "Print :LSMinimumSystemVersion" "$INFO_PLIST" 2>/dev/null || echo "13.5") | ||
| else | ||
| echo "⚠️ Could not find app Info.plist in ZIP, using defaults from tag" | ||
| BUILD_NUMBER="1" | ||
| SHORT_VERSION="$VERSION" | ||
| MIN_OS="13.5" | ||
| fi | ||
| rm -rf "$TEMP_DIR" | ||
| # Extract release notes from CHANGELOG.md and convert to HTML for appcast | ||
| NOTES=$(awk -v ver="$VERSION" ' | ||
| /^## \[/ { | ||
| if (found) exit | ||
| if ($0 ~ "\\[" ver "\\]") { found=1; next } | ||
| } | ||
| found { print } | ||
| ' CHANGELOG.md) | ||
| if [ -z "$NOTES" ]; then | ||
| RELEASE_HTML="<li>Bug fixes and improvements</li>" | ||
| else | ||
| # Convert markdown to simple HTML: | ||
| # ### Header -> <h3>Header</h3> | ||
| # - item -> <li>item</li> | ||
| # Wrap consecutive <li> runs in <ul>...</ul> | ||
| RELEASE_HTML=$(echo "$NOTES" | sed -E \ | ||
| -e 's/^### (.+)$/<h3>\1<\/h3>/' \ | ||
| -e 's/^- (.+)$/<li>\1<\/li>/' \ | ||
| -e '/^[[:space:]]*$/d' \ | ||
| | awk ' | ||
| /<li>/ { | ||
| if (!in_list) { print "<ul>"; in_list=1 } | ||
| print; next | ||
| } | ||
| { | ||
| if (in_list) { print "</ul>"; in_list=0 } | ||
| } | ||
| END { if (in_list) print "</ul>" } | ||
| ') | ||
| fi | ||
| # Wrap in a styled HTML body | ||
| DESCRIPTION_HTML="<body style=\"font-family: -apple-system, sans-serif; font-size: 13px; padding: 8px;\">${RELEASE_HTML}</body>" | ||
| # Build appcast.xml with architecture-specific items (Sparkle 2 convention) | ||
| # Each item has sparkle:architectures on the enclosure so the client | ||
| # automatically picks the matching architecture | ||
| DOWNLOAD_PREFIX="https://github.com/datlechin/TablePro/releases/download/v${VERSION}" | ||
| PUB_DATE=$(date -u '+%a, %d %b %Y %H:%M:%S +0000') | ||
| mkdir -p appcast | ||
| cat > appcast/appcast.xml << APPCAST_EOF | ||
| <?xml version="1.0" standalone="yes"?> | ||
| <rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0"> | ||
| <channel> | ||
| <title>TablePro</title> | ||
| <item> | ||
| <title>${SHORT_VERSION}</title> | ||
| <pubDate>${PUB_DATE}</pubDate> | ||
| <sparkle:version>${BUILD_NUMBER}</sparkle:version> | ||
| <sparkle:shortVersionString>${SHORT_VERSION}</sparkle:shortVersionString> | ||
| <sparkle:minimumSystemVersion>${MIN_OS}</sparkle:minimumSystemVersion> | ||
| <description><![CDATA[${DESCRIPTION_HTML}]]></description> | ||
| <enclosure url="${DOWNLOAD_PREFIX}/TablePro-${VERSION}-arm64.zip" length="${ARM64_LENGTH}" type="application/octet-stream" sparkle:edSignature="${ARM64_ED_SIG}" sparkle:architectures="arm64"/> | ||
| </item> | ||
| <item> | ||
| <title>${SHORT_VERSION}</title> | ||
| <pubDate>${PUB_DATE}</pubDate> | ||
| <sparkle:version>${BUILD_NUMBER}</sparkle:version> | ||
| <sparkle:shortVersionString>${SHORT_VERSION}</sparkle:shortVersionString> | ||
| <sparkle:minimumSystemVersion>${MIN_OS}</sparkle:minimumSystemVersion> | ||
| <description><![CDATA[${DESCRIPTION_HTML}]]></description> | ||
| <enclosure url="${DOWNLOAD_PREFIX}/TablePro-${VERSION}-x86_64.zip" length="${X86_64_LENGTH}" type="application/octet-stream" sparkle:edSignature="${X86_64_ED_SIG}" sparkle:architectures="x86_64"/> | ||
| </item> | ||
| </channel> | ||
| </rss> | ||
| APPCAST_EOF | ||
| echo "✅ Appcast generated with architecture-specific items:" | ||
| cat appcast/appcast.xml | ||
| - name: Upload appcast artifact | ||
| if: env.SPARKLE_PRIVATE_KEY != '' | ||
| uses: actions/upload-artifact@v4 | ||
| env: | ||
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | ||
| with: | ||
| name: appcast-${{ github.sha }} | ||
| path: appcast/appcast.xml | ||
| retention-days: 90 | ||
| - name: Commit appcast.xml to repo | ||
| if: env.SPARKLE_PRIVATE_KEY != '' | ||
| continue-on-error: true | ||
| env: | ||
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | ||
| run: | | ||
| if [ ! -f appcast/appcast.xml ]; then | ||
| echo "⚠️ No appcast.xml to commit" | ||
| exit 0 | ||
| fi | ||
| cp appcast/appcast.xml appcast.xml | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
| git fetch origin main | ||
| git checkout main | ||
| git add appcast.xml | ||
| git diff --cached --quiet && echo "No changes to appcast.xml" && exit 0 | ||
| git commit -m "Update appcast.xml for v${GITHUB_REF#refs/tags/v}" | ||
| git push origin main | ||
| - name: Extract release notes from CHANGELOG.md | ||
| run: | | ||
| VERSION=${GITHUB_REF#refs/tags/v} | ||
| echo "Extracting release notes for version: $VERSION" | ||
| # Extract the section for this version from CHANGELOG.md | ||
| # Matches from "## [X.Y.Z]" until the next "## [" or end of file | ||
| NOTES=$(awk -v ver="$VERSION" ' | ||
| /^## \[/ { | ||
| if (found) exit | ||
| if ($0 ~ "\\[" ver "\\]") { found=1; next } | ||
| } | ||
| found { print } | ||
| ' CHANGELOG.md) | ||
| if [ -z "$NOTES" ]; then | ||
| echo "⚠️ No changelog entry found for version $VERSION, using fallback" | ||
| echo "- Bug fixes and improvements" > release_notes.md | ||
| else | ||
| echo "$NOTES" > release_notes.md | ||
| fi | ||
| echo "✅ Release notes extracted" | ||
| cat release_notes.md | ||
| - name: Create GitHub Release | ||
| uses: softprops/action-gh-release@v1 | ||
| with: | ||
| files: | | ||
| artifacts/*.dmg | ||
| artifacts/*.zip | ||
| body_path: release_notes.md | ||
| draft: false | ||
| prerelease: ${{ contains(github.ref, '-beta') || contains(github.ref, '-alpha') || contains(github.ref, '-rc') }} | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Notify Telegram | ||
| if: success() && env.TELEGRAM_BOT_TOKEN != '' | ||
| env: | ||
| TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} | ||
| TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} | ||
| TELEGRAM_TOPIC_ID: ${{ secrets.TELEGRAM_TOPIC_ID }} | ||
| run: | | ||
| VERSION=${GITHUB_REF#refs/tags/v} | ||
| RELEASE_URL="https://github.com/datlechin/TablePro/releases/tag/v${VERSION}" | ||
| # Build message with release notes | ||
| NOTES=$(cat release_notes.md 2>/dev/null || echo "Bug fixes and improvements") | ||
| read -r -d '' TEXT <<MSG_EOF || true | ||
| *TablePro v${VERSION} Released* | ||
| ${NOTES} | ||
| [View Release](${RELEASE_URL}) | ||
| MSG_EOF | ||
| PAYLOAD=$(jq -n \ | ||
| --arg chat_id "$TELEGRAM_CHAT_ID" \ | ||
| --arg text "$TEXT" \ | ||
| --arg parse_mode "Markdown" \ | ||
| --arg topic_id "$TELEGRAM_TOPIC_ID" \ | ||
| '{ | ||
| chat_id: $chat_id, | ||
| text: $text, | ||
| parse_mode: $parse_mode, | ||
| disable_web_page_preview: true | ||
| } + (if $topic_id != "" then {message_thread_id: ($topic_id | tonumber)} else {} end)') | ||
| curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d "$PAYLOAD" | ||
| - name: Clean up staging directory | ||
| if: always() | ||
| run: rm -rf "/tmp/tablepro-artifacts-${{ github.sha }}" || true | ||