fix: route Custom LLM provider through OpenAI-compatible endpoint #137
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 and Release | |
| on: | |
| push: | |
| branches-ignore: | |
| - main | |
| tags: | |
| - "v*.*.*" | |
| - "v*.*.*-beta.*" | |
| pull_request: | |
| branches: | |
| - main | |
| permissions: | |
| contents: write | |
| pages: write | |
| id-token: write | |
| env: | |
| XCODE_VERSION: "26.0.1" | |
| BUNDLE_ID: "com.ameba.TRex" | |
| APP_NAME: "TRex" | |
| BETA_FEED_URL: "https://amebalabs.github.io/TRex/appcast_beta.xml" | |
| PROD_FEED_URL: "https://amebalabs.github.io/TRex/appcast.xml" | |
| jobs: | |
| verify-release-notes: | |
| if: startsWith(github.ref, 'refs/tags/v') && github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_notes: ${{ steps.read_notes.outputs.content }} | |
| steps: | |
| - uses: actions/checkout@v3 | |
| - name: Get version | |
| id: get_version | |
| run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT | |
| - name: Check release notes exist | |
| id: check_notes | |
| run: | | |
| VERSION=${{ steps.get_version.outputs.VERSION }} | |
| NOTES_PATH="docs/release-notes/${VERSION}.md" | |
| if [ ! -f "$NOTES_PATH" ]; then | |
| echo "Error: Release notes not found for version ${VERSION}" | |
| echo "Expected path: ${NOTES_PATH}" | |
| exit 1 | |
| fi | |
| - name: Read release notes | |
| id: read_notes | |
| run: | | |
| VERSION=${{ steps.get_version.outputs.VERSION }} | |
| NOTES_PATH="docs/release-notes/${VERSION}.md" | |
| CONTENT=$(cat "$NOTES_PATH" | perl -p -e 's/%/%25/g' | perl -p -e 's/\n/%0A/g' | perl -p -e 's/\r/%0D/g') | |
| echo "content=$CONTENT" >> $GITHUB_OUTPUT | |
| build: | |
| runs-on: macos-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| build_number: ${{ steps.get_versions.outputs.BUILD_NUMBER }} | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get Version Numbers | |
| id: get_versions | |
| run: | | |
| # Get build number from commit count | |
| BUILD_NUMBER=$(git rev-list --count HEAD) | |
| echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_OUTPUT | |
| # Initialize version variables | |
| VERSION="0.0.0" | |
| IS_RELEASE=false | |
| # Determine if this is a release tag | |
| if [ "${GITHUB_REF_TYPE}" = "tag" ] && [[ "${GITHUB_REF_NAME}" == v* ]]; then | |
| VERSION=${GITHUB_REF_NAME#v} | |
| IS_RELEASE=true | |
| fi | |
| echo "VERSION=$VERSION" >> $GITHUB_OUTPUT | |
| echo "IS_RELEASE=$IS_RELEASE" >> $GITHUB_OUTPUT | |
| # Debug logging | |
| echo "Debug information:" | |
| echo "GITHUB_REF_TYPE: ${GITHUB_REF_TYPE}" | |
| echo "GITHUB_REF_NAME: ${GITHUB_REF_NAME}" | |
| echo "GITHUB_REF: ${GITHUB_REF}" | |
| echo "BUILD_NUMBER: ${BUILD_NUMBER}" | |
| echo "VERSION: ${VERSION}" | |
| echo "IS_RELEASE: ${IS_RELEASE}" | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: ${{ env.XCODE_VERSION }} | |
| - name: Build Application | |
| env: | |
| MARKETING_VERSION: ${{ steps.get_versions.outputs.VERSION }} | |
| CURRENT_PROJECT_VERSION: ${{ steps.get_versions.outputs.BUILD_NUMBER }} | |
| run: | | |
| # Build universal (arm64 + x86_64) so the app launches on Intel Macs. | |
| # TesseractSwift now ships universal xcframeworks, lifting the prior | |
| # arm64-only constraint. | |
| xcodebuild \ | |
| -scheme "${{ env.APP_NAME }}" \ | |
| -configuration Release \ | |
| -derivedDataPath build \ | |
| -arch arm64 -arch x86_64 \ | |
| -skipMacroValidation \ | |
| -skipPackagePluginValidation \ | |
| ONLY_ACTIVE_ARCH=NO \ | |
| MARKETING_VERSION="$MARKETING_VERSION" \ | |
| CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \ | |
| build | |
| - name: Build CLI | |
| env: | |
| MARKETING_VERSION: ${{ steps.get_versions.outputs.VERSION }} | |
| CURRENT_PROJECT_VERSION: ${{ steps.get_versions.outputs.BUILD_NUMBER }} | |
| run: | | |
| # Build universal (arm64 + x86_64). TesseractSwift now ships universal | |
| # xcframeworks, so the CLI no longer needs to be arm64-only. | |
| xcodebuild \ | |
| -scheme "TRex CLI" \ | |
| -configuration Release \ | |
| -derivedDataPath build-cli \ | |
| -arch arm64 -arch x86_64 \ | |
| -skipMacroValidation \ | |
| -skipPackagePluginValidation \ | |
| ONLY_ACTIVE_ARCH=NO \ | |
| MARKETING_VERSION="$MARKETING_VERSION" \ | |
| CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \ | |
| build | |
| - name: Verify App Binary | |
| run: | | |
| echo "Checking app binary architectures..." | |
| APP_BINARY="build/Build/Products/Release/${{ env.APP_NAME }}.app/Contents/MacOS/${{ env.APP_NAME }}" | |
| ARCHS=$(lipo -archs "$APP_BINARY") | |
| echo "Found architectures: $ARCHS" | |
| # The app must be universal so it launches on both Apple Silicon and Intel. | |
| for arch in arm64 x86_64; do | |
| if ! echo "$ARCHS" | grep -qw "$arch"; then | |
| echo "❌ Error: $arch architecture missing from app binary" | |
| exit 1 | |
| fi | |
| done | |
| echo "✅ App binary is universal (arm64 + x86_64)" | |
| file "$APP_BINARY" | |
| - name: Prepare CLI Binary | |
| run: | | |
| # The CLI binary is named 'trex' and located in the Release directory | |
| CLI_PATH="build-cli/Build/Products/Release/trex" | |
| if [ ! -f "$CLI_PATH" ]; then | |
| echo "❌ Error: CLI binary not found at $CLI_PATH" | |
| echo "Contents of build-cli/Build/Products/Release/:" | |
| ls -la "build-cli/Build/Products/Release/" || true | |
| exit 1 | |
| fi | |
| echo "Found CLI binary, size: $(ls -lh "$CLI_PATH" | awk '{print $5}')" | |
| file "$CLI_PATH" | |
| CLI_CACHE_DIR=".cli-cache" | |
| mkdir -p "$CLI_CACHE_DIR" | |
| CACHE_CLI_PATH="$CLI_CACHE_DIR/trex" | |
| # Copy the binary into the cache directory | |
| cp "$CLI_PATH" "$CACHE_CLI_PATH" | |
| chmod +x "$CACHE_CLI_PATH" | |
| echo "Cached binary info:" | |
| ls -lh "$CACHE_CLI_PATH" | |
| file "$CACHE_CLI_PATH" | |
| echo "Checking CLI binary architectures..." | |
| ARCHS=$(lipo -archs "$CACHE_CLI_PATH") | |
| echo "Found architectures: $ARCHS" | |
| # The CLI must be universal so it runs natively on Apple Silicon and Intel. | |
| for arch in arm64 x86_64; do | |
| if ! echo "$ARCHS" | grep -qw "$arch"; then | |
| echo "❌ Error: $arch architecture missing from CLI binary" | |
| exit 1 | |
| fi | |
| done | |
| echo "✅ CLI binary is universal (arm64 + x86_64)" | |
| - name: Package App Bundle | |
| run: | | |
| cd build/Build/Products/Release | |
| ditto -c -k --keepParent "${{ env.APP_NAME }}.app" "${{ env.APP_NAME }}.app.zip" | |
| - name: Upload App Bundle | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.APP_NAME }}-Unsigned | |
| path: build/Build/Products/Release/${{ env.APP_NAME }}.app.zip | |
| retention-days: 5 | |
| - name: Cache CLI Binary | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: .cli-cache | |
| key: trex-cli-${{ github.run_id }}-${{ github.run_attempt }} | |
| # Validates Intel support (#69) by exercising the build on a real Intel runner. | |
| # macos-15-intel is GitHub's last Intel (x86_64) image; the other macos-* | |
| # images are Apple Silicon. (macos-13, the previous Intel image, was retired | |
| # in December 2025.) The timeout fails fast if the Intel label stops being | |
| # schedulable rather than hanging for the 6-hour default. | |
| intel-smoke-test: | |
| name: Intel (x86_64) Smoke Test | |
| needs: build | |
| runs-on: macos-15-intel | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Confirm runner is Intel | |
| run: | | |
| ARCH=$(uname -m) | |
| echo "Runner architecture: $ARCH" | |
| if [ "$ARCH" != "x86_64" ]; then | |
| echo "❌ Expected an Intel (x86_64) runner but got '$ARCH'." | |
| echo "This job must run on Intel to validate Intel support (see #69)." | |
| exit 1 | |
| fi | |
| - name: Download Unsigned App | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ env.APP_NAME }}-Unsigned | |
| path: . | |
| - name: Restore CLI Binary from Cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: .cli-cache | |
| key: trex-cli-${{ github.run_id }}-${{ github.run_attempt }} | |
| fail-on-cache-miss: true | |
| - name: Unpack App Bundle | |
| run: ditto -x -k "${{ env.APP_NAME }}.app.zip" . | |
| - name: Verify every embedded binary includes x86_64 | |
| run: | | |
| APP="${{ env.APP_NAME }}.app" | |
| FAIL=0 | |
| while IFS= read -r f; do | |
| if file "$f" | grep -q "Mach-O"; then | |
| ARCHS=$(lipo -archs "$f") | |
| printf '%-60s %s\n' "${f#$APP/}" "$ARCHS" | |
| echo "$ARCHS" | grep -qw x86_64 || { echo " ^^^ missing x86_64"; FAIL=1; } | |
| fi | |
| done < <(find "$APP" -type f \( -perm +111 -o -name "*.dylib" -o -name "*.framework" \) | sort) | |
| if [ "$FAIL" != 0 ]; then | |
| echo "❌ One or more embedded binaries lack an x86_64 slice (would crash on Intel)" | |
| exit 1 | |
| fi | |
| echo "✅ All embedded binaries include an x86_64 slice" | |
| - name: Launch app on Intel | |
| run: | | |
| APP_BIN="${{ env.APP_NAME }}.app/Contents/MacOS/${{ env.APP_NAME }}" | |
| "$APP_BIN" > app_launch.log 2>&1 & | |
| APP_PID=$! | |
| sleep 8 | |
| if kill -0 "$APP_PID" 2>/dev/null; then | |
| echo "✅ App launched and is running natively on Intel" | |
| kill "$APP_PID" 2>/dev/null || true | |
| wait "$APP_PID" 2>/dev/null || true | |
| else | |
| wait "$APP_PID" 2>/dev/null; CODE=$? | |
| echo "App process exited early (code $CODE). Output:" | |
| cat app_launch.log || true | |
| # A missing x86_64 slice makes the kernel reject the binary outright; | |
| # that is the exact #69 failure and must fail the job. Other early | |
| # exits (e.g. headless GUI quirks) are reported but not treated as | |
| # an architecture failure. | |
| if grep -qi "bad CPU type" app_launch.log; then | |
| echo "❌ Kernel rejected the binary on Intel — x86_64 slice missing" | |
| exit 1 | |
| fi | |
| echo "⚠️ App did not stay running, but not due to a missing arch slice." | |
| fi | |
| - name: Run CLI on Intel | |
| run: | | |
| CLI=".cli-cache/trex" | |
| chmod +x "$CLI" | |
| echo "CLI architectures: $(lipo -archs "$CLI")" | |
| # The runner is Intel, so a successful run proves the x86_64 slice | |
| # executes natively (and that the bundled Tesseract/Leptonica x86_64 | |
| # native code loads). | |
| if "$CLI" --help; then | |
| echo "✅ CLI ran natively on Intel" | |
| else | |
| echo "❌ CLI failed to run on Intel" | |
| exit 1 | |
| fi | |
| sign: | |
| needs: build | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v3 | |
| - name: Download Unsigned App | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ env.APP_NAME }}-Unsigned | |
| path: . | |
| - name: Restore CLI Binary from Cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: .cli-cache | |
| key: trex-cli-${{ github.run_id }}-${{ github.run_attempt }} | |
| fail-on-cache-miss: true | |
| - name: Stage CLI Binary | |
| run: | | |
| CACHE_CLI_PATH=".cli-cache/trex" | |
| if [ ! -f "$CACHE_CLI_PATH" ]; then | |
| echo "Error: Cached CLI binary not found at $CACHE_CLI_PATH" | |
| exit 1 | |
| fi | |
| chmod +x "$CACHE_CLI_PATH" | |
| - name: Unpack App Bundle | |
| run: | | |
| ditto -x -k "${{ env.APP_NAME }}.app.zip" . | |
| - name: Normalize Embedded Frameworks | |
| run: Scripts/flatten-frameworks.sh "${{ env.APP_NAME }}.app" | |
| - name: Sign Application | |
| uses: ./.github/actions/sign | |
| with: | |
| certificate: ${{ secrets.CERTIFICATES_P12 }} | |
| certificate-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }} | |
| apple-team-id: ${{ secrets.TEAM_ID }} | |
| app-path: "${{ env.APP_NAME }}.app" | |
| entitlements-path: "TRex/Resources/TRex.entitlements" | |
| - name: Sign CLI Binary | |
| uses: ./.github/actions/sign | |
| with: | |
| certificate: ${{ secrets.CERTIFICATES_P12 }} | |
| certificate-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }} | |
| apple-team-id: ${{ secrets.TEAM_ID }} | |
| app-path: ".cli-cache/trex" | |
| entitlements-path: "" | |
| - name: Package Signed App Bundle | |
| run: | | |
| ditto -c -k --keepParent "${{ env.APP_NAME }}.app" "${{ env.APP_NAME }}.app.zip" | |
| - name: Upload Signed App Bundle | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.APP_NAME }}-Signed | |
| path: ${{ env.APP_NAME }}.app.zip | |
| retention-days: 5 | |
| - name: Cache Signed CLI Binary | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: .cli-cache | |
| key: trex-cli-signed-${{ github.run_id }}-${{ github.run_attempt }} | |
| notarize: | |
| needs: sign | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v3 | |
| - name: Download Signed App | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ env.APP_NAME }}-Signed | |
| path: . | |
| - name: Restore Signed CLI Binary from Cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: .cli-cache | |
| key: trex-cli-signed-${{ github.run_id }}-${{ github.run_attempt }} | |
| fail-on-cache-miss: true | |
| - name: Stage CLI Binary | |
| run: | | |
| CACHE_CLI_PATH=".cli-cache/trex" | |
| if [ ! -f "$CACHE_CLI_PATH" ]; then | |
| echo "Error: Cached CLI binary not found at $CACHE_CLI_PATH" | |
| exit 1 | |
| fi | |
| chmod +x "$CACHE_CLI_PATH" | |
| - name: Unpack App Bundle | |
| run: | | |
| ditto -x -k "${{ env.APP_NAME }}.app.zip" . | |
| - name: Notarize Application | |
| uses: ./.github/actions/notarize | |
| with: | |
| username: ${{ secrets.APPLE_ID }} | |
| password: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| apple-team-id: ${{ secrets.TEAM_ID }} | |
| app-path: "${{ env.APP_NAME }}.app" | |
| - name: Notarize CLI Binary | |
| run: | | |
| # Create ZIP for notarization | |
| ditto -c -k ".cli-cache/trex" trex.zip | |
| # Submit for notarization and capture output | |
| set +e # Don't exit on error, we'll handle it | |
| xcrun notarytool submit \ | |
| trex.zip \ | |
| --wait \ | |
| --apple-id "${{ secrets.APPLE_ID }}" \ | |
| --password "${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" \ | |
| --team-id "${{ secrets.TEAM_ID }}" \ | |
| --output-format json \ | |
| > notarization_output.json | |
| NOTARIZE_EXIT_CODE=$? | |
| set -e | |
| # Display the output | |
| cat notarization_output.json | |
| # Check result using exit code and JSON parsing | |
| if [ $NOTARIZE_EXIT_CODE -ne 0 ]; then | |
| echo "❌ Notarization submission failed with exit code $NOTARIZE_EXIT_CODE" | |
| # Try to get the submission ID for logs | |
| SUBMISSION_ID=$(jq -r '.id // empty' notarization_output.json) | |
| if [ -n "$SUBMISSION_ID" ]; then | |
| echo "Fetching notarization logs for submission $SUBMISSION_ID..." | |
| xcrun notarytool log "$SUBMISSION_ID" \ | |
| --apple-id "${{ secrets.APPLE_ID }}" \ | |
| --password "${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" \ | |
| --team-id "${{ secrets.TEAM_ID }}" || true | |
| fi | |
| exit 1 | |
| fi | |
| # Verify the status is "Accepted" | |
| STATUS=$(jq -r '.status // empty' notarization_output.json) | |
| if [ "$STATUS" != "Accepted" ]; then | |
| echo "❌ Notarization failed with status: $STATUS" | |
| exit 1 | |
| fi | |
| echo "✅ CLI binary notarized successfully" | |
| # Note: Stapling is not needed for command-line tools distributed via download | |
| - name: Package Notarized App Bundle | |
| run: | | |
| ditto -c -k --keepParent "${{ env.APP_NAME }}.app" "${{ env.APP_NAME }}.app.zip" | |
| - name: Upload Notarized App Bundle | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.APP_NAME }}-Notarized | |
| path: ${{ env.APP_NAME }}.app.zip | |
| retention-days: 5 | |
| - name: Upload CLI Artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.APP_NAME }}-CLI | |
| path: .cli-cache/trex | |
| retention-days: 5 | |
| - name: Cache Notarized CLI Binary | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: .cli-cache | |
| key: trex-cli-notarized-${{ github.run_id }}-${{ github.run_attempt }} | |
| release: | |
| needs: [notarize, verify-release-notes, intel-smoke-test] | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| runs-on: macos-latest | |
| steps: | |
| - uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set version and beta variables | |
| run: | | |
| # Extract version and determine if beta | |
| VERSION=$(echo "${{ github.ref_name }}" | sed 's/v//') | |
| if echo "${{ github.ref_name }}" | grep -qi 'beta'; then | |
| IS_BETA=true | |
| else | |
| IS_BETA=false | |
| fi | |
| echo "VERSION=$VERSION" >> $GITHUB_ENV | |
| echo "IS_BETA=$IS_BETA" >> $GITHUB_ENV | |
| echo "TAG=${{ github.ref_name }}" >> $GITHUB_ENV | |
| echo "Debug: VERSION=$VERSION, IS_BETA=$IS_BETA, TAG=${{ github.ref_name }}" | |
| - name: Download Notarized App | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ env.APP_NAME }}-Notarized | |
| path: . | |
| - name: Restore Notarized CLI Binary from Cache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: .cli-cache | |
| key: trex-cli-notarized-${{ github.run_id }}-${{ github.run_attempt }} | |
| fail-on-cache-miss: true | |
| - name: Stage CLI Binary | |
| run: | | |
| CACHE_CLI_PATH=".cli-cache/trex" | |
| if [ ! -f "$CACHE_CLI_PATH" ]; then | |
| echo "Error: Cached CLI binary not found at $CACHE_CLI_PATH" | |
| exit 1 | |
| fi | |
| chmod +x "$CACHE_CLI_PATH" | |
| echo "Restored executable permissions for CLI binary" | |
| - name: Generate CLI Checksum | |
| run: | | |
| shasum -a 256 .cli-cache/trex > trex.sha256 | |
| echo "Generated SHA-256 checksum for CLI binary" | |
| cat trex.sha256 | |
| - name: Unpack App Bundle | |
| run: | | |
| ditto -x -k "${{ env.APP_NAME }}.app.zip" . | |
| - name: Get Build Number | |
| run: | | |
| BUILD_NUMBER=$(defaults read "$(pwd)/${{ env.APP_NAME }}.app/Contents/Info" CFBundleVersion) | |
| echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV | |
| echo "Debug: Retrieved BUILD_NUMBER=$BUILD_NUMBER from app bundle" | |
| - name: Create Release Archive | |
| run: | | |
| ditto -c -k --keepParent "${{ env.APP_NAME }}.app" "${{ env.APP_NAME }}-${{ env.VERSION }}.zip" | |
| - name: Calculate SHA256 | |
| run: | | |
| SHA=$(shasum -a 256 "${{ env.APP_NAME }}-${{ env.VERSION }}.zip" | cut -d' ' -f1) | |
| echo "ZIP_SHA=$SHA" >> $GITHUB_ENV | |
| echo "Debug: Calculated ZIP_SHA=$SHA" | |
| - name: Read Release Notes | |
| id: release_notes | |
| run: | | |
| NOTES_PATH="docs/release-notes/${{ env.TAG }}.md" | |
| if [ ! -f "$NOTES_PATH" ]; then | |
| echo "Error: Release notes not found at $NOTES_PATH" | |
| exit 1 | |
| fi | |
| # Prepare escaped version for XML | |
| CONTENT=$(cat "$NOTES_PATH" | perl -p -e 's/%/%25/g' | perl -p -e 's/\n/%0A/g' | perl -p -e 's/\r/%0D/g') | |
| echo "CONTENT=$CONTENT" >> $GITHUB_OUTPUT | |
| # Also store raw notes for release body and changelog | |
| RELEASE_NOTES=$(cat "$NOTES_PATH" | awk '{printf "%s\\n", $0}') | |
| echo "RELEASE_NOTES=$RELEASE_NOTES" >> $GITHUB_ENV | |
| # Verify the content was set | |
| if [ -z "$RELEASE_NOTES" ]; then | |
| echo "Error: RELEASE_NOTES is empty" | |
| exit 1 | |
| fi | |
| - name: Generate Sparkle Signature | |
| id: generate_signature | |
| uses: ./.github/actions/sparkle-sign | |
| with: | |
| private-key: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| file-path: ${{ env.APP_NAME }}-${{ env.VERSION }}.zip | |
| - name: Update Appcast | |
| run: | | |
| # Determine appcast file based on beta flag | |
| APPCAST_NAME="appcast" | |
| if [ "${IS_BETA}" = "true" ]; then | |
| APPCAST_NAME="appcast_beta.xml" | |
| else | |
| APPCAST_NAME="appcast.xml" | |
| fi | |
| APPCAST_FILE="docs/${APPCAST_NAME}" | |
| # Debug logs | |
| echo "Updating appcast: $APPCAST_FILE" | |
| echo "Using URL:" $( [ "${IS_BETA}" = "true" ] && echo "${BETA_FEED_URL}" || echo "${PROD_FEED_URL}" ) | |
| echo "Using signature: ${{ steps.generate_signature.outputs.signature }}" | |
| echo "ZIP_SHA: ${ZIP_SHA}" | |
| echo "BUILD_NUMBER: ${BUILD_NUMBER}" | |
| echo "VERSION: ${VERSION}" | |
| # Use unindented heredoc for proper XML formatting | |
| cat > "$APPCAST_FILE" <<-EOF | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/"> | |
| <channel> | |
| <title>${APP_NAME} $([ "${IS_BETA}" = "true" ] && echo "Beta " )Appcast</title> | |
| <link>$([ "${IS_BETA}" = "true" ] && echo "${BETA_FEED_URL}" || echo "${PROD_FEED_URL}")</link> | |
| <description>${APP_NAME} $([ "${IS_BETA}" = "true" ] && echo "Beta " )Updates</description> | |
| <language>en</language> | |
| <item> | |
| <title>${APP_NAME} ${VERSION}</title> | |
| <sparkle:version>${BUILD_NUMBER}</sparkle:version> | |
| <sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString> | |
| <description><![CDATA[${CONTENT}]]></description> | |
| <pubDate>$(date -R)</pubDate> | |
| <enclosure | |
| url="https://github.com/${{ github.repository }}/releases/download/${{ env.TAG }}/${APP_NAME}-${VERSION}.zip" | |
| sparkle:version="${BUILD_NUMBER}" | |
| sparkle:shortVersionString="${VERSION}" | |
| type="application/octet-stream" | |
| ${{ steps.generate_signature.outputs.signature }} | |
| sparkle:sha256="${ZIP_SHA}" | |
| /> | |
| </item> | |
| </channel> | |
| </rss> | |
| EOF | |
| # Validate the XML | |
| xmllint --noout "$APPCAST_FILE" || { echo "Error: Invalid XML format in appcast"; exit 1; } | |
| - name: Update CHANGELOG.md | |
| run: | | |
| TEMP_FILE=$(mktemp) | |
| echo "# ${TAG} ($(date +'%Y-%m-%d'))" > "$TEMP_FILE" | |
| echo "" >> "$TEMP_FILE" | |
| cat "docs/release-notes/${TAG}.md" >> "$TEMP_FILE" | |
| echo "" >> "$TEMP_FILE" | |
| if [ -f "CHANGELOG.md" ]; then | |
| cat "CHANGELOG.md" >> "$TEMP_FILE" | |
| fi | |
| mv "$TEMP_FILE" "CHANGELOG.md" | |
| - name: Generate Homebrew Formula | |
| if: ${{ env.IS_BETA != 'true' }} | |
| run: | | |
| # Only generate formula for non-beta releases | |
| echo "Generating Homebrew formula for version $VERSION" | |
| # Ensure Formula directory exists | |
| mkdir -p Formula | |
| # Create the formula from template | |
| FORMULA_FILE="Formula/trex.rb" | |
| cp docs/templates/trex.rb.template "$FORMULA_FILE" | |
| # Replace template variables | |
| sed -i '' "s/{{VERSION}}/$VERSION/g" "$FORMULA_FILE" | |
| sed -i '' "s/{{SHA256}}/$ZIP_SHA/g" "$FORMULA_FILE" | |
| echo "Generated Homebrew formula at $FORMULA_FILE" | |
| - name: Commit and Push Changes | |
| run: | | |
| git config --local user.email "action@github.com" | |
| git config --local user.name "GitHub Action" | |
| # Fetch latest main to avoid conflicts | |
| git fetch origin main | |
| # Preserve generated artifacts while switching branches | |
| TMP_DIR=$(mktemp -d) | |
| trap 'rm -rf "$TMP_DIR"' EXIT | |
| cp CHANGELOG.md "$TMP_DIR/CHANGELOG.md" | |
| find docs -maxdepth 1 -name 'appcast*.xml' -exec cp {} "$TMP_DIR/" \; | |
| # Preserve Formula if it exists (non-beta releases) | |
| if [ "$IS_BETA" != "true" ] && [ -f "Formula/trex.rb" ]; then | |
| cp Formula/trex.rb "$TMP_DIR/trex.rb" | |
| fi | |
| # Reset any local changes before checking out new branch | |
| git reset --hard HEAD | |
| git checkout -B release-docs-${TAG} origin/main | |
| cp "$TMP_DIR/CHANGELOG.md" CHANGELOG.md | |
| for FILE in "$TMP_DIR"/appcast*.xml; do | |
| [ -e "$FILE" ] || continue | |
| cp "$FILE" docs/ | |
| done | |
| # Restore Formula if it was backed up | |
| if [ -f "$TMP_DIR/trex.rb" ]; then | |
| mkdir -p Formula | |
| cp "$TMP_DIR/trex.rb" Formula/trex.rb | |
| fi | |
| git add CHANGELOG.md docs/appcast*.xml | |
| if [ "$IS_BETA" != "true" ] && [ -f "Formula/trex.rb" ]; then | |
| git add Formula/trex.rb | |
| fi | |
| git commit -m "docs: update CHANGELOG.md, appcast and Homebrew formula for ${TAG}" || echo "Nothing to commit" | |
| git push origin release-docs-${TAG}:main | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| files: | | |
| ${{ env.APP_NAME }}-${{ env.VERSION }}.zip | |
| .cli-cache/trex | |
| trex.sha256 | |
| body_path: docs/release-notes/${{ env.TAG }}.md | |
| prerelease: ${{ env.IS_BETA == 'true' }} | |
| draft: false | |
| fail_on_unmatched_files: true | |
| generate_release_notes: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| notify: | |
| needs: [build, sign, notarize] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Notify Status | |
| run: | | |
| echo "Build status: ${{ needs.build.result }}" | |
| echo "Sign status: ${{ needs.sign.result }}" | |
| echo "Notarize status: ${{ needs.notarize.result }}" |