name: Reusable SideStore Build on: workflow_call: inputs: is_beta: required: false default: false type: boolean publish: required: false default: false type: boolean is_shared_build_num: required: false default: true type: boolean release_name: required: true type: string release_tag: required: true type: string upstream_tag: required: true type: string upstream_name: required: true type: string bundle_id: default: com.SideStore.SideStore required: true type: string bundle_id_suffix: default: '' required: false type: string secrets: # GITHUB_TOKEN: # required: true CROSS_REPO_PUSH_KEY: required: true BUILD_LOG_ZIP_PASSWORD: required: false jobs: build: name: Build and upload SideStore ${{ inputs.release_tag }} releases concurrency: group: build-number-increment # serialize for build num cache access strategy: fail-fast: false matrix: include: - os: 'macos-15' version: '16.1' runs-on: ${{ matrix.os }} steps: - name: Set beta status run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV - name: Checkout code uses: actions/checkout@v4 with: submodules: recursive # dispatch simulator boot in bg coz it take a while to boot-up fresh - name: Boot Simulator for testing run: | mkdir -p build/logs make -B boot-sim-async | tee -a build/logs/test.log exit ${PIPESTATUS[0]} - name: Install dependencies - ldid & xcbeautify & xcpretty run: | brew install ldid xcbeautify gem install xcpretty # for test reports - name: Set ref based on is_shared_build_num if: ${{ inputs.is_beta }} id: set_ref run: | if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then echo "ref=main" >> $GITHUB_ENV else echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV fi - name: Checkout SideStore/beta-build-num repo if: ${{ inputs.is_beta }} uses: actions/checkout@v4 with: repository: 'SideStore/beta-build-num' ref: ${{ env.ref }} token: ${{ secrets.CROSS_REPO_PUSH_KEY }} path: 'SideStore/beta-build-num' - name: Copy build_number.txt to repo root if: ${{ inputs.is_beta }} run: | cp SideStore/beta-build-num/build_number.txt . echo "cat build_number.txt" cat build_number.txt - name: Echo Build.xcconfig run: | echo "cat Build.xcconfig" cat Build.xcconfig - name: Set Release Channel info for build number bumper run: | echo "RELEASE_CHANNEL=${{ inputs.release_tag }}" >> $GITHUB_ENV echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" - name: Increase build number for beta builds if: ${{ inputs.is_beta }} run: | bash .github/workflows/increase-beta-build-num.sh - name: Extract MARKETING_VERSION from Build.xcconfig id: version run: | version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g') echo "version=$version" >> $GITHUB_OUTPUT echo "version=$version" - name: Get short commit hash run: | # SHORT_COMMIT="${{ github.sha }}" SHORT_COMMIT=${GITHUB_SHA:0:7} echo "Short commit hash: $SHORT_COMMIT" echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_ENV - name: Set MARKETING_VERSION if: ${{ inputs.is_beta }} run: | # Extract version number (e.g., "0.6.0") version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/') # Extract date (YYYYMMDD) (e.g., "20250205") date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/') # Extract build number (e.g., "2") build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/') # Combine them into the final output MARKETING_VERSION="${version}-${date}.${build_num}+${SHORT_COMMIT}" echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV echo "MARKETING_VERSION=$MARKETING_VERSION" - name: Echo Updated Build.xcconfig, build_number.txt if: ${{ inputs.is_beta }} run: | cat Build.xcconfig cat build_number.txt - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1.6.0 with: xcode-version: ${{ matrix.version }} - name: Cache Build uses: irgaly/xcode-cache@v1 with: key: xcode-cache-deriveddata-${{ github.sha }} restore-keys: xcode-cache-deriveddata- swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }} swiftpm-cache-restore-keys: | xcode-cache-sourcedata- - name: Restore Pods from Cache (Exact match) id: pods-restore uses: actions/cache/restore@v3 with: path: | ./Podfile.lock ./Pods/ ./AltStore.xcworkspace/ key: pods-cache-${{ hashFiles('Podfile') }} # restore-keys: | # commented out to strictly check cache for this particular podfile # pods-cache- - name: Restore Pods from Cache (Last Available) if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }} id: pods-restore-recent uses: actions/cache/restore@v3 with: path: | ./Podfile.lock ./Pods/ ./AltStore.xcworkspace/ key: pods-cache- - name: Install CocoaPods run: pod install - name: Save Pods to Cache id: save-pods if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }} uses: actions/cache/save@v3 with: path: | ./Podfile.lock ./Pods/ ./AltStore.xcworkspace/ key: pods-cache-${{ hashFiles('Podfile') }} - name: Clean previous build artifacts # using 'tee' to intercept stdout and log for detailed build-log run: | make clean mkdir -p build/logs - name: List Files and derived data if: always() run: | echo ">>>>>>>>> Workdir <<<<<<<<<<" ls -la . echo "" echo ">>>>>>>>> Pods <<<<<<<<<<" find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists echo "" echo ">>>>>>>>> SideStore <<<<<<<<<<" find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists echo "" echo ">>>>>>>>> Dependencies <<<<<<<<<<" find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists echo "" echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<" ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists echo "" - name: Set BundleID Suffix for Sidestore build run: | echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV - name: Build SideStore.xcarchive # using 'tee' to intercept stdout and log for detailed build-log run: | NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]} - name: Fakesign app run: make fakesign | tee -a build/logs/build.log - name: Convert to IPA run: make ipa | tee -a build/logs/build.log # we expect simulator to have been booted by now, so exit otherwise - name: Simulator Boot Check run: | mkdir -p build/logs make -B sim-boot-check | tee -a build/logs/test.log exit ${PIPESTATUS[0]} - name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1) if: ${{ vars.DEBUG_RECORD_TESTS == '1' }} run: | nohup xcrun simctl io booted recordVideo -f test-recording.mp4 --codec h264 test-recording.log 2>&1 & RECORD_PID=$! echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV # build will be up-to-date from previous step so here only test will be executed directly - name: Run SideStore Tests # using 'tee' to intercept stdout and log for detailed build-log run: | NSUnbufferedIO=YES make -B build-and-test 2>&1 | tee -a build/logs/test.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]} # NSUnbufferedIO=YES make -B build-and-test 2>&1 | tee build/logs/test.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]} - name: Stop Recording tests if: ${{ always() && env.RECORD_PID != '' }} run: | kill -INT ${{ env.RECORD_PID }} - name: List Files and Build artifacts if: always() run: | echo ">>>>>>>>> Workdir <<<<<<<<<<" ls -la . echo "" echo ">>>>>>>>> Build <<<<<<<<<<" find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists echo "" echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<" find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists echo "" - name: Encrypt build-logs for upload id: encrypt-build-log if: always() run: | DEFAULT_BUILD_LOG_PASSWORD=12345 BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }} BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD} if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'." fi pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd echo "::set-output name=encrypted::true" - name: Upload encrypted-build-logs.zip id: attach-encrypted-build-log if: always() && steps.encrypt-build-log.outputs.encrypted == 'true' uses: actions/upload-artifact@v4 with: name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip path: encrypted-build-logs.zip - name: Print test-recording.log contents (if exists) if: ${{ always() && env.RECORD_PID != '' }} run: | if [ -f test-recording.log ]; then echo "test-recording.log found. Its contents:" cat test-recording.log else echo "test-recording.log not found." fi - name: Check for test-recording.mp4 presence id: check-recording if: ${{ always() && env.RECORD_PID != '' }} run: | if [ -f test-recording.mp4 ]; then echo "::set-output name=found::true" echo "test-recording.mp4 found." else echo "test-recording.mp4 not found, skipping upload." echo "::set-output name=found::false" fi - name: Upload test-recording.mp4 id: upload-recording if: ${{ always() && steps.check-recording.outputs.found == 'true' }} uses: actions/upload-artifact@v4 with: name: test-recording-${{ steps.version.outputs.version }}.mp4 path: test-recording.mp4 - name: Upload Test Artifacts uses: actions/upload-artifact@v4 with: name: test-results-${{ steps.version.outputs.version }}.zip path: ./build/tests/* - name: Upload SideStore.ipa Artifact uses: actions/upload-artifact@v4 with: name: SideStore-${{ steps.version.outputs.version }}.ipa path: SideStore.ipa - name: Upload *.dSYM Artifact uses: actions/upload-artifact@v4 with: name: SideStore-${{ steps.version.outputs.version }}-dSYM path: ./SideStore.xcarchive/dSYMs/* - name: Get current date id: date run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT - name: Get current date in AltStore date form id: date_altstore run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT - name: Create dSYMs zip run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs/* - name: Upload to releases uses: IsaacShelton/update-existing-release@v1.3.1 with: token: ${{ secrets.GITHUB_TOKEN }} release: ${{ inputs.release_name }} tag: ${{ inputs.release_tag }} prerelease: ${{ inputs.is_beta }} files: SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip body: | This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}). ${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!** If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }}). ## Build Info Built at (UTC): `${{ steps.date.outputs.date }}` Built at (UTC date): `${{ steps.date_altstore.outputs.date }}` Commit SHA: `${{ github.sha }}` Version: `${{ steps.version.outputs.version }}` # save it - name: Publish to SideStore/beta-build-num if: ${{ inputs.is_beta }} run: | rm SideStore/beta-build-num/build_number.txt mv build_number.txt SideStore/beta-build-num/build_number.txt pushd SideStore/beta-build-num/ echo "Configure Git user (committer details)" git config user.name "GitHub Actions" git config user.email "github-actions@github.com" echo "Adding files to commit" git add --verbose build_number.txt git commit -m " - updated for ${{ inputs.release_tag }} - $SHORT_COMMIT deployment" || echo "No changes to commit" echo "Pushing to remote repo" git push --verbose popd - name: Get formatted date run: | FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") echo "Formatted date: $FORMATTED_DATE" echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV - name: Get size of IPA in bytes (macOS/Linux) run: | if [[ "$(uname)" == "Darwin" ]]; then # macOS IPA_SIZE=$(stat -f %z SideStore.ipa) else # Linux IPA_SIZE=$(stat -c %s SideStore.ipa) fi echo "IPA size in bytes: $IPA_SIZE" echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV - name: Compute SHA-256 of IPA run: | SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }') echo "SHA-256 Hash: $SHA256_HASH" echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV - name: Set Release Info variables run: | # Format localized description LOCALIZED_DESCRIPTION=$(cat <> $GITHUB_ENV echo "VERSION_IPA=$MARKETING_VERSION" >> $GITHUB_ENV echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV # multiline strings echo "LOCALIZED_DESCRIPTION<> $GITHUB_ENV echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - name: Check if Publish updates is set id: check_publish run: | echo "Publish updates to source.json = ${{ inputs.publish }}" - name: Checkout SideStore/apps-v2.json if: ${{ inputs.is_beta && inputs.publish }} uses: actions/checkout@v4 with: repository: 'SideStore/apps-v2.json' ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized token: ${{ secrets.CROSS_REPO_PUSH_KEY }} path: 'SideStore/apps-v2.json' # for stable builds, let the user manually edit the source.json - name: Publish to SideStore/apps-v2.json if: ${{ inputs.is_beta && inputs.publish }} id: publish-release run: | # Copy and execute the update script pushd SideStore/apps-v2.json/ # Configure Git user (committer details) git config user.name "GitHub Actions" git config user.email "github-actions@github.com" # update the source.json python3 ../../update_apps.py "./_includes/source.json" # Commit changes and push using SSH git add --verbose ./_includes/source.json git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit" git push --verbose popd