Skip to content

fix: artwork backfill runs after every sync cycle, not just initial load #2455

fix: artwork backfill runs after every sync cycle, not just initial load

fix: artwork backfill runs after every sync cycle, not just initial load #2455

Workflow file for this run

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: false
fetch-depth: 0
- name: Initialize submodules (shallow recursive)
if: steps.cache-git.outputs.cache-hit != 'true'
run: |
# Recursive --depth 1 init for all submodules.
# Cores need recursive init because their Package.swift files reference
# paths inside nested submodules (e.g. VirtualJaguar/Sources/virtualjaguar-libretro/).
git submodule update --init --recursive --depth 1 2>&1 || true
echo "Submodule init complete."
- 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}"