skins detect earlier #2441
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 Upload Provenance | |
| on: | |
| push: | |
| branches: | |
| - master | |
| - develop | |
| pull_request: | |
| branches: | |
| - master | |
| - develop | |
| # Full archive matrix only runs on PRs that touch cores, project config, or build infra, | |
| # AND only for external contributors (not JoeMatt/owner, not agent bots). | |
| # JoeMatt and anyone else can trigger a build explicitly via the build-ipa label or /build comment. | |
| types: [opened, synchronize, reopened] | |
| paths: | |
| - 'Cores/**' | |
| - 'CoresRetro/**' | |
| - 'Provenance.xcodeproj/**' | |
| - 'Provenance.xcworkspace/**' | |
| - 'Package.swift' | |
| - 'Package.resolved' | |
| - '*.xcconfig' | |
| - '.github/workflows/build.yml' | |
| # Manual / label / comment triggered build via build-trigger.yml | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to post artifacts to (leave empty for branch-only build)' | |
| required: false | |
| type: string | |
| # Concurrency: keyed on PR number when available so same-PR builds cancel each other, | |
| # but different PRs and develop pushes are independent. | |
| concurrency: | |
| group: build-${{ github.event.pull_request.number || github.event.inputs.pr_number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| SWIFT_PACKAGE_ALLOW_WRITING_TO_DIRECTORY: ${{ github.workspace }} | |
| DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer | |
| XCODE_VERSION: '26.3' | |
| jobs: | |
| build: | |
| name: Build and upload Provenance | |
| # Build conditions: | |
| # push to develop/master → always (→ alpha release) | |
| # workflow_dispatch → always (triggered by label/comment/manual via build-trigger.yml) | |
| # pull_request from external → only non-agent, non-owner PRs touching core paths | |
| # JoeMatt (OWNER) PRs → only via workflow_dispatch (label / /build comment) | |
| if: >- | |
| github.event_name == 'push' || | |
| github.event_name == 'workflow_dispatch' || | |
| ( | |
| !startsWith(github.event.pull_request.title, '[Agent]') && | |
| github.event.pull_request.author_association != 'OWNER' | |
| ) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: "Provenance-iOS" | |
| sdk: iphoneos | |
| destination: "generic/platform=iOS" | |
| scheme: "Provenance (AppStore) (Release)" | |
| APP_NAME: "Provenance" | |
| IPA_NAME: "Provenance-iOS" | |
| - target: "Provenance-tvOS" | |
| sdk: appletvos | |
| destination: "generic/platform=tvOS" | |
| scheme: "Provenance (AppStore) (Release)" | |
| APP_NAME: "Provenance" | |
| IPA_NAME: "Provenance-tvOS" | |
| optional: true | |
| # Lite targets — uncomment if needed for fast-fail validation | |
| # - target: "Provenance-Lite-iOS" | |
| # sdk: iphoneos | |
| # scheme: "Provenance-Lite (AppStore) (Release)" | |
| # APP_NAME: "Provenance Lite" | |
| # IPA_NAME: "Provenance-Lite-iOS" | |
| # - target: "Provenance-Lite-tvOS" | |
| # sdk: appletvos | |
| # scheme: "Provenance-Lite (AppStore) (Release)" | |
| # APP_NAME: "Provenance Lite" | |
| # IPA_NAME: "Provenance-Lite-tvOS" | |
| runs-on: 'macos-26' | |
| continue-on-error: ${{ matrix.optional || false }} | |
| timeout-minutes: 300 | |
| steps: | |
| - name: Cache Git checkout | |
| id: cache-git | |
| uses: actions/cache@v4 | |
| with: | |
| path: . | |
| key: git-checkout-${{ github.sha }} | |
| restore-keys: | | |
| git-checkout- | |
| - name: Checkout code | |
| if: steps.cache-git.outputs.cache-hit != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| fetch-depth: 0 | |
| - name: Setup Xcode | |
| uses: maxim-lobanov/setup-xcode@v1.6.0 | |
| with: | |
| xcode-version: ${{ env.XCODE_VERSION }} | |
| - name: Set Build Number | |
| run: | | |
| BUILD_NUMBER=$(git rev-list --count HEAD) | |
| echo "BUILD_NUMBER=${BUILD_NUMBER}" >> $GITHUB_ENV | |
| echo "MARKETING_VERSION=$(grep 'MARKETING_VERSION' Build.xcconfig | cut -d'=' -f2 | xargs)" >> $GITHUB_ENV | |
| - name: Set Alpha Build Overrides | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | |
| run: | | |
| SHORT_SHA="${GITHUB_SHA:0:7}" | |
| ALPHA_DATE=$(date -u +"%Y%m%d") | |
| echo "IS_ALPHA=true" >> $GITHUB_ENV | |
| echo "ALPHA_BUNDLE_SUFFIX=.alpha" >> $GITHUB_ENV | |
| echo "ALPHA_DISPLAY_NAME_SUFFIX= Alpha" >> $GITHUB_ENV | |
| echo "ALPHA_VERSION_SUFFIX=-alpha.${ALPHA_DATE}.${SHORT_SHA}" >> $GITHUB_ENV | |
| - name: Set Stable Build Overrides | |
| if: github.event_name != 'push' || github.ref != 'refs/heads/develop' | |
| run: | | |
| echo "IS_ALPHA=false" >> $GITHUB_ENV | |
| echo "ALPHA_BUNDLE_SUFFIX=" >> $GITHUB_ENV | |
| echo "ALPHA_DISPLAY_NAME_SUFFIX=" >> $GITHUB_ENV | |
| echo "ALPHA_VERSION_SUFFIX=" >> $GITHUB_ENV | |
| # Cache RetroArch prebuilt libretro dylibs (~1.4 GB uncompressed). | |
| # These are downloaded from the libretro buildbot during the Xcode build | |
| # phase (get-modules.sh). Caching them avoids re-downloading ~120 dylibs | |
| # on every CI run. The cache key is based on the URL list files that define | |
| # which cores to fetch — when those change, the cache is invalidated. | |
| - name: Cache RetroArch libretro dylibs | |
| id: cache-retroarch-modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| CoresRetro/RetroArch/modules | |
| CoresRetro/RetroArch/modules_compressed | |
| key: ${{ runner.os }}-retroarch-modules-${{ matrix.sdk }}-${{ hashFiles('CoresRetro/RetroArch/scripts/urls*.txt') }} | |
| restore-keys: | | |
| ${{ runner.os }}-retroarch-modules-${{ matrix.sdk }}- | |
| # Pre-populate RetroArch modules from compressed zips if cache was restored | |
| # but dylibs directory is empty (e.g., partial cache hit from restore-keys). | |
| # Also set the timestamp so get-modules.sh skips re-downloading. | |
| - name: Restore RetroArch modules from cache | |
| if: steps.cache-retroarch-modules.outputs.cache-hit == 'true' | |
| run: | | |
| CORES_DIR="CoresRetro/RetroArch/modules" | |
| if [ "${{ matrix.sdk }}" = "appletvos" ]; then | |
| ARCHIVE_DIR="CoresRetro/RetroArch/modules_compressed/tvOS" | |
| else | |
| ARCHIVE_DIR="CoresRetro/RetroArch/modules_compressed/iOS" | |
| fi | |
| # Count existing dylibs | |
| DYLIB_COUNT=$(find "$CORES_DIR" -name "*.dylib" 2>/dev/null | wc -l | tr -d ' ') | |
| echo "Cached dylibs found: $DYLIB_COUNT" | |
| # If we have compressed zips but no dylibs, unzip them | |
| if [ "$DYLIB_COUNT" -eq 0 ] && [ -d "$ARCHIVE_DIR" ]; then | |
| ZIP_COUNT=$(find "$ARCHIVE_DIR" -name "*.zip" 2>/dev/null | wc -l | tr -d ' ') | |
| echo "Unzipping $ZIP_COUNT compressed modules..." | |
| find "$ARCHIVE_DIR" -name "*.zip" -exec unzip -n {} -d "$CORES_DIR/" \; 2>/dev/null || true | |
| fi | |
| # Refresh timestamp so get-modules.sh thinks cores are recent | |
| mkdir -p "$ARCHIVE_DIR" | |
| date +%s > "$ARCHIVE_DIR/timestamp.txt" | |
| echo "RetroArch modules restored from cache" | |
| - name: Cache SPM and Xcode DerivedData | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| .build | |
| ~/Library/Caches/org.swift.swiftpm | |
| ~/Library/Developer/Xcode/DerivedData | |
| key: ${{ runner.os }}-spm-xcode-${{ hashFiles('**/Package.resolved') }}-${{ hashFiles('**/*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }} | |
| restore-keys: | | |
| ${{ runner.os }}-spm-xcode- | |
| # Cache the generated libretro cheat database (~31 MB zip). | |
| # Key is the hash of the generator scripts — invalidates when format changes. | |
| - name: Cache libretro cheat database | |
| id: cache-cheatdb | |
| uses: actions/cache@v4 | |
| with: | |
| path: PVLookup/Sources/LibretroCheatDB/Resources/libretro_cheats.sqlite.zip | |
| key: cheatdb-${{ hashFiles('Scripts/generate_cheatdb.py', 'PVLookup/Scripts/generate_cheatdb_if_needed.sh') }} | |
| - name: Generate libretro cheat database | |
| if: steps.cache-cheatdb.outputs.cache-hit != 'true' | |
| run: | | |
| PVLookup/Scripts/generate_cheatdb_if_needed.sh | |
| - name: Install dependencies | |
| run: | | |
| brew install ldid | |
| brew install xcbeautify | |
| - name: Clean Build Directory | |
| run: | | |
| rm -rf ./build | |
| mkdir -p ./build | |
| - name: Patch Info.plist for Alpha (bundle ID + display name) | |
| if: env.IS_ALPHA == 'true' | |
| run: | | |
| echo "Patching for alpha build: bundle suffix='${ALPHA_BUNDLE_SUFFIX}', display name suffix='${ALPHA_DISPLAY_NAME_SUFFIX}'" | |
| # Patch all Info.plist files to add alpha display name | |
| for plist in Provenance/Info.plist ProvenanceTV/Info.plist; do | |
| if [ -f "$plist" ]; then | |
| # Add CFBundleDisplayName if not present, or it will come from build settings | |
| /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string 'Provenance Alpha'" "$plist" 2>/dev/null || \ | |
| /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName 'Provenance Alpha'" "$plist" 2>/dev/null || true | |
| fi | |
| done | |
| - name: Build Provenance | |
| id: build | |
| env: | |
| MARKETING_VERSION: ${{ env.MARKETING_VERSION }} | |
| BUILD_NUMBER: ${{ env.BUILD_NUMBER }} | |
| run: | | |
| set -o pipefail | |
| # For alpha builds, override bundle identifier to allow side-by-side install | |
| EXTRA_BUILD_SETTINGS="" | |
| if [ "$IS_ALPHA" = "true" ]; then | |
| EXTRA_BUILD_SETTINGS="PRODUCT_BUNDLE_IDENTIFIER=org.provenance-emu.provenance${ALPHA_BUNDLE_SUFFIX}" | |
| echo "Alpha build: overriding bundle ID to org.provenance-emu.provenance${ALPHA_BUNDLE_SUFFIX}" | |
| fi | |
| start_time=$(date +%s) | |
| xcodebuild -configuration Release \ | |
| -workspace Provenance.xcworkspace \ | |
| -scheme "${{ matrix.scheme }}" \ | |
| -sdk ${{ matrix.sdk }} \ | |
| -destination "${{ matrix.destination }}" \ | |
| -skipPackagePluginValidation \ | |
| -skipMacroValidation \ | |
| MARKETING_VERSION="${MARKETING_VERSION}" \ | |
| CURRENT_PROJECT_VERSION="${BUILD_NUMBER}" \ | |
| archive \ | |
| -archivePath ./archive \ | |
| CODE_SIGNING_REQUIRED=NO \ | |
| AD_HOC_CODE_SIGNING_ALLOWED=YES \ | |
| CODE_SIGNING_ALLOWED=NO \ | |
| SKIP_INSTALL=NO \ | |
| SWIFT_PACKAGE_ALLOW_WRITING_TO_DIRECTORY=${{ env.SWIFT_PACKAGE_ALLOW_WRITING_TO_DIRECTORY }} \ | |
| DEVELOPMENT_TEAM=S32Z3HMYVQ \ | |
| ORG_IDENTIFIER=org.provenance-emu \ | |
| $EXTRA_BUILD_SETTINGS \ | |
| 2>&1 | tee /tmp/xcodebuild.log | xcbeautify | |
| # Verify archive was created properly | |
| if [ ! -d "archive.xcarchive/Products/Applications" ]; then | |
| echo "::warning::Archive Products/Applications not found, checking alternative paths..." | |
| # Some Xcode versions put the app in a different location when signing is disabled | |
| find archive.xcarchive -name "*.app" -maxdepth 4 2>/dev/null || true | |
| # Try to find and move the app to the expected location | |
| APP_PATH=$(find archive.xcarchive -name "${{ matrix.APP_NAME }}.app" -maxdepth 4 2>/dev/null | head -1) | |
| if [ -n "$APP_PATH" ]; then | |
| mkdir -p archive.xcarchive/Products/Applications | |
| cp -pR "$APP_PATH" "archive.xcarchive/Products/Applications/" | |
| echo "Moved app from $APP_PATH to archive.xcarchive/Products/Applications/" | |
| fi | |
| fi | |
| end_time=$(date +%s) | |
| echo "duration=$((end_time - start_time))" >> $GITHUB_OUTPUT | |
| - name: Print build errors on failure | |
| if: failure() | |
| run: | | |
| if [ -f /tmp/xcodebuild.log ]; then | |
| echo "::group::Build errors" | |
| grep -A2 "error:" /tmp/xcodebuild.log | tail -200 | |
| echo "::endgroup::" | |
| echo "::group::Build warnings (last 50)" | |
| grep "warning:" /tmp/xcodebuild.log | tail -50 | |
| echo "::endgroup::" | |
| fi | |
| - name: Fakesign app | |
| continue-on-error: true | |
| run: | | |
| echo "Contents of Provenance directory:" | |
| ls -la "Provenance/" | |
| echo "Contents of ./ directory:" | |
| ls -la "./" | |
| echo "Contents of archive.xcarchive/Products/Applications/ directory:" | |
| ls -la "archive.xcarchive/Products/Applications/" | |
| echo "Checking app binary..." | |
| ls -la "archive.xcarchive/Products/Applications/${{ matrix.APP_NAME }}.app/" | |
| ldid -S"Provenance/Provenance-AppStore.entitlements" "archive.xcarchive/Products/Applications/${{ matrix.APP_NAME }}.app/${{ matrix.APP_NAME }}" || echo "::warning::Fakesign failed but continuing build" | |
| - name: Convert to IPA | |
| run: | | |
| APP_DIR="archive.xcarchive/Products/Applications" | |
| # If the standard path doesn't exist, search the entire archive | |
| if [ ! -d "$APP_DIR" ] || [ -z "$(ls -A "$APP_DIR" 2>/dev/null)" ]; then | |
| echo "Standard archive path empty, searching..." | |
| find archive.xcarchive -name "*.app" -maxdepth 5 2>/dev/null | |
| APP_PATH=$(find archive.xcarchive -name "${{ matrix.APP_NAME }}.app" -maxdepth 5 2>/dev/null | head -1) | |
| if [ -z "$APP_PATH" ]; then | |
| echo "::error::No .app bundle found in archive" | |
| echo "Archive structure:" | |
| find archive.xcarchive -type d -maxdepth 4 | |
| exit 1 | |
| fi | |
| mkdir -p "$APP_DIR" | |
| cp -pR "$APP_PATH" "$APP_DIR/" | |
| echo "Found app at: $APP_PATH" | |
| fi | |
| mkdir Payload | |
| ls -la "$APP_DIR" | |
| cp -pR "$APP_DIR/${{ matrix.APP_NAME }}.app" "Payload/${{ matrix.IPA_NAME }}.app" | |
| zip -r "${{ matrix.IPA_NAME }}.ipa" Payload | |
| - name: Upload Artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: "${{ matrix.IPA_NAME }}-v${{ env.MARKETING_VERSION }}(${{ env.BUILD_NUMBER }}).ipa" | |
| path: "${{ matrix.IPA_NAME }}.ipa" | |
| if-no-files-found: error | |
| retention-days: 90 | |
| - name: Record Build Time | |
| run: | | |
| duration=${{ steps.build.outputs.duration }} | |
| minutes=$((duration / 60)) | |
| seconds=$((duration % 60)) | |
| echo "Build took ${minutes}m ${seconds}s" | |
| echo "### Build Time ⏱️" >> $GITHUB_STEP_SUMMARY | |
| echo "- Duration: ${minutes}m ${seconds}s" >> $GITHUB_STEP_SUMMARY | |
| - name: Build Summary | |
| run: | | |
| echo "### Build Complete! :rocket:" >> $GITHUB_STEP_SUMMARY | |
| echo "- Version: ${{ env.MARKETING_VERSION }} (${{ env.BUILD_NUMBER }})" >> $GITHUB_STEP_SUMMARY | |
| echo "- Target: ${{ matrix.target }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- Scheme: ${{ matrix.scheme }}" >> $GITHUB_STEP_SUMMARY | |
| - name: Update Build Status | |
| if: always() | |
| run: | | |
| if [ "${{ job.status }}" = "success" ]; then | |
| echo "✅ Build succeeded for ${{ matrix.target }}" | |
| else | |
| echo "❌ Build failed for ${{ matrix.target }}" | |
| exit 1 | |
| fi | |
| - name: Notify Discord Build Status | |
| if: always() | |
| env: | |
| DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} | |
| run: | | |
| if [ "${{ job.status }}" = "success" ]; then | |
| STATUS_COLOR="65280" | |
| STATUS="✅ Success" | |
| else | |
| STATUS_COLOR="16711680" | |
| STATUS="❌ Failed" | |
| fi | |
| curl -H "Content-Type: application/json" -X POST $DISCORD_WEBHOOK \ | |
| -d '{ | |
| "embeds": [{ | |
| "title": "Build ${{ matrix.target }}", | |
| "description": "Version ${{ env.MARKETING_VERSION }} (Build ${{ env.BUILD_NUMBER }})\nScheme: ${{ matrix.scheme }}\nBuild Duration: ${{ steps.build.outputs.duration }}s", | |
| "color": '"$STATUS_COLOR"', | |
| "footer": { | |
| "text": "'"$STATUS"'" | |
| } | |
| }] | |
| }' | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| rm -rf .git | |
| rm -rf .build | |
| # Update rolling alpha release on successful develop pushes | |
| # tvOS is continue-on-error so this runs even if only iOS succeeds | |
| alpha-release: | |
| name: Update Alpha Release | |
| needs: build | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| issues: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| - name: Build Release Notes | |
| id: release_notes | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| SHA="${{ github.sha }}" | |
| SHORT_SHA="${SHA:0:7}" | |
| DATE=$(date -u +"%Y-%m-%d %H:%M UTC") | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| # Get the previous alpha tag SHA (if it exists) | |
| PREV_SHA=$(gh api "repos/${REPO}/git/refs/tags/alpha" --jq '.object.sha' 2>/dev/null || echo "") | |
| # Build commit log since last alpha | |
| COMMIT_LOG="" | |
| if [ -n "$PREV_SHA" ] && [ "$PREV_SHA" != "$SHA" ]; then | |
| COMMIT_LOG=$(git log --oneline --no-merges "${PREV_SHA}..${SHA}" 2>/dev/null | head -30 || echo "") | |
| fi | |
| if [ -z "$COMMIT_LOG" ]; then | |
| COMMIT_LOG=$(git log --oneline --no-merges -10 2>/dev/null || echo "No commit history available") | |
| fi | |
| # Get merged PRs since last alpha | |
| MERGED_PRS="" | |
| if [ -n "$PREV_SHA" ]; then | |
| # Extract PR numbers from merge commits | |
| PR_NUMBERS=$(git log --oneline "${PREV_SHA}..${SHA}" 2>/dev/null | grep -oP '#\d+' | sort -u | head -15 || echo "") | |
| for pr in $PR_NUMBERS; do | |
| PR_NUM="${pr#\#}" | |
| PR_TITLE=$(gh pr view "$PR_NUM" --repo "$REPO" --json title --jq '.title' 2>/dev/null || echo "") | |
| if [ -n "$PR_TITLE" ]; then | |
| MERGED_PRS="${MERGED_PRS}- ${pr}: ${PR_TITLE}"$'\n' | |
| fi | |
| done | |
| fi | |
| # Write release notes to file (avoids quoting issues) | |
| cat > /tmp/release_notes.md << NOTESEOF | |
| ## Alpha Build \`${SHORT_SHA}\` | |
| **Automated alpha build from \`develop\` branch** | |
| | | | | |
| |---|---| | |
| | **Commit** | [\`${SHORT_SHA}\`](https://github.com/${REPO}/commit/${SHA}) | | |
| | **Built** | ${DATE} | | |
| | **CI Run** | [View build log](${RUN_URL}) | | |
| | **Bundle ID** | \`com.provenance-emu.provenance.alpha\` | | |
| > **Note:** This alpha build uses a separate bundle ID so it can be installed alongside the stable release. | |
| ### Commits since last alpha | |
| \`\`\` | |
| ${COMMIT_LOG} | |
| \`\`\` | |
| NOTESEOF | |
| if [ -n "$MERGED_PRS" ]; then | |
| cat >> /tmp/release_notes.md << PREOF | |
| ### Merged PRs | |
| ${MERGED_PRS} | |
| PREOF | |
| fi | |
| cat >> /tmp/release_notes.md << FOOTEREOF | |
| --- | |
| These builds are unsigned and intended for sideloading via AltStore/SideStore. | |
| Install the **Provenance Alpha** app (separate from stable) using the [sideload feed](https://provenance-emu.com/altstore-source.json). | |
| > This release is automatically updated on every successful develop push. | |
| FOOTEREOF | |
| - name: Update alpha release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TAG="alpha" | |
| REPO="${{ github.repository }}" | |
| SHA="${{ github.sha }}" | |
| SHORT_SHA="${SHA:0:7}" | |
| # Create or update the alpha tag | |
| gh api repos/$REPO/git/refs/tags/$TAG \ | |
| -X PATCH -f sha="$SHA" -f force=true 2>/dev/null || \ | |
| gh api repos/$REPO/git/refs \ | |
| -X POST -f ref="refs/tags/$TAG" -f sha="$SHA" | |
| # Delete existing alpha release if it exists | |
| gh release delete $TAG --repo $REPO --yes 2>/dev/null || true | |
| # Collect IPA files | |
| find artifacts -name "*.ipa" -exec mv {} . \; | |
| # Create new pre-release with IPAs | |
| gh release create $TAG \ | |
| --repo $REPO \ | |
| --title "Provenance Alpha ($SHORT_SHA)" \ | |
| --notes-file /tmp/release_notes.md \ | |
| --prerelease \ | |
| *.ipa 2>/dev/null || echo "::warning::No IPA files to attach" | |
| - name: Update Build Status Issue | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| SHORT_SHA="${{ github.sha }}" | |
| SHORT_SHA="${SHORT_SHA:0:7}" | |
| DATE=$(date -u +"%Y-%m-%d %H:%M UTC") | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| # Update issue #3163 with latest build status | |
| ISSUE_BODY=$(cat << 'ISSUEEOF' | |
| # Alpha & Beta Builds -- Download & Install Guide | |
| ## Latest Alpha Build | |
| | | | | |
| |---|---| | |
| ISSUEEOF | |
| ) | |
| ISSUE_BODY="${ISSUE_BODY} | |
| | **Status** | :white_check_mark: Build Passing | | |
| | **Commit** | [\`${SHORT_SHA}\`](https://github.com/${REPO}/commit/${{ github.sha }}) | | |
| | **Built** | ${DATE} | | |
| | **CI Run** | [View build log](${RUN_URL}) | | |
| | **Download** | [GitHub Release](https://github.com/${REPO}/releases/tag/alpha) | | |
| | **Bundle ID** | \`com.provenance-emu.provenance.alpha\` |" | |
| ISSUE_BODY="${ISSUE_BODY} | |
| --- | |
| ## Install via AltStore | |
| 1. Open AltStore on your device | |
| 2. Go to **Sources** (or **Browse** > **Sources**) | |
| 3. Tap **Add Source** and enter: | |
| \`\`\` | |
| https://provenance-emu.com/altstore-source.json | |
| \`\`\` | |
| 4. You will see **two** apps: **Provenance** (stable) and **Provenance Alpha** (latest dev builds) | |
| 5. Install either or both -- they use separate bundle IDs so both can be installed simultaneously | |
| ## Install via SideStore | |
| 1. Open SideStore on your device | |
| 2. Go to **Sources** | |
| 3. Add the following source URL: | |
| \`\`\` | |
| https://provenance-emu.com/sidestore-source.json | |
| \`\`\` | |
| 4. Browse the source and install **Provenance** or **Provenance Alpha** | |
| ## Direct IPA Download from GitHub Releases | |
| - **Latest stable release:** [Releases page](https://github.com/${REPO}/releases/latest) | |
| - **Latest alpha build:** [Alpha release](https://github.com/${REPO}/releases/tag/alpha) | |
| --- | |
| ## FAQ | |
| ### What is the difference between stable and alpha builds? | |
| | | Stable | Alpha | | |
| |---|---|---| | |
| | **Source** | Tagged releases on \`master\` | Automatic builds from \`develop\` branch | | |
| | **Bundle ID** | \`com.provenance-emu.provenance\` | \`com.provenance-emu.provenance.alpha\` | | |
| | **App Name** | Provenance | Provenance Alpha | | |
| | **Side-by-side** | N/A | Yes -- install both at once | | |
| | **Reliability** | Tested and ready for daily use | May contain bugs or incomplete features | | |
| | **Updates** | Released periodically | Built on every successful \`develop\` CI run | | |
| ### Can I install both stable and alpha at the same time? | |
| **Yes!** Alpha builds use a different bundle ID (\`com.provenance-emu.provenance.alpha\`) so they install as a separate app called **Provenance Alpha**. Your stable install is not affected. | |
| ### Are alpha builds safe to use? | |
| Alpha builds pass all CI checks before being published, but they have not gone through the full QA process of a stable release. **Back up your save data** before switching to an alpha build. | |
| ### How do I report bugs in alpha builds? | |
| Please [open an issue](https://github.com/${REPO}/issues/new) and include: | |
| - The build version (visible in Settings > About) | |
| - Steps to reproduce | |
| - Device model and iOS/tvOS version | |
| --- | |
| > **Note:** This issue is auto-updated by CI on every successful alpha build. Do not edit manually." | |
| gh issue edit 3163 --repo "$REPO" --body "$ISSUE_BODY" 2>/dev/null || \ | |
| echo "::warning::Could not update issue #3163" | |
| # Post artifact download links to PR for workflow_dispatch builds (label / /build triggered) | |
| post-to-pr: | |
| name: Post IPA links to PR | |
| needs: build | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_number != '' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Post build links comment | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.inputs.pr_number }} | |
| run: | | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| # Fetch artifact names for this run | |
| ARTIFACT_NAMES=$(gh api \ | |
| "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \ | |
| --jq '.artifacts[].name' 2>/dev/null || echo "") | |
| # Build artifact list | |
| ARTIFACT_LIST="" | |
| while IFS= read -r name; do | |
| [ -n "$name" ] && ARTIFACT_LIST="${ARTIFACT_LIST}- \`${name}\`"$'\n' | |
| done <<< "$ARTIFACT_NAMES" | |
| cat > /tmp/build_links.md << COMMENTEOF | |
| ## 📦 IPA Builds Ready | |
| Download from the [Actions run page](${RUN_URL}). | |
| ### Available IPAs | |
| ${ARTIFACT_LIST} | |
| > **Install:** Download the \`.ipa\`, then sideload via AltStore, SideStore, or TrollStore. | |
| > Artifacts expire after 90 days. | |
| COMMENTEOF | |
| gh pr comment "$PR_NUMBER" \ | |
| --repo "${{ github.repository }}" \ | |
| --body-file /tmp/build_links.md 2>/dev/null || \ | |
| echo "::warning::Could not post comment to PR #${PR_NUMBER}" |