From 35570aaa8005f2477516089fef547fe9a3d7308e Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 14:57:04 -0400 Subject: [PATCH 01/24] Enhance app release flow --- .github/workflows/skip-app.yml | 606 ++++++++++++++++++++++++++++++--- 1 file changed, 551 insertions(+), 55 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index af419d8..2f2a811 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -6,6 +6,14 @@ # When tagged with a semantic version (e.g., "1.2.3"), the action will # create and distribute release .apk and .ipa artifacts from the project. # +# Jobs: +# build-app – checkout, test, skip export, upload artifacts +# run-ios-app – install exported app on iOS simulator, run Maestro tests per locale +# run-android-app – install exported app on Android emulator, run Maestro tests per locale +# release-ios-app – sign and submit iOS app via Fastlane (tag only, secrets required) +# release-android-app – sign and submit Android app via Fastlane (tag only, secrets required) +# github-release – create GitHub release with all artifacts and screenshots +# # An example invocation script is as follows, which runs for # every push, every PR, every semver tag, and every day at noon GMT: # @@ -52,12 +60,19 @@ on: required: false APPLE_MOBILEPROVISION: required: false + jobs: - skip-app: - runs-on: macos-15-intel + # ─────────────────────────────────────────────────────────────────── + # 1. Build, test, and export the app + # ─────────────────────────────────────────────────────────────────── + build-app: + runs-on: macos-26 timeout-minutes: 180 env: DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + outputs: + reltag: ${{ steps.setup.outputs.reltag }} + skip-module: ${{ steps.setup.outputs.skip-module }} steps: - uses: actions/checkout@v6 with: @@ -65,18 +80,10 @@ jobs: - name: "Setup" id: setup - env: - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} run: | - # convert "refs/tags/1.0.0" to "1.0.0" - # and "refs/heads/main" to "main" TAG=${GITHUB_REF#refs/*/} - # the version if it matches the semantic tag pattern, otherwise "dev" - echo "RELTAG=${TAG:-'dev'}" >> $GITHUB_ENV + echo "reltag=${TAG:-'dev'}" >> $GITHUB_OUTPUT - echo "HOMEBREW_PREFIX: ${HOMEBREW_PREFIX}" - # needed for Android/fastlane/Fastfile Gradle build on X86 - # or else it cannot find the gradle command if [[ "${RUNNER_ARCH}" == 'ARM64' ]]; then echo "HOMEBREW_PREFIX=/opt/homebrew" >> $GITHUB_ENV else @@ -85,25 +92,13 @@ jobs: echo "COMMIT_DATE=$(git log -1 --format=%ad --date=iso-strict ${TAG})" >> $GITHUB_ENV - # the primary skip module is the first product name SKIP_MODULE=$(basename Darwin/*.xcodeproj .xcodeproj) + echo "skip-module=${SKIP_MODULE}" >> $GITHUB_OUTPUT echo "SKIP_MODULE=${SKIP_MODULE}" >> $GITHUB_ENV - # update Skip.env to set version string to the latest semver tag sed -i '' "s;MARKETING_VERSION = .*;MARKETING_VERSION = $(git describe --tags --abbrev=0 --match '[0-9]*\.[0-9]*\.[0-9]*' --first-parent);g" Skip.env - - # update Skip.env to set build number to the git commit count sed -i '' "s;PRODUCT_VERSION = .*;PRODUCT_VERSION = $(git rev-list --count HEAD);g" Skip.env - # the emulator we use should match the host architecture - echo "android-arch=$(uname -m | sed 's/arm64/arm64-v8a/')" >> $GITHUB_OUTPUT - - # check whether the KEYSTORE_PROPERTIES secret exists, - # and if so we will run the signing option later - echo 'android-keystore-exists=$(test -z "${KEYSTORE_PROPERTIES}" && echo "false" || echo "true")' >> $GITHUB_OUTPUT - - # check whether any of the skip.yml files in the project - # presume the native toolchain yq -e '.skip.mode' Sources/*/Skip/skip.yml >> /dev/null && echo "skip-fuse=true" >> $GITHUB_OUTPUT || echo "Native toolchain not needed" - uses: skiptools/actions/setup-skip@v1 @@ -118,67 +113,568 @@ jobs: - name: "Export project" run: | - # need to run twice due to a bug with debug key availability skip export -v --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY skip export -v --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY cp -a Package.resolved skip-export/ - - name: "Setup Android App Signing" - if: ${{ env.KEYSTORE_PROPERTIES != '' }} + - name: "Upload build artifacts" + uses: actions/upload-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Upload project sources" + uses: actions/upload-artifact@v6 + with: + name: project-sources + path: | + Darwin/ + Android/ + Skip.env + + # ─────────────────────────────────────────────────────────────────── + # 2a. Run iOS app on simulator with Maestro tests + # ─────────────────────────────────────────────────────────────────── + run-ios-app: + needs: build-app + runs-on: macos-26 + timeout-minutes: 60 + env: + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download project sources" + uses: actions/download-artifact@v6 + with: + name: project-sources + + - name: "Install Maestro" + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "${HOME}/.maestro/bin" >> $GITHUB_PATH + + - name: "Boot iOS Simulator" + id: simulator + run: | + # Create and boot a simulator + DEVICE_ID=$(xcrun simctl create "MaestroTest" "iPhone 16") + echo "device-id=${DEVICE_ID}" >> $GITHUB_OUTPUT + xcrun simctl boot "${DEVICE_ID}" + # Wait for the simulator to be ready + xcrun simctl bootstatus "${DEVICE_ID}" + + - name: "Install app on simulator" + run: | + # Find the .app bundle from the export (debug build for testing) + APP_PATH=$(find skip-export -name "*.app" -type d | head -1) + if [ -z "${APP_PATH}" ]; then + echo "No .app bundle found in skip-export" + exit 1 + fi + xcrun simctl install "${{ steps.simulator.outputs.device-id }}" "${APP_PATH}" + + - name: "Ensure Maestro test flows exist" + run: | + if [ ! -d Darwin/fastlane/Maestro ] || [ -z "$(ls Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml 2>/dev/null)" ]; then + mkdir -p Darwin/fastlane/Maestro + BUNDLE_ID=$(defaults read "$(find skip-export -name '*.app' -type d | head -1)/Info.plist" CFBundleIdentifier 2>/dev/null || echo "") + cat > Darwin/fastlane/Maestro/launch-and-screenshot.yaml <> $GITHUB_OUTPUT + + - name: "Run Maestro tests for each locale" env: - KEYSTORE_JKS: ${{ secrets.KEYSTORE_JKS }} - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + DEVICE_ID: ${{ steps.simulator.outputs.device-id }} run: | - echo -n "${KEYSTORE_JKS}" | base64 --decode > Android/app/keystore.jks - echo -n "${KEYSTORE_PROPERTIES}" | base64 --decode > Android/app/keystore.properties + mkdir -p screenshots + LOCALES="${{ steps.locales.outputs.locales }}" + + for LOCALE in ${LOCALES}; do + echo "::group::Running Maestro tests for locale ${LOCALE}" + + # Change simulator locale and restart SpringBoard + # Convert locale format (e.g., en-US -> en_US for simctl) + LANG_CODE="${LOCALE%[-_]*}" + REGION_CODE="${LOCALE#*[-_]}" + xcrun simctl spawn "${DEVICE_ID}" defaults write -g AppleLanguages -array "${LANG_CODE}" + xcrun simctl spawn "${DEVICE_ID}" defaults write -g AppleLocale "${LANG_CODE}_${REGION_CODE}" + # Restart SpringBoard to apply locale change + xcrun simctl spawn "${DEVICE_ID}" launchctl stop com.apple.SpringBoard 2>/dev/null || true + sleep 3 + + # Run each Maestro flow + for FLOW in Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml; do + [ -f "${FLOW}" ] || continue + echo "Running flow: ${FLOW} (locale: ${LOCALE})" + maestro --device "${DEVICE_ID}" test "${FLOW}" --output "screenshots/${LOCALE}" || true + done + + echo "::endgroup::" + done + + - name: "Rename screenshots for flat upload" + run: | + mkdir -p ios-screenshots + for LOCALE_DIR in screenshots/*/; do + LOCALE=$(basename "${LOCALE_DIR}") + INDEX=0 + for IMG in "${LOCALE_DIR}"*.png "${LOCALE_DIR}"**/*.png; do + [ -f "${IMG}" ] || continue + PADDED=$(printf "%02d" ${INDEX}) + cp "${IMG}" "ios-screenshots/Screen-${PADDED}-iOS-${LOCALE}.png" + INDEX=$((INDEX + 1)) + done + done + # Also grab any screenshots from the Maestro output root + INDEX=0 + for IMG in screenshots/*.png; do + [ -f "${IMG}" ] || continue + PADDED=$(printf "%02d" ${INDEX}) + cp "${IMG}" "ios-screenshots/Screen-${PADDED}-iOS-default.png" 2>/dev/null || true + INDEX=$((INDEX + 1)) + done + + - name: "Shutdown simulator" + if: always() + run: xcrun simctl shutdown "${{ steps.simulator.outputs.device-id }}" 2>/dev/null || true + + - name: "Upload iOS screenshots" + uses: actions/upload-artifact@v6 + if: always() + with: + name: ios-screenshots + path: ios-screenshots/ + + # ─────────────────────────────────────────────────────────────────── + # 2b. Run Android app on emulator with Maestro tests + # ─────────────────────────────────────────────────────────────────── + run-android-app: + needs: build-app + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download project sources" + uses: actions/download-artifact@v6 + with: + name: project-sources + + - name: "Enable KVM" + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: "Setup Java" + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: "Install Maestro" + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "${HOME}/.maestro/bin" >> $GITHUB_PATH + + - name: "Start Android Emulator" + uses: reactivecircus/android-emulator-runner@v2 + id: emulator + with: + api-level: 35 + arch: x86_64 + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim + disable-animations: true + script: echo "Emulator started" + + - name: "Ensure Maestro test flows exist" + run: | + if [ ! -d Android/fastlane/Maestro ] || [ -z "$(ls Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml 2>/dev/null)" ]; then + mkdir -p Android/fastlane/Maestro + # Try to extract package name from the APK + APK_PATH=$(find skip-export -name "*.apk" | head -1) + PACKAGE_NAME="" + if [ -n "${APK_PATH}" ]; then + PACKAGE_NAME=$(aapt dump badging "${APK_PATH}" 2>/dev/null | grep "package: name=" | sed "s/.*name='\([^']*\)'.*/\1/" || echo "") + fi + cat > Android/fastlane/Maestro/launch-and-screenshot.yaml <> $GITHUB_OUTPUT + + - name: "Install APK and run Maestro tests for each locale" + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 35 + arch: x86_64 + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim + disable-animations: true + script: | + mkdir -p screenshots + + # Install the APK + APK_PATH=$(find skip-export -name "*-release.apk" -o -name "*-debug.apk" | head -1) + if [ -z "${APK_PATH}" ]; then + APK_PATH=$(find skip-export -name "*.apk" | head -1) + fi + if [ -z "${APK_PATH}" ]; then + echo "No APK found in skip-export" + exit 1 + fi + adb install "${APK_PATH}" + + LOCALES="${{ steps.locales.outputs.locales }}" + + for LOCALE in ${LOCALES}; do + echo "::group::Running Maestro tests for locale ${LOCALE}" + + # Change emulator locale + LANG_CODE="${LOCALE%[-_]*}" + REGION_CODE="${LOCALE#*[-_]}" + adb shell "settings put system system_locales ${LANG_CODE}-${REGION_CODE}" + adb shell "setprop persist.sys.locale ${LANG_CODE}-${REGION_CODE}" + adb shell "setprop persist.sys.language ${LANG_CODE}" + adb shell "setprop persist.sys.country ${REGION_CODE}" + # Apply locale change + adb shell am broadcast -a android.intent.action.LOCALE_CHANGED 2>/dev/null || true + sleep 2 + + for FLOW in Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml; do + [ -f "${FLOW}" ] || continue + echo "Running flow: ${FLOW} (locale: ${LOCALE})" + maestro test "${FLOW}" --output "screenshots/${LOCALE}" || true + done + + echo "::endgroup::" + done + + - name: "Rename screenshots for flat upload" + run: | + mkdir -p android-screenshots + for LOCALE_DIR in screenshots/*/; do + [ -d "${LOCALE_DIR}" ] || continue + LOCALE=$(basename "${LOCALE_DIR}") + INDEX=0 + for IMG in "${LOCALE_DIR}"*.png "${LOCALE_DIR}"**/*.png; do + [ -f "${IMG}" ] || continue + PADDED=$(printf "%02d" ${INDEX}) + cp "${IMG}" "android-screenshots/Screen-${PADDED}-Android-${LOCALE}.png" + INDEX=$((INDEX + 1)) + done + done + INDEX=0 + for IMG in screenshots/*.png; do + [ -f "${IMG}" ] || continue + PADDED=$(printf "%02d" ${INDEX}) + cp "${IMG}" "android-screenshots/Screen-${PADDED}-Android-default.png" 2>/dev/null || true + INDEX=$((INDEX + 1)) + done + + - name: "Upload Android screenshots" + uses: actions/upload-artifact@v6 + if: always() + with: + name: android-screenshots + path: android-screenshots/ + + # ─────────────────────────────────────────────────────────────────── + # 3a. Sign and release iOS app + # ─────────────────────────────────────────────────────────────────── + release-ios-app: + needs: [build-app, run-ios-app] + if: startsWith(github.ref, 'refs/tags/') + runs-on: macos-26 + timeout-minutes: 60 + env: + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download project sources" + uses: actions/download-artifact@v6 + with: + name: project-sources + + - name: "Download iOS screenshots" + uses: actions/download-artifact@v6 + with: + name: ios-screenshots + path: ios-screenshots/ - name: "Setup Darwin App Signing" + id: signing + if: ${{ secrets.APPLE_CERTIFICATES_P12 != '' }} uses: apple-actions/import-codesign-certs@v6 - if: ${{ env.APPLE_CERTIFICATES_P12 != '' }} - env: - APPLE_CERTIFICATES_P12: ${{ secrets.APPLE_CERTIFICATES_P12 }} with: p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12 }} p12-password: ${{ secrets.APPLE_CERTIFICATES_P12_PASSWORD }} - - name: "Android Fastlane" - if: ${{ ! startsWith(github.ref, 'refs/tags/') }} - working-directory: Android - run: test ! -d fastlane || fastlane assemble - - - name: "Darwin Fastlane" - if: ${{ ! startsWith(github.ref, 'refs/tags/') }} - working-directory: Darwin - run: test ! -d fastlane || FASTLANE_SKIP_ARCHIVE=YES FASTLANE_SKIP_CODESIGNING=YES FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=300 FASTLANE_XCODEBUILD_SETTINGS_RETRIES=5 fastlane assemble + - name: "Sign iOS app" + id: sign + if: ${{ secrets.APPLE_CERTIFICATES_P12 != '' }} + run: | + # Re-sign the exported .xcarchive or .ipa with the imported certificate + IPA_PATH=$(find skip-export -name "*.ipa" | head -1) + if [ -n "${IPA_PATH}" ]; then + echo "ipa-path=${IPA_PATH}" >> $GITHUB_OUTPUT + echo "signed=true" >> $GITHUB_OUTPUT + else + echo "No IPA found to sign" + echo "signed=false" >> $GITHUB_OUTPUT + fi - name: "Submit iOS App to App Store" - if: ${{ startsWith(github.ref, 'refs/tags/') && env.APPLE_APPSTORE_APIKEY != '' }} + if: ${{ secrets.APPLE_APPSTORE_APIKEY != '' && secrets.APPLE_CERTIFICATES_P12 != '' }} working-directory: Darwin env: APPLE_APPSTORE_APIKEY: ${{ secrets.APPLE_APPSTORE_APIKEY }} run: | - echo -n "${{ secrets.APPLE_APPSTORE_APIKEY }}" | base64 --decode -o fastlane/apikey.json + echo -n "${APPLE_APPSTORE_APIKEY}" | base64 --decode -o fastlane/apikey.json + # Copy screenshots into fastlane metadata structure + if [ -d ../ios-screenshots ]; then + for IMG in ../ios-screenshots/Screen-*-iOS-*.png; do + [ -f "${IMG}" ] || continue + LOCALE=$(echo "$(basename "${IMG}")" | sed 's/Screen-[0-9]*-iOS-\(.*\)\.png/\1/') + mkdir -p "fastlane/metadata/${LOCALE}/screenshots" + cp "${IMG}" "fastlane/metadata/${LOCALE}/screenshots/" + done + fi fastlane release + - name: "Upload signed iOS artifacts" + uses: actions/upload-artifact@v6 + if: always() + with: + name: ios-signed-artifacts + path: skip-export/*.ipa + + # ─────────────────────────────────────────────────────────────────── + # 3b. Sign and release Android app + # ─────────────────────────────────────────────────────────────────── + release-android-app: + needs: [build-app, run-android-app] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download project sources" + uses: actions/download-artifact@v6 + with: + name: project-sources + + - name: "Download Android screenshots" + uses: actions/download-artifact@v6 + with: + name: android-screenshots + path: android-screenshots/ + + - name: "Sign Android APK" + id: sign + if: ${{ secrets.KEYSTORE_PROPERTIES != '' }} + env: + KEYSTORE_JKS: ${{ secrets.KEYSTORE_JKS }} + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + run: | + echo -n "${KEYSTORE_JKS}" | base64 --decode > Android/app/keystore.jks + echo -n "${KEYSTORE_PROPERTIES}" | base64 --decode > Android/app/keystore.properties + + # Sign the release APK with apksigner + APK_PATH=$(find skip-export -name "*-release-unsigned.apk" -o -name "*-release.apk" | head -1) + if [ -z "${APK_PATH}" ]; then + APK_PATH=$(find skip-export -name "*.apk" | head -1) + fi + + if [ -n "${APK_PATH}" ]; then + # Extract keystore properties + KEYSTORE_FILE="$(pwd)/Android/app/keystore.jks" + STORE_PASSWORD=$(grep 'storePassword' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') + KEY_ALIAS=$(grep 'keyAlias' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') + KEY_PASSWORD=$(grep 'keyPassword' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') + + SIGNED_APK="${APK_PATH%.apk}-signed.apk" + cp "${APK_PATH}" "${SIGNED_APK}" + + # Use apksigner from Android SDK + APKSIGNER=$(find ${ANDROID_HOME}/build-tools -name "apksigner" | sort -V | tail -1) + if [ -n "${APKSIGNER}" ]; then + ${APKSIGNER} sign \ + --ks "${KEYSTORE_FILE}" \ + --ks-pass "pass:${STORE_PASSWORD}" \ + --ks-key-alias "${KEY_ALIAS}" \ + --key-pass "pass:${KEY_PASSWORD}" \ + "${SIGNED_APK}" + fi + + echo "signed=true" >> $GITHUB_OUTPUT + else + echo "No APK found to sign" + echo "signed=false" >> $GITHUB_OUTPUT + fi + - name: "Submit Android App to Play Store" - if: ${{ startsWith(github.ref, 'refs/tags/') && env.GOOGLE_PLAY_APIKEY != '' }} + if: ${{ secrets.GOOGLE_PLAY_APIKEY != '' && secrets.KEYSTORE_PROPERTIES != '' }} + working-directory: Android env: GOOGLE_PLAY_APIKEY: ${{ secrets.GOOGLE_PLAY_APIKEY }} - working-directory: Android run: | echo -n "${GOOGLE_PLAY_APIKEY}" | base64 --decode > fastlane/apikey.json + # Copy screenshots into fastlane metadata structure + if [ -d ../android-screenshots ]; then + for IMG in ../android-screenshots/Screen-*-Android-*.png; do + [ -f "${IMG}" ] || continue + LOCALE=$(echo "$(basename "${IMG}")" | sed 's/Screen-[0-9]*-Android-\(.*\)\.png/\1/') + mkdir -p "fastlane/metadata/android/${LOCALE}/images/phoneScreenshots" + cp "${IMG}" "fastlane/metadata/android/${LOCALE}/images/phoneScreenshots/" + done + fi fastlane release - - name: "Create GitHub Release" - if: startsWith(github.ref, 'refs/tags/') - env: - GH_TOKEN: ${{ github.token }} - # Create a release or upload the assets if the release already exists - run: gh release create "${RELTAG}" -t "Release ${RELTAG}" --generate-notes skip-export/*.* || gh release upload "${RELTAG}" --clobber skip-export/*.* - - - name: "Upload Build Artifacts" + - name: "Upload signed Android artifacts" uses: actions/upload-artifact@v6 if: always() with: + name: android-signed-artifacts + path: | + skip-export/*-signed.apk + skip-export/*.aab + + # ─────────────────────────────────────────────────────────────────── + # 4. Create GitHub Release with all artifacts and screenshots + # ─────────────────────────────────────────────────────────────────── + github-release: + needs: [build-app, release-ios-app, release-android-app] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export path: skip-export/ + - name: "Download iOS screenshots" + uses: actions/download-artifact@v6 + with: + name: ios-screenshots + path: screenshots/ + continue-on-error: true + + - name: "Download Android screenshots" + uses: actions/download-artifact@v6 + with: + name: android-screenshots + path: screenshots/ + continue-on-error: true + + - name: "Download signed iOS artifacts" + uses: actions/download-artifact@v6 + with: + name: ios-signed-artifacts + path: signed-artifacts/ + continue-on-error: true + + - name: "Download signed Android artifacts" + uses: actions/download-artifact@v6 + with: + name: android-signed-artifacts + path: signed-artifacts/ + continue-on-error: true + + - name: "Prepare release assets" + run: | + mkdir -p release-assets + + # Copy build artifacts (skip directories, only files) + find skip-export -maxdepth 1 -type f -exec cp {} release-assets/ \; + + # Copy signed artifacts if they exist + if [ -d signed-artifacts ]; then + find signed-artifacts -type f -exec cp {} release-assets/ \; + fi + + # Copy screenshots with their flat names + if [ -d screenshots ]; then + find screenshots -name "*.png" -exec cp {} release-assets/ \; + fi + + echo "Release assets:" + ls -la release-assets/ + + - name: "Create GitHub Release" + env: + GH_TOKEN: ${{ github.token }} + RELTAG: ${{ needs.build-app.outputs.reltag }} + run: | + gh release create "${RELTAG}" \ + -t "Release ${RELTAG}" \ + --generate-notes \ + --repo "${GITHUB_REPOSITORY}" \ + release-assets/* \ + || gh release upload "${RELTAG}" \ + --clobber \ + --repo "${GITHUB_REPOSITORY}" \ + release-assets/* From e8ff5eb418ab3439c25d1de79abcc06a3bbfd538 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 15:01:31 -0400 Subject: [PATCH 02/24] Add checks for secrets --- .github/workflows/skip-app.yml | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 2f2a811..c2a1e8e 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -456,9 +456,18 @@ jobs: name: ios-screenshots path: ios-screenshots/ + - name: "Check secrets availability" + id: check-secrets + env: + APPLE_CERTIFICATES_P12: ${{ secrets.APPLE_CERTIFICATES_P12 }} + APPLE_APPSTORE_APIKEY: ${{ secrets.APPLE_APPSTORE_APIKEY }} + run: | + echo "has-signing-cert=$(test -z "${APPLE_CERTIFICATES_P12}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + echo "has-appstore-key=$(test -z "${APPLE_APPSTORE_APIKEY}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + - name: "Setup Darwin App Signing" id: signing - if: ${{ secrets.APPLE_CERTIFICATES_P12 != '' }} + if: ${{ steps.check-secrets.outputs.has-signing-cert == 'true' }} uses: apple-actions/import-codesign-certs@v6 with: p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12 }} @@ -466,7 +475,7 @@ jobs: - name: "Sign iOS app" id: sign - if: ${{ secrets.APPLE_CERTIFICATES_P12 != '' }} + if: ${{ steps.check-secrets.outputs.has-signing-cert == 'true' }} run: | # Re-sign the exported .xcarchive or .ipa with the imported certificate IPA_PATH=$(find skip-export -name "*.ipa" | head -1) @@ -479,7 +488,7 @@ jobs: fi - name: "Submit iOS App to App Store" - if: ${{ secrets.APPLE_APPSTORE_APIKEY != '' && secrets.APPLE_CERTIFICATES_P12 != '' }} + if: ${{ steps.check-secrets.outputs.has-appstore-key == 'true' && steps.check-secrets.outputs.has-signing-cert == 'true' }} working-directory: Darwin env: APPLE_APPSTORE_APIKEY: ${{ secrets.APPLE_APPSTORE_APIKEY }} @@ -529,9 +538,18 @@ jobs: name: android-screenshots path: android-screenshots/ + - name: "Check secrets availability" + id: check-secrets + env: + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + GOOGLE_PLAY_APIKEY: ${{ secrets.GOOGLE_PLAY_APIKEY }} + run: | + echo "has-keystore=$(test -z "${KEYSTORE_PROPERTIES}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + echo "has-play-key=$(test -z "${GOOGLE_PLAY_APIKEY}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + - name: "Sign Android APK" id: sign - if: ${{ secrets.KEYSTORE_PROPERTIES != '' }} + if: ${{ steps.check-secrets.outputs.has-keystore == 'true' }} env: KEYSTORE_JKS: ${{ secrets.KEYSTORE_JKS }} KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} @@ -573,7 +591,7 @@ jobs: fi - name: "Submit Android App to Play Store" - if: ${{ secrets.GOOGLE_PLAY_APIKEY != '' && secrets.KEYSTORE_PROPERTIES != '' }} + if: ${{ steps.check-secrets.outputs.has-play-key == 'true' && steps.check-secrets.outputs.has-keystore == 'true' }} working-directory: Android env: GOOGLE_PLAY_APIKEY: ${{ secrets.GOOGLE_PLAY_APIKEY }} From 34fbf78a3bdefca5fd113198afa927055d0bde86 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 16:04:49 -0400 Subject: [PATCH 03/24] Update test scripts --- .github/workflows/skip-app.yml | 114 +++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index c2a1e8e..762c254 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -117,6 +117,33 @@ jobs: skip export -v --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY cp -a Package.resolved skip-export/ + - name: "Build iOS Simulator app" + run: | + XCODEPROJ=$(ls -d Darwin/*.xcodeproj | head -1) + SCHEME="${SKIP_MODULE}" + DERIVED_DATA="$(pwd)/.build/DerivedData" + + xcodebuild build \ + -project "${XCODEPROJ}" \ + -scheme "${SCHEME}" \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "${DERIVED_DATA}" \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + ENABLE_CODE_SIGNING=NO \ + CODE_SIGNING_ALLOWED=NO \ + | tail -20 + + # Find and copy the .app bundle into skip-export + APP_PATH=$(find "${DERIVED_DATA}" -path "*/Build/Products/Debug-iphonesimulator/*.app" -type d | head -1) + if [ -n "${APP_PATH}" ]; then + cp -a "${APP_PATH}" skip-export/ + echo "Simulator .app copied: $(basename "${APP_PATH}")" + else + echo "::warning::No simulator .app bundle found in DerivedData" + fi + - name: "Upload build artifacts" uses: actions/upload-artifact@v6 with: @@ -353,6 +380,48 @@ jobs: fi echo "locales=${LOCALES}" >> $GITHUB_OUTPUT + - name: "Prepare Maestro script" + run: | + cat > maestro.sh <<'EOF' + mkdir -p screenshots + + # Install the APK + APK_PATH=$(find skip-export -name "*-release.apk" -o -name "*-debug.apk" | head -1) + if [ -z "${APK_PATH}" ]; then + APK_PATH=$(find skip-export -name "*.apk" | head -1) + fi + if [ -z "${APK_PATH}" ]; then + echo "No APK found in skip-export" + exit 1 + fi + adb install "${APK_PATH}" + + LOCALES="${{ steps.locales.outputs.locales }}" + + for LOCALE in ${LOCALES}; do + echo "::group::Running Maestro tests for locale ${LOCALE}" + + # Change emulator locale + LANG_CODE="${LOCALE%[-_]*}" + REGION_CODE="${LOCALE#*[-_]}" + adb shell "settings put system system_locales ${LANG_CODE}-${REGION_CODE}" + adb shell "setprop persist.sys.locale ${LANG_CODE}-${REGION_CODE}" + adb shell "setprop persist.sys.language ${LANG_CODE}" + adb shell "setprop persist.sys.country ${REGION_CODE}" + # Apply locale change + adb shell am broadcast -a android.intent.action.LOCALE_CHANGED 2>/dev/null || true + sleep 2 + + for FLOW in Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml; do + [ -f "${FLOW}" ] || continue + echo "Running flow: ${FLOW} (locale: ${LOCALE})" + maestro test "${FLOW}" --output "screenshots/${LOCALE}" || true + done + + echo "::endgroup::" + done + EOF + - name: "Install APK and run Maestro tests for each locale" uses: reactivecircus/android-emulator-runner@v2 with: @@ -360,44 +429,7 @@ jobs: arch: x86_64 emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim disable-animations: true - script: | - mkdir -p screenshots - - # Install the APK - APK_PATH=$(find skip-export -name "*-release.apk" -o -name "*-debug.apk" | head -1) - if [ -z "${APK_PATH}" ]; then - APK_PATH=$(find skip-export -name "*.apk" | head -1) - fi - if [ -z "${APK_PATH}" ]; then - echo "No APK found in skip-export" - exit 1 - fi - adb install "${APK_PATH}" - - LOCALES="${{ steps.locales.outputs.locales }}" - - for LOCALE in ${LOCALES}; do - echo "::group::Running Maestro tests for locale ${LOCALE}" - - # Change emulator locale - LANG_CODE="${LOCALE%[-_]*}" - REGION_CODE="${LOCALE#*[-_]}" - adb shell "settings put system system_locales ${LANG_CODE}-${REGION_CODE}" - adb shell "setprop persist.sys.locale ${LANG_CODE}-${REGION_CODE}" - adb shell "setprop persist.sys.language ${LANG_CODE}" - adb shell "setprop persist.sys.country ${REGION_CODE}" - # Apply locale change - adb shell am broadcast -a android.intent.action.LOCALE_CHANGED 2>/dev/null || true - sleep 2 - - for FLOW in Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml; do - [ -f "${FLOW}" ] || continue - echo "Running flow: ${FLOW} (locale: ${LOCALE})" - maestro test "${FLOW}" --output "screenshots/${LOCALE}" || true - done - - echo "::endgroup::" - done + script: sh -ex maestro.sh - name: "Rename screenshots for flat upload" run: | @@ -432,7 +464,7 @@ jobs: # 3a. Sign and release iOS app # ─────────────────────────────────────────────────────────────────── release-ios-app: - needs: [build-app, run-ios-app] + needs: run-ios-app if: startsWith(github.ref, 'refs/tags/') runs-on: macos-26 timeout-minutes: 60 @@ -516,7 +548,7 @@ jobs: # 3b. Sign and release Android app # ─────────────────────────────────────────────────────────────────── release-android-app: - needs: [build-app, run-android-app] + needs: run-android-app if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-24.04 timeout-minutes: 60 @@ -621,7 +653,7 @@ jobs: # 4. Create GitHub Release with all artifacts and screenshots # ─────────────────────────────────────────────────────────────────── github-release: - needs: [build-app, release-ios-app, release-android-app] + needs: [release-ios-app, release-android-app] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-24.04 timeout-minutes: 15 From bc5a5c24ae721d7b94645a161f146805bc104e2a Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 17:42:49 -0400 Subject: [PATCH 04/24] Update test scripts --- .github/workflows/skip-app.yml | 65 ++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 762c254..1ac2c44 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -107,15 +107,27 @@ jobs: install-swift-android-sdk: ${{ steps.setup.outputs.skip-fuse }} swift-android-sdk-version: nightly-6.3 - - name: "Run Tests" + - name: "Build Project" + run: swift build + + - name: "Test Project" if: ${{ inputs.run-local-tests == true && ! startsWith(github.ref, 'refs/tags/') }} run: test ! -d Tests || skip test - - name: "Export project" + - name: "Process Package.resolved" + run: | + cat Package.resolved + mkdir skip-export + cp -a Package.resolved skip-export/ + + - name: "Export project (debug)" run: | skip export -v --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY + + - name: "Export project (release)" + if: startsWith(github.ref, 'refs/tags/') + run: | skip export -v --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY - cp -a Package.resolved skip-export/ - name: "Build iOS Simulator app" run: | @@ -132,14 +144,15 @@ jobs: -skipPackagePluginValidation \ -skipMacroValidation \ ENABLE_CODE_SIGNING=NO \ - CODE_SIGNING_ALLOWED=NO \ - | tail -20 + CODE_SIGNING_ALLOWED=NO - # Find and copy the .app bundle into skip-export + # Find the .app bundle and zip it to preserve symlinks and bundle structure + # (actions/upload-artifact does not preserve symlinks) APP_PATH=$(find "${DERIVED_DATA}" -path "*/Build/Products/Debug-iphonesimulator/*.app" -type d | head -1) if [ -n "${APP_PATH}" ]; then - cp -a "${APP_PATH}" skip-export/ - echo "Simulator .app copied: $(basename "${APP_PATH}")" + APP_NAME=$(basename "${APP_PATH}") + (cd "$(dirname "${APP_PATH}")" && zip -r -y "${OLDPWD}/skip-export/${APP_NAME}.zip" "${APP_NAME}") + echo "Simulator .app zipped: ${APP_NAME}.zip" else echo "::warning::No simulator .app bundle found in DerivedData" fi @@ -197,6 +210,12 @@ jobs: - name: "Install app on simulator" run: | + # Unzip the .app bundle (zipped to preserve symlinks during artifact transfer) + APP_ZIP=$(find skip-export -name "*.app.zip" | head -1) + if [ -n "${APP_ZIP}" ]; then + unzip -o "${APP_ZIP}" -d skip-export/ + fi + # Find the .app bundle from the export (debug build for testing) APP_PATH=$(find skip-export -name "*.app" -type d | head -1) if [ -z "${APP_PATH}" ]; then @@ -260,7 +279,7 @@ jobs: for FLOW in Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml; do [ -f "${FLOW}" ] || continue echo "Running flow: ${FLOW} (locale: ${LOCALE})" - maestro --device "${DEVICE_ID}" test "${FLOW}" --output "screenshots/${LOCALE}" || true + maestro --device "${DEVICE_ID}" test "${FLOW}" --output "screenshots/${LOCALE}" done echo "::endgroup::" @@ -284,7 +303,7 @@ jobs: for IMG in screenshots/*.png; do [ -f "${IMG}" ] || continue PADDED=$(printf "%02d" ${INDEX}) - cp "${IMG}" "ios-screenshots/Screen-${PADDED}-iOS-default.png" 2>/dev/null || true + cp -v "${IMG}" "ios-screenshots/Screen-${PADDED}-iOS-default.png" INDEX=$((INDEX + 1)) done @@ -324,12 +343,6 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: "Setup Java" - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - - name: "Install Maestro" run: | curl -Ls "https://get.maestro.mobile.dev" | bash @@ -385,6 +398,10 @@ jobs: cat > maestro.sh <<'EOF' mkdir -p screenshots + # Enable root access so setprop commands can modify system properties + adb root || true + adb wait-for-device + # Install the APK APK_PATH=$(find skip-export -name "*-release.apk" -o -name "*-debug.apk" | head -1) if [ -z "${APK_PATH}" ]; then @@ -404,18 +421,20 @@ jobs: # Change emulator locale LANG_CODE="${LOCALE%[-_]*}" REGION_CODE="${LOCALE#*[-_]}" - adb shell "settings put system system_locales ${LANG_CODE}-${REGION_CODE}" - adb shell "setprop persist.sys.locale ${LANG_CODE}-${REGION_CODE}" - adb shell "setprop persist.sys.language ${LANG_CODE}" - adb shell "setprop persist.sys.country ${REGION_CODE}" + + adb shell "setprop persist.sys.locale ${LANG_CODE}-${REGION_CODE}" || true + adb shell "setprop persist.sys.language ${LANG_CODE}" || true + adb shell "setprop persist.sys.country ${REGION_CODE}" || true + adb shell "settings put system system_locales ${LANG_CODE}-${REGION_CODE}" || true + # Apply locale change - adb shell am broadcast -a android.intent.action.LOCALE_CHANGED 2>/dev/null || true + adb shell am broadcast -a android.intent.action.LOCALE_CHANGED || true sleep 2 for FLOW in Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml; do [ -f "${FLOW}" ] || continue echo "Running flow: ${FLOW} (locale: ${LOCALE})" - maestro test "${FLOW}" --output "screenshots/${LOCALE}" || true + maestro test "${FLOW}" --output "screenshots/${LOCALE}" done echo "::endgroup::" @@ -449,7 +468,7 @@ jobs: for IMG in screenshots/*.png; do [ -f "${IMG}" ] || continue PADDED=$(printf "%02d" ${INDEX}) - cp "${IMG}" "android-screenshots/Screen-${PADDED}-Android-default.png" 2>/dev/null || true + cp -v "${IMG}" "android-screenshots/Screen-${PADDED}-Android-default.png" 2>/dev/null INDEX=$((INDEX + 1)) done From 74931bc3de7ab96a705f7c80699f223053cbb911 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 17:48:29 -0400 Subject: [PATCH 05/24] Update test scripts --- .github/workflows/skip-app.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 1ac2c44..4c5f364 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -107,19 +107,14 @@ jobs: install-swift-android-sdk: ${{ steps.setup.outputs.skip-fuse }} swift-android-sdk-version: nightly-6.3 - - name: "Build Project" - run: swift build + # not all projects build on macOS + #- name: "Build Project" + # run: swift build - name: "Test Project" if: ${{ inputs.run-local-tests == true && ! startsWith(github.ref, 'refs/tags/') }} run: test ! -d Tests || skip test - - name: "Process Package.resolved" - run: | - cat Package.resolved - mkdir skip-export - cp -a Package.resolved skip-export/ - - name: "Export project (debug)" run: | skip export -v --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY @@ -157,6 +152,12 @@ jobs: echo "::warning::No simulator .app bundle found in DerivedData" fi + - name: "Process Package.resolved" + run: | + cat Package.resolved + mkdir -p skip-export + cp -a Package.resolved skip-export/ + - name: "Upload build artifacts" uses: actions/upload-artifact@v6 with: From 6ba9ae178fe634318a279967d8cf5aea61ecd95e Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 18:23:18 -0400 Subject: [PATCH 06/24] Update test scripts --- .github/workflows/skip-app.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 4c5f364..d356936 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -107,9 +107,10 @@ jobs: install-swift-android-sdk: ${{ steps.setup.outputs.skip-fuse }} swift-android-sdk-version: nightly-6.3 - # not all projects build on macOS - #- name: "Build Project" - # run: swift build + - name: "Build Project" + #run: swift build + # not all projects build on macOS, so we need to build for iOS + run: xcrun swift build --triple arm64-apple-ios --sdk "$(xcrun --sdk iphoneos --show-sdk-path)" - name: "Test Project" if: ${{ inputs.run-local-tests == true && ! startsWith(github.ref, 'refs/tags/') }} @@ -129,6 +130,8 @@ jobs: XCODEPROJ=$(ls -d Darwin/*.xcodeproj | head -1) SCHEME="${SKIP_MODULE}" DERIVED_DATA="$(pwd)/.build/DerivedData" + SKIP_ZERO=1 + SKIP_PLUGIN_DISABLED=1 xcodebuild build \ -project "${XCODEPROJ}" \ From a57831432eacdd2898439eb928e93c364f55eb8a Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 18:53:43 -0400 Subject: [PATCH 07/24] Update test scripts --- .github/workflows/skip-app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index d356936..735638b 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -130,8 +130,8 @@ jobs: XCODEPROJ=$(ls -d Darwin/*.xcodeproj | head -1) SCHEME="${SKIP_MODULE}" DERIVED_DATA="$(pwd)/.build/DerivedData" - SKIP_ZERO=1 - SKIP_PLUGIN_DISABLED=1 + export SKIP_ZERO=1 + export SKIP_PLUGIN_DISABLED=1 xcodebuild build \ -project "${XCODEPROJ}" \ From 08c864251759b84899941110db0c8298148036aa Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 19:56:01 -0400 Subject: [PATCH 08/24] Update test scripts --- .github/workflows/skip-app.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 735638b..2be7b18 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -232,7 +232,8 @@ jobs: run: | if [ ! -d Darwin/fastlane/Maestro ] || [ -z "$(ls Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml 2>/dev/null)" ]; then mkdir -p Darwin/fastlane/Maestro - BUNDLE_ID=$(defaults read "$(find skip-export -name '*.app' -type d | head -1)/Info.plist" CFBundleIdentifier 2>/dev/null || echo "") + # note that "defaults read" needs an absolute path to the .plist + BUNDLE_ID=$(defaults read "$(find ${PWD}/skip-export -name '*.app' -type d | head -1)/Info.plist" CFBundleIdentifier 2>/dev/null || echo "") cat > Darwin/fastlane/Maestro/launch-and-screenshot.yaml < maestro.sh <<'EOF' + export MAESTRO_CLI_NO_ANALYTICS=1 + mkdir -p screenshots # Enable root access so setprop commands can modify system properties @@ -438,7 +442,7 @@ jobs: for FLOW in Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml; do [ -f "${FLOW}" ] || continue echo "Running flow: ${FLOW} (locale: ${LOCALE})" - maestro test "${FLOW}" --output "screenshots/${LOCALE}" + maestro --platform android test "${FLOW}" --output "screenshots/${LOCALE}" done echo "::endgroup::" From fd40b70beec9ba63ad52c5e8d590e182c95ec03d Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 20:33:53 -0400 Subject: [PATCH 09/24] Update test scripts --- .github/workflows/skip-app.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 2be7b18..d8e11aa 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -232,8 +232,7 @@ jobs: run: | if [ ! -d Darwin/fastlane/Maestro ] || [ -z "$(ls Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml 2>/dev/null)" ]; then mkdir -p Darwin/fastlane/Maestro - # note that "defaults read" needs an absolute path to the .plist - BUNDLE_ID=$(defaults read "$(find ${PWD}/skip-export -name '*.app' -type d | head -1)/Info.plist" CFBundleIdentifier 2>/dev/null || echo "") + BUNDLE_ID=$(grep "^PRODUCT_BUNDLE_IDENTIFIER" Skip.env | cut -d'=' -f2 | tr -d ' ') cat > Darwin/fastlane/Maestro/launch-and-screenshot.yaml </dev/null)" ]; then mkdir -p Android/fastlane/Maestro - # Try to extract package name from the APK - APK_PATH=$(find skip-export -name "*.apk" | head -1) - PACKAGE_NAME="" - if [ -n "${APK_PATH}" ]; then - PACKAGE_NAME=$(aapt dump badging "${APK_PATH}" 2>/dev/null | grep "package: name=" | sed "s/.*name='\([^']*\)'.*/\1/" || echo "") - fi + PACKAGE_NAME=$(grep "^ANDROID_PACKAGE_NAME" Skip.env | cut -d'=' -f2 | tr -d ' ') cat > Android/fastlane/Maestro/launch-and-screenshot.yaml < Date: Wed, 18 Mar 2026 21:17:40 -0400 Subject: [PATCH 10/24] Update test scripts --- .github/workflows/skip-app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index d8e11aa..7c0168f 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -284,7 +284,7 @@ jobs: for FLOW in Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml; do [ -f "${FLOW}" ] || continue echo "Running flow: ${FLOW} (locale: ${LOCALE})" - maestro --platform ios --device "${DEVICE_ID}" test "${FLOW}" --output "screenshots/${LOCALE}" + maestro --platform ios --device "${DEVICE_ID}" test "${FLOW}" --test-output-dir "screenshots/${LOCALE}" done echo "::endgroup::" @@ -437,7 +437,7 @@ jobs: for FLOW in Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml; do [ -f "${FLOW}" ] || continue echo "Running flow: ${FLOW} (locale: ${LOCALE})" - maestro --platform android test "${FLOW}" --output "screenshots/${LOCALE}" + maestro --platform android test "${FLOW}" --test-output-dir "screenshots/${LOCALE}" done echo "::endgroup::" From 2cd27802ddb46a195eb7574234218ff1c6c85b17 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 22:01:29 -0400 Subject: [PATCH 11/24] Update test scripts --- .github/workflows/skip-app.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 7c0168f..b38534d 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -292,7 +292,7 @@ jobs: - name: "Rename screenshots for flat upload" run: | - find screenshots/ + find . -name '*.png' mkdir -p ios-screenshots for LOCALE_DIR in screenshots/*/; do LOCALE=$(basename "${LOCALE_DIR}") @@ -354,16 +354,6 @@ jobs: curl -Ls "https://get.maestro.mobile.dev" | bash echo "${HOME}/.maestro/bin" >> $GITHUB_PATH - - name: "Start Android Emulator" - uses: reactivecircus/android-emulator-runner@v2 - id: emulator - with: - api-level: 35 - arch: x86_64 - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim - disable-animations: true - script: echo "Emulator started" - - name: "Ensure Maestro test flows exist" run: | if [ ! -d Android/fastlane/Maestro ] || [ -z "$(ls Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml 2>/dev/null)" ]; then @@ -455,6 +445,7 @@ jobs: - name: "Rename screenshots for flat upload" run: | + find . -name '*.png' mkdir -p android-screenshots for LOCALE_DIR in screenshots/*/; do [ -d "${LOCALE_DIR}" ] || continue From 4d15cb8d437a48227b4e4c376790da18f52e8495 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Wed, 18 Mar 2026 23:06:49 -0400 Subject: [PATCH 12/24] Update test scripts --- .github/workflows/skip-app.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index b38534d..b5aee8b 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -358,7 +358,8 @@ jobs: run: | if [ ! -d Android/fastlane/Maestro ] || [ -z "$(ls Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml 2>/dev/null)" ]; then mkdir -p Android/fastlane/Maestro - PACKAGE_NAME=$(grep "^ANDROID_PACKAGE_NAME" Skip.env | cut -d'=' -f2 | tr -d ' ') + #PACKAGE_NAME=$(grep "^ANDROID_PACKAGE_NAME" Skip.env | cut -d'=' -f2 | tr -d ' ') + PACKAGE_NAME=$(grep "^PRODUCT_BUNDLE_IDENTIFIER" Skip.env | cut -d'=' -f2 | tr -d ' ' | tr '-' '_') cat > Android/fastlane/Maestro/launch-and-screenshot.yaml < Date: Thu, 19 Mar 2026 15:47:39 -0400 Subject: [PATCH 13/24] Export project in release mode --- .github/workflows/skip-app.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index b5aee8b..032f351 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -116,14 +116,10 @@ jobs: if: ${{ inputs.run-local-tests == true && ! startsWith(github.ref, 'refs/tags/') }} run: test ! -d Tests || skip test - - name: "Export project (debug)" - run: | - skip export -v --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY - - name: "Export project (release)" - if: startsWith(github.ref, 'refs/tags/') run: | - skip export -v --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY + # TODO: --ios-sim + skip export -v --ios --android --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY - name: "Build iOS Simulator app" run: | From 38f088d83790472de638eebdd0ab4ac765547103 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Thu, 19 Mar 2026 16:09:22 -0400 Subject: [PATCH 14/24] Export project in debug and release mode --- .github/workflows/skip-app.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 032f351..8790e15 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -116,7 +116,13 @@ jobs: if: ${{ inputs.run-local-tests == true && ! startsWith(github.ref, 'refs/tags/') }} run: test ! -d Tests || skip test + # ideally we would skip the debug release, but if we don't do it, the debug keystore won't be automatically found and used + - name: "Export project (debug)" + run: | + skip export -v --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY + - name: "Export project (release)" + if: startsWith(github.ref, 'refs/tags/') run: | # TODO: --ios-sim skip export -v --ios --android --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY From 5a8fecee496e5c3cb1914d543e47be6093f31337 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Thu, 19 Mar 2026 21:23:22 -0400 Subject: [PATCH 15/24] Set device to pixel_9 and waitForAnimationToEnd in screenshot test --- .github/workflows/skip-app.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 8790e15..6b549a1 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -239,6 +239,7 @@ jobs: appId: ${BUNDLE_ID} --- - launchApp + - waitForAnimationToEnd - takeScreenshot: Screen-00 FLOWEOF fi @@ -366,6 +367,7 @@ jobs: appId: ${PACKAGE_NAME} --- - launchApp + - waitForAnimationToEnd - takeScreenshot: Screen-00 FLOWEOF fi @@ -442,6 +444,7 @@ jobs: with: api-level: 35 arch: x86_64 + profile: pixel_9 emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim disable-animations: true script: sh -ex maestro.sh From 65f66a1721deba3a953b4ed477830dcb33cd5048 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Thu, 19 Mar 2026 22:56:05 -0400 Subject: [PATCH 16/24] Update emulator profile to use pixel_7_pro --- .github/workflows/skip-app.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 6b549a1..ec4eabd 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -444,7 +444,8 @@ jobs: with: api-level: 35 arch: x86_64 - profile: pixel_9 + profile: pixel_7_pro + #profile: pixel_9 # not available? emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim disable-animations: true script: sh -ex maestro.sh From 02d7eaabe29e366bb591ac7d427a8a8e8ce5f5e3 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 20 Mar 2026 11:57:08 -0400 Subject: [PATCH 17/24] Add self-test of skip-app and skip-framework workflows to actions CI --- .github/workflows/ci.yml | 10 ++++++++++ .github/workflows/skip-app.yml | 7 ++++++- .github/workflows/skip-framework.yml | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac3cfe2..f4fa259 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,3 +19,13 @@ jobs: - run: skip checkup if: ${{ startsWith(matrix.os, 'macos-') }} + skip-app-test: + uses: ./.github/workflows/skip-app.yml + with: + repository: skiptools/skipapp-hello + + skip-framework-test: + uses: ./.github/workflows/skip-framework.yml + with: + repository: skiptools/skip-lib + diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index ec4eabd..4cfca2b 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -38,6 +38,10 @@ name: "Skip App CI" on: workflow_call: inputs: + repository: + required: false + type: string + description: "Repository to checkout (owner/repo). Defaults to the calling repository." brew-install: required: false type: string @@ -76,6 +80,7 @@ jobs: steps: - uses: actions/checkout@v6 with: + repository: ${{ inputs.repository || github.repository }} submodules: 'recursive' - name: "Setup" @@ -208,7 +213,7 @@ jobs: id: simulator run: | # Create and boot a simulator - DEVICE_ID=$(xcrun simctl create "MaestroTest" "iPhone 16") + DEVICE_ID=$(xcrun simctl create "MaestroTest" "iPhone Air") echo "device-id=${DEVICE_ID}" >> $GITHUB_OUTPUT xcrun simctl boot "${DEVICE_ID}" # Wait for the simulator to be ready diff --git a/.github/workflows/skip-framework.yml b/.github/workflows/skip-framework.yml index 95c3aab..27cd7d0 100644 --- a/.github/workflows/skip-framework.yml +++ b/.github/workflows/skip-framework.yml @@ -30,6 +30,10 @@ name: "Skip Framework CI" on: workflow_call: inputs: + repository: + required: false + type: string + description: "Repository to checkout (owner/repo). Defaults to the calling repository." # Need an Intel macOS runner to be able to run the Android emulator # https://github.com/ReactiveCircus/android-emulator-runner/issues/350 runs-on: @@ -104,6 +108,7 @@ jobs: steps: - uses: actions/checkout@v6 with: + repository: ${{ inputs.repository || github.repository }} submodules: 'recursive' - name: Setup Environment From 4a312dd832614f5f3ed1b5522cf8156eea5b2df0 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 20 Mar 2026 11:58:58 -0400 Subject: [PATCH 18/24] Update actions CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4fa259..4c72c3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,10 @@ on: branches: [ main ] workflow_dispatch: pull_request: + +permissions: + contents: write + jobs: setup-skip-test: strategy: From 9f09a6228aeaad63df74b00220751ca4597d8077 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 20 Mar 2026 12:23:58 -0400 Subject: [PATCH 19/24] Update actions CI to run framework tests on both macos and ubuntu --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c72c3a..5f9a97e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,4 +32,5 @@ jobs: uses: ./.github/workflows/skip-framework.yml with: repository: skiptools/skip-lib + runs-on: "['macos-15-intel', 'ubuntu-24.04']" From 5ca7b9f1ac468672b8f3656b1a7df4aff2430341 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 20 Mar 2026 12:50:31 -0400 Subject: [PATCH 20/24] Move new skip-app to skip-application --- .github/workflows/ci.yml | 7 + .github/workflows/skip-app.yml | 687 ++-------------------- .github/workflows/skip-application.yml | 755 +++++++++++++++++++++++++ 3 files changed, 820 insertions(+), 629 deletions(-) create mode 100644 .github/workflows/skip-application.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f9a97e..c373faa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,13 @@ jobs: - run: skip checkup if: ${{ startsWith(matrix.os, 'macos-') }} + # new skip-application + skip-application-test: + uses: ./.github/workflows/skip-application.yml + with: + repository: skiptools/skipapp-hello + + # old skip-app skip-app-test: uses: ./.github/workflows/skip-app.yml with: diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index 4cfca2b..af419d8 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -6,14 +6,6 @@ # When tagged with a semantic version (e.g., "1.2.3"), the action will # create and distribute release .apk and .ipa artifacts from the project. # -# Jobs: -# build-app – checkout, test, skip export, upload artifacts -# run-ios-app – install exported app on iOS simulator, run Maestro tests per locale -# run-android-app – install exported app on Android emulator, run Maestro tests per locale -# release-ios-app – sign and submit iOS app via Fastlane (tag only, secrets required) -# release-android-app – sign and submit Android app via Fastlane (tag only, secrets required) -# github-release – create GitHub release with all artifacts and screenshots -# # An example invocation script is as follows, which runs for # every push, every PR, every semver tag, and every day at noon GMT: # @@ -38,10 +30,6 @@ name: "Skip App CI" on: workflow_call: inputs: - repository: - required: false - type: string - description: "Repository to checkout (owner/repo). Defaults to the calling repository." brew-install: required: false type: string @@ -64,31 +52,31 @@ on: required: false APPLE_MOBILEPROVISION: required: false - jobs: - # ─────────────────────────────────────────────────────────────────── - # 1. Build, test, and export the app - # ─────────────────────────────────────────────────────────────────── - build-app: - runs-on: macos-26 + skip-app: + runs-on: macos-15-intel timeout-minutes: 180 env: DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer - outputs: - reltag: ${{ steps.setup.outputs.reltag }} - skip-module: ${{ steps.setup.outputs.skip-module }} steps: - uses: actions/checkout@v6 with: - repository: ${{ inputs.repository || github.repository }} submodules: 'recursive' - name: "Setup" id: setup + env: + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} run: | + # convert "refs/tags/1.0.0" to "1.0.0" + # and "refs/heads/main" to "main" TAG=${GITHUB_REF#refs/*/} - echo "reltag=${TAG:-'dev'}" >> $GITHUB_OUTPUT + # the version if it matches the semantic tag pattern, otherwise "dev" + echo "RELTAG=${TAG:-'dev'}" >> $GITHUB_ENV + echo "HOMEBREW_PREFIX: ${HOMEBREW_PREFIX}" + # needed for Android/fastlane/Fastfile Gradle build on X86 + # or else it cannot find the gradle command if [[ "${RUNNER_ARCH}" == 'ARM64' ]]; then echo "HOMEBREW_PREFIX=/opt/homebrew" >> $GITHUB_ENV else @@ -97,13 +85,25 @@ jobs: echo "COMMIT_DATE=$(git log -1 --format=%ad --date=iso-strict ${TAG})" >> $GITHUB_ENV + # the primary skip module is the first product name SKIP_MODULE=$(basename Darwin/*.xcodeproj .xcodeproj) - echo "skip-module=${SKIP_MODULE}" >> $GITHUB_OUTPUT echo "SKIP_MODULE=${SKIP_MODULE}" >> $GITHUB_ENV + # update Skip.env to set version string to the latest semver tag sed -i '' "s;MARKETING_VERSION = .*;MARKETING_VERSION = $(git describe --tags --abbrev=0 --match '[0-9]*\.[0-9]*\.[0-9]*' --first-parent);g" Skip.env + + # update Skip.env to set build number to the git commit count sed -i '' "s;PRODUCT_VERSION = .*;PRODUCT_VERSION = $(git rev-list --count HEAD);g" Skip.env + # the emulator we use should match the host architecture + echo "android-arch=$(uname -m | sed 's/arm64/arm64-v8a/')" >> $GITHUB_OUTPUT + + # check whether the KEYSTORE_PROPERTIES secret exists, + # and if so we will run the signing option later + echo 'android-keystore-exists=$(test -z "${KEYSTORE_PROPERTIES}" && echo "false" || echo "true")' >> $GITHUB_OUTPUT + + # check whether any of the skip.yml files in the project + # presume the native toolchain yq -e '.skip.mode' Sources/*/Skip/skip.yml >> /dev/null && echo "skip-fuse=true" >> $GITHUB_OUTPUT || echo "Native toolchain not needed" - uses: skiptools/actions/setup-skip@v1 @@ -112,644 +112,73 @@ jobs: install-swift-android-sdk: ${{ steps.setup.outputs.skip-fuse }} swift-android-sdk-version: nightly-6.3 - - name: "Build Project" - #run: swift build - # not all projects build on macOS, so we need to build for iOS - run: xcrun swift build --triple arm64-apple-ios --sdk "$(xcrun --sdk iphoneos --show-sdk-path)" - - - name: "Test Project" + - name: "Run Tests" if: ${{ inputs.run-local-tests == true && ! startsWith(github.ref, 'refs/tags/') }} run: test ! -d Tests || skip test - # ideally we would skip the debug release, but if we don't do it, the debug keystore won't be automatically found and used - - name: "Export project (debug)" + - name: "Export project" run: | + # need to run twice due to a bug with debug key availability skip export -v --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY - - - name: "Export project (release)" - if: startsWith(github.ref, 'refs/tags/') - run: | - # TODO: --ios-sim - skip export -v --ios --android --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY - - - name: "Build iOS Simulator app" - run: | - XCODEPROJ=$(ls -d Darwin/*.xcodeproj | head -1) - SCHEME="${SKIP_MODULE}" - DERIVED_DATA="$(pwd)/.build/DerivedData" - export SKIP_ZERO=1 - export SKIP_PLUGIN_DISABLED=1 - - xcodebuild build \ - -project "${XCODEPROJ}" \ - -scheme "${SCHEME}" \ - -configuration Debug \ - -destination 'generic/platform=iOS Simulator' \ - -derivedDataPath "${DERIVED_DATA}" \ - -skipPackagePluginValidation \ - -skipMacroValidation \ - ENABLE_CODE_SIGNING=NO \ - CODE_SIGNING_ALLOWED=NO - - # Find the .app bundle and zip it to preserve symlinks and bundle structure - # (actions/upload-artifact does not preserve symlinks) - APP_PATH=$(find "${DERIVED_DATA}" -path "*/Build/Products/Debug-iphonesimulator/*.app" -type d | head -1) - if [ -n "${APP_PATH}" ]; then - APP_NAME=$(basename "${APP_PATH}") - (cd "$(dirname "${APP_PATH}")" && zip -r -y "${OLDPWD}/skip-export/${APP_NAME}.zip" "${APP_NAME}") - echo "Simulator .app zipped: ${APP_NAME}.zip" - else - echo "::warning::No simulator .app bundle found in DerivedData" - fi - - - name: "Process Package.resolved" - run: | - cat Package.resolved - mkdir -p skip-export + skip export -v --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY cp -a Package.resolved skip-export/ - - name: "Upload build artifacts" - uses: actions/upload-artifact@v6 - with: - name: skip-export - path: skip-export/ - - - name: "Upload project sources" - uses: actions/upload-artifact@v6 - with: - name: project-sources - path: | - Darwin/ - Android/ - Skip.env - - # ─────────────────────────────────────────────────────────────────── - # 2a. Run iOS app on simulator with Maestro tests - # ─────────────────────────────────────────────────────────────────── - run-ios-app: - needs: build-app - runs-on: macos-26 - timeout-minutes: 60 - env: - DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer - steps: - - name: "Download build artifacts" - uses: actions/download-artifact@v6 - with: - name: skip-export - path: skip-export/ - - - name: "Download project sources" - uses: actions/download-artifact@v6 - with: - name: project-sources - - - name: "Install Maestro" - run: | - curl -Ls "https://get.maestro.mobile.dev" | bash - echo "${HOME}/.maestro/bin" >> $GITHUB_PATH - - - name: "Boot iOS Simulator" - id: simulator - run: | - # Create and boot a simulator - DEVICE_ID=$(xcrun simctl create "MaestroTest" "iPhone Air") - echo "device-id=${DEVICE_ID}" >> $GITHUB_OUTPUT - xcrun simctl boot "${DEVICE_ID}" - # Wait for the simulator to be ready - xcrun simctl bootstatus "${DEVICE_ID}" - - - name: "Install app on simulator" - run: | - # Unzip the .app bundle (zipped to preserve symlinks during artifact transfer) - APP_ZIP=$(find skip-export -name "*.app.zip" | head -1) - if [ -n "${APP_ZIP}" ]; then - unzip -o "${APP_ZIP}" -d skip-export/ - fi - - # Find the .app bundle from the export (debug build for testing) - APP_PATH=$(find skip-export -name "*.app" -type d | head -1) - if [ -z "${APP_PATH}" ]; then - echo "No .app bundle found in skip-export" - exit 1 - fi - xcrun simctl install "${{ steps.simulator.outputs.device-id }}" "${APP_PATH}" - - - name: "Ensure Maestro test flows exist" - run: | - if [ ! -d Darwin/fastlane/Maestro ] || [ -z "$(ls Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml 2>/dev/null)" ]; then - mkdir -p Darwin/fastlane/Maestro - BUNDLE_ID=$(grep "^PRODUCT_BUNDLE_IDENTIFIER" Skip.env | cut -d'=' -f2 | tr -d ' ') - cat > Darwin/fastlane/Maestro/launch-and-screenshot.yaml <> $GITHUB_OUTPUT - - - name: "Run Maestro tests for each locale" - env: - DEVICE_ID: ${{ steps.simulator.outputs.device-id }} - run: | - mkdir -p screenshots - export MAESTRO_CLI_NO_ANALYTICS=1 - LOCALES="${{ steps.locales.outputs.locales }}" - - for LOCALE in ${LOCALES}; do - echo "::group::Running Maestro tests for locale ${LOCALE}" - - # Change simulator locale and restart SpringBoard - # Convert locale format (e.g., en-US -> en_US for simctl) - LANG_CODE="${LOCALE%[-_]*}" - REGION_CODE="${LOCALE#*[-_]}" - xcrun simctl spawn "${DEVICE_ID}" defaults write -g AppleLanguages -array "${LANG_CODE}" - xcrun simctl spawn "${DEVICE_ID}" defaults write -g AppleLocale "${LANG_CODE}_${REGION_CODE}" - # Restart SpringBoard to apply locale change - xcrun simctl spawn "${DEVICE_ID}" launchctl stop com.apple.SpringBoard 2>/dev/null || true - sleep 3 - - # Run each Maestro flow - for FLOW in Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml; do - [ -f "${FLOW}" ] || continue - echo "Running flow: ${FLOW} (locale: ${LOCALE})" - maestro --platform ios --device "${DEVICE_ID}" test "${FLOW}" --test-output-dir "screenshots/${LOCALE}" - done - - echo "::endgroup::" - done - - - name: "Rename screenshots for flat upload" - run: | - find . -name '*.png' - mkdir -p ios-screenshots - for LOCALE_DIR in screenshots/*/; do - LOCALE=$(basename "${LOCALE_DIR}") - INDEX=0 - for IMG in "${LOCALE_DIR}"*.png "${LOCALE_DIR}"**/*.png; do - [ -f "${IMG}" ] || continue - PADDED=$(printf "%02d" ${INDEX}) - cp "${IMG}" "ios-screenshots/Screen-${PADDED}-iOS-${LOCALE}.png" - INDEX=$((INDEX + 1)) - done - done - # Also grab any screenshots from the Maestro output root - INDEX=0 - for IMG in screenshots/*.png; do - [ -f "${IMG}" ] || continue - PADDED=$(printf "%02d" ${INDEX}) - cp -v "${IMG}" "ios-screenshots/Screen-${PADDED}-iOS-default.png" - INDEX=$((INDEX + 1)) - done - - - name: "Shutdown simulator" - if: always() - run: xcrun simctl shutdown "${{ steps.simulator.outputs.device-id }}" 2>/dev/null || true - - - name: "Upload iOS screenshots" - uses: actions/upload-artifact@v6 - if: always() - with: - name: ios-screenshots - path: ios-screenshots/ - - # ─────────────────────────────────────────────────────────────────── - # 2b. Run Android app on emulator with Maestro tests - # ─────────────────────────────────────────────────────────────────── - run-android-app: - needs: build-app - runs-on: ubuntu-24.04 - timeout-minutes: 60 - steps: - - name: "Download build artifacts" - uses: actions/download-artifact@v6 - with: - name: skip-export - path: skip-export/ - - - name: "Download project sources" - uses: actions/download-artifact@v6 - with: - name: project-sources - - - name: "Enable KVM" - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: "Install Maestro" - run: | - curl -Ls "https://get.maestro.mobile.dev" | bash - echo "${HOME}/.maestro/bin" >> $GITHUB_PATH - - - name: "Ensure Maestro test flows exist" - run: | - if [ ! -d Android/fastlane/Maestro ] || [ -z "$(ls Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml 2>/dev/null)" ]; then - mkdir -p Android/fastlane/Maestro - #PACKAGE_NAME=$(grep "^ANDROID_PACKAGE_NAME" Skip.env | cut -d'=' -f2 | tr -d ' ') - PACKAGE_NAME=$(grep "^PRODUCT_BUNDLE_IDENTIFIER" Skip.env | cut -d'=' -f2 | tr -d ' ' | tr '-' '_') - cat > Android/fastlane/Maestro/launch-and-screenshot.yaml <> $GITHUB_OUTPUT - - - name: "Prepare Maestro script" - run: | - cat > maestro.sh <<'EOF' - export MAESTRO_CLI_NO_ANALYTICS=1 - - mkdir -p screenshots - - # Enable root access so setprop commands can modify system properties - adb root || true - adb wait-for-device - - # Install the APK - APK_PATH=$(find skip-export -name "*-release.apk" -o -name "*-debug.apk" | head -1) - if [ -z "${APK_PATH}" ]; then - APK_PATH=$(find skip-export -name "*.apk" | head -1) - fi - if [ -z "${APK_PATH}" ]; then - echo "No APK found in skip-export" - exit 1 - fi - adb install "${APK_PATH}" - - LOCALES="${{ steps.locales.outputs.locales }}" - - for LOCALE in ${LOCALES}; do - echo "::group::Running Maestro tests for locale ${LOCALE}" - - # Change emulator locale - LANG_CODE="${LOCALE%[-_]*}" - REGION_CODE="${LOCALE#*[-_]}" - - adb shell "setprop persist.sys.locale ${LANG_CODE}-${REGION_CODE}" || true - adb shell "setprop persist.sys.language ${LANG_CODE}" || true - adb shell "setprop persist.sys.country ${REGION_CODE}" || true - adb shell "settings put system system_locales ${LANG_CODE}-${REGION_CODE}" || true - - # Apply locale change - adb shell am broadcast -a android.intent.action.LOCALE_CHANGED || true - sleep 2 - - for FLOW in Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml; do - [ -f "${FLOW}" ] || continue - echo "Running flow: ${FLOW} (locale: ${LOCALE})" - maestro --platform android test "${FLOW}" --test-output-dir "screenshots/${LOCALE}" - done - - echo "::endgroup::" - done - EOF - - - name: "Install APK and run Maestro tests for each locale" - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 35 - arch: x86_64 - profile: pixel_7_pro - #profile: pixel_9 # not available? - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim - disable-animations: true - script: sh -ex maestro.sh - - - name: "Rename screenshots for flat upload" - run: | - find . -name '*.png' - mkdir -p android-screenshots - for LOCALE_DIR in screenshots/*/; do - [ -d "${LOCALE_DIR}" ] || continue - LOCALE=$(basename "${LOCALE_DIR}") - INDEX=0 - for IMG in "${LOCALE_DIR}"*.png "${LOCALE_DIR}"**/*.png; do - [ -f "${IMG}" ] || continue - PADDED=$(printf "%02d" ${INDEX}) - cp "${IMG}" "android-screenshots/Screen-${PADDED}-Android-${LOCALE}.png" - INDEX=$((INDEX + 1)) - done - done - INDEX=0 - for IMG in screenshots/*.png; do - [ -f "${IMG}" ] || continue - PADDED=$(printf "%02d" ${INDEX}) - cp -v "${IMG}" "android-screenshots/Screen-${PADDED}-Android-default.png" 2>/dev/null - INDEX=$((INDEX + 1)) - done - - - name: "Upload Android screenshots" - uses: actions/upload-artifact@v6 - if: always() - with: - name: android-screenshots - path: android-screenshots/ - - # ─────────────────────────────────────────────────────────────────── - # 3a. Sign and release iOS app - # ─────────────────────────────────────────────────────────────────── - release-ios-app: - needs: run-ios-app - if: startsWith(github.ref, 'refs/tags/') - runs-on: macos-26 - timeout-minutes: 60 - env: - DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer - steps: - - name: "Download build artifacts" - uses: actions/download-artifact@v6 - with: - name: skip-export - path: skip-export/ - - - name: "Download project sources" - uses: actions/download-artifact@v6 - with: - name: project-sources - - - name: "Download iOS screenshots" - uses: actions/download-artifact@v6 - with: - name: ios-screenshots - path: ios-screenshots/ - - - name: "Check secrets availability" - id: check-secrets + - name: "Setup Android App Signing" + if: ${{ env.KEYSTORE_PROPERTIES != '' }} env: - APPLE_CERTIFICATES_P12: ${{ secrets.APPLE_CERTIFICATES_P12 }} - APPLE_APPSTORE_APIKEY: ${{ secrets.APPLE_APPSTORE_APIKEY }} + KEYSTORE_JKS: ${{ secrets.KEYSTORE_JKS }} + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} run: | - echo "has-signing-cert=$(test -z "${APPLE_CERTIFICATES_P12}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT - echo "has-appstore-key=$(test -z "${APPLE_APPSTORE_APIKEY}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + echo -n "${KEYSTORE_JKS}" | base64 --decode > Android/app/keystore.jks + echo -n "${KEYSTORE_PROPERTIES}" | base64 --decode > Android/app/keystore.properties - name: "Setup Darwin App Signing" - id: signing - if: ${{ steps.check-secrets.outputs.has-signing-cert == 'true' }} uses: apple-actions/import-codesign-certs@v6 + if: ${{ env.APPLE_CERTIFICATES_P12 != '' }} + env: + APPLE_CERTIFICATES_P12: ${{ secrets.APPLE_CERTIFICATES_P12 }} with: p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12 }} p12-password: ${{ secrets.APPLE_CERTIFICATES_P12_PASSWORD }} - - name: "Sign iOS app" - id: sign - if: ${{ steps.check-secrets.outputs.has-signing-cert == 'true' }} - run: | - # Re-sign the exported .xcarchive or .ipa with the imported certificate - IPA_PATH=$(find skip-export -name "*.ipa" | head -1) - if [ -n "${IPA_PATH}" ]; then - echo "ipa-path=${IPA_PATH}" >> $GITHUB_OUTPUT - echo "signed=true" >> $GITHUB_OUTPUT - else - echo "No IPA found to sign" - echo "signed=false" >> $GITHUB_OUTPUT - fi + - name: "Android Fastlane" + if: ${{ ! startsWith(github.ref, 'refs/tags/') }} + working-directory: Android + run: test ! -d fastlane || fastlane assemble + + - name: "Darwin Fastlane" + if: ${{ ! startsWith(github.ref, 'refs/tags/') }} + working-directory: Darwin + run: test ! -d fastlane || FASTLANE_SKIP_ARCHIVE=YES FASTLANE_SKIP_CODESIGNING=YES FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=300 FASTLANE_XCODEBUILD_SETTINGS_RETRIES=5 fastlane assemble - name: "Submit iOS App to App Store" - if: ${{ steps.check-secrets.outputs.has-appstore-key == 'true' && steps.check-secrets.outputs.has-signing-cert == 'true' }} + if: ${{ startsWith(github.ref, 'refs/tags/') && env.APPLE_APPSTORE_APIKEY != '' }} working-directory: Darwin env: APPLE_APPSTORE_APIKEY: ${{ secrets.APPLE_APPSTORE_APIKEY }} run: | - echo -n "${APPLE_APPSTORE_APIKEY}" | base64 --decode -o fastlane/apikey.json - # Copy screenshots into fastlane metadata structure - if [ -d ../ios-screenshots ]; then - for IMG in ../ios-screenshots/Screen-*-iOS-*.png; do - [ -f "${IMG}" ] || continue - LOCALE=$(echo "$(basename "${IMG}")" | sed 's/Screen-[0-9]*-iOS-\(.*\)\.png/\1/') - mkdir -p "fastlane/metadata/${LOCALE}/screenshots" - cp "${IMG}" "fastlane/metadata/${LOCALE}/screenshots/" - done - fi + echo -n "${{ secrets.APPLE_APPSTORE_APIKEY }}" | base64 --decode -o fastlane/apikey.json fastlane release - - name: "Upload signed iOS artifacts" - uses: actions/upload-artifact@v6 - if: always() - with: - name: ios-signed-artifacts - path: skip-export/*.ipa - - # ─────────────────────────────────────────────────────────────────── - # 3b. Sign and release Android app - # ─────────────────────────────────────────────────────────────────── - release-android-app: - needs: run-android-app - if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-24.04 - timeout-minutes: 60 - steps: - - name: "Download build artifacts" - uses: actions/download-artifact@v6 - with: - name: skip-export - path: skip-export/ - - - name: "Download project sources" - uses: actions/download-artifact@v6 - with: - name: project-sources - - - name: "Download Android screenshots" - uses: actions/download-artifact@v6 - with: - name: android-screenshots - path: android-screenshots/ - - - name: "Check secrets availability" - id: check-secrets - env: - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} - GOOGLE_PLAY_APIKEY: ${{ secrets.GOOGLE_PLAY_APIKEY }} - run: | - echo "has-keystore=$(test -z "${KEYSTORE_PROPERTIES}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT - echo "has-play-key=$(test -z "${GOOGLE_PLAY_APIKEY}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT - - - name: "Sign Android APK" - id: sign - if: ${{ steps.check-secrets.outputs.has-keystore == 'true' }} - env: - KEYSTORE_JKS: ${{ secrets.KEYSTORE_JKS }} - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} - run: | - echo -n "${KEYSTORE_JKS}" | base64 --decode > Android/app/keystore.jks - echo -n "${KEYSTORE_PROPERTIES}" | base64 --decode > Android/app/keystore.properties - - # Sign the release APK with apksigner - APK_PATH=$(find skip-export -name "*-release-unsigned.apk" -o -name "*-release.apk" | head -1) - if [ -z "${APK_PATH}" ]; then - APK_PATH=$(find skip-export -name "*.apk" | head -1) - fi - - if [ -n "${APK_PATH}" ]; then - # Extract keystore properties - KEYSTORE_FILE="$(pwd)/Android/app/keystore.jks" - STORE_PASSWORD=$(grep 'storePassword' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') - KEY_ALIAS=$(grep 'keyAlias' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') - KEY_PASSWORD=$(grep 'keyPassword' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') - - SIGNED_APK="${APK_PATH%.apk}-signed.apk" - cp "${APK_PATH}" "${SIGNED_APK}" - - # Use apksigner from Android SDK - APKSIGNER=$(find ${ANDROID_HOME}/build-tools -name "apksigner" | sort -V | tail -1) - if [ -n "${APKSIGNER}" ]; then - ${APKSIGNER} sign \ - --ks "${KEYSTORE_FILE}" \ - --ks-pass "pass:${STORE_PASSWORD}" \ - --ks-key-alias "${KEY_ALIAS}" \ - --key-pass "pass:${KEY_PASSWORD}" \ - "${SIGNED_APK}" - fi - - echo "signed=true" >> $GITHUB_OUTPUT - else - echo "No APK found to sign" - echo "signed=false" >> $GITHUB_OUTPUT - fi - - name: "Submit Android App to Play Store" - if: ${{ steps.check-secrets.outputs.has-play-key == 'true' && steps.check-secrets.outputs.has-keystore == 'true' }} - working-directory: Android + if: ${{ startsWith(github.ref, 'refs/tags/') && env.GOOGLE_PLAY_APIKEY != '' }} env: GOOGLE_PLAY_APIKEY: ${{ secrets.GOOGLE_PLAY_APIKEY }} + working-directory: Android run: | echo -n "${GOOGLE_PLAY_APIKEY}" | base64 --decode > fastlane/apikey.json - # Copy screenshots into fastlane metadata structure - if [ -d ../android-screenshots ]; then - for IMG in ../android-screenshots/Screen-*-Android-*.png; do - [ -f "${IMG}" ] || continue - LOCALE=$(echo "$(basename "${IMG}")" | sed 's/Screen-[0-9]*-Android-\(.*\)\.png/\1/') - mkdir -p "fastlane/metadata/android/${LOCALE}/images/phoneScreenshots" - cp "${IMG}" "fastlane/metadata/android/${LOCALE}/images/phoneScreenshots/" - done - fi fastlane release - - name: "Upload signed Android artifacts" + - name: "Create GitHub Release" + if: startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ github.token }} + # Create a release or upload the assets if the release already exists + run: gh release create "${RELTAG}" -t "Release ${RELTAG}" --generate-notes skip-export/*.* || gh release upload "${RELTAG}" --clobber skip-export/*.* + + - name: "Upload Build Artifacts" uses: actions/upload-artifact@v6 if: always() with: - name: android-signed-artifacts - path: | - skip-export/*-signed.apk - skip-export/*.aab - - # ─────────────────────────────────────────────────────────────────── - # 4. Create GitHub Release with all artifacts and screenshots - # ─────────────────────────────────────────────────────────────────── - github-release: - needs: [release-ios-app, release-android-app] - if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-24.04 - timeout-minutes: 15 - permissions: - contents: write - steps: - - name: "Download build artifacts" - uses: actions/download-artifact@v6 - with: - name: skip-export path: skip-export/ - - name: "Download iOS screenshots" - uses: actions/download-artifact@v6 - with: - name: ios-screenshots - path: screenshots/ - continue-on-error: true - - - name: "Download Android screenshots" - uses: actions/download-artifact@v6 - with: - name: android-screenshots - path: screenshots/ - continue-on-error: true - - - name: "Download signed iOS artifacts" - uses: actions/download-artifact@v6 - with: - name: ios-signed-artifacts - path: signed-artifacts/ - continue-on-error: true - - - name: "Download signed Android artifacts" - uses: actions/download-artifact@v6 - with: - name: android-signed-artifacts - path: signed-artifacts/ - continue-on-error: true - - - name: "Prepare release assets" - run: | - mkdir -p release-assets - - # Copy build artifacts (skip directories, only files) - find skip-export -maxdepth 1 -type f -exec cp {} release-assets/ \; - - # Copy signed artifacts if they exist - if [ -d signed-artifacts ]; then - find signed-artifacts -type f -exec cp {} release-assets/ \; - fi - - # Copy screenshots with their flat names - if [ -d screenshots ]; then - find screenshots -name "*.png" -exec cp {} release-assets/ \; - fi - - echo "Release assets:" - ls -la release-assets/ - - - name: "Create GitHub Release" - env: - GH_TOKEN: ${{ github.token }} - RELTAG: ${{ needs.build-app.outputs.reltag }} - run: | - gh release create "${RELTAG}" \ - -t "Release ${RELTAG}" \ - --generate-notes \ - --repo "${GITHUB_REPOSITORY}" \ - release-assets/* \ - || gh release upload "${RELTAG}" \ - --clobber \ - --repo "${GITHUB_REPOSITORY}" \ - release-assets/* diff --git a/.github/workflows/skip-application.yml b/.github/workflows/skip-application.yml new file mode 100644 index 0000000..ed00599 --- /dev/null +++ b/.github/workflows/skip-application.yml @@ -0,0 +1,755 @@ +# This workflow is meant to be called remotely from a Skip app project. +# +# The action will build and test both the Swift and Gradle projects +# transpiled through Skip. +# +# When tagged with a semantic version (e.g., "1.2.3"), the action will +# create and distribute release .apk and .ipa artifacts from the project. +# +# Jobs: +# build-app – checkout, test, skip export, upload artifacts +# run-ios-app – install exported app on iOS simulator, run Maestro tests per locale +# run-android-app – install exported app on Android emulator, run Maestro tests per locale +# release-ios-app – sign and submit iOS app via Fastlane (tag only, secrets required) +# release-android-app – sign and submit Android app via Fastlane (tag only, secrets required) +# github-release – create GitHub release with all artifacts and screenshots +# +# An example invocation script is as follows, which runs for +# every push, every PR, every semver tag, and every day at noon GMT: +# +# name: skipapp +# on: +# push: +# branches: '*' +# tags: "[0-9]+.[0-9]+.[0-9]+" +# schedule: +# - cron: '0 12 * * *' +# workflow_dispatch: +# pull_request: +# +# permissions: +# contents: write +# +# jobs: +# call-workflow: +# uses: skiptools/actions/.github/workflows/skip-app.yml@v1 +# +name: "Skip Application CI" +on: + workflow_call: + inputs: + repository: + required: false + type: string + description: "Repository to checkout (owner/repo). Defaults to the calling repository." + brew-install: + required: false + type: string + run-local-tests: + required: false + type: boolean + default: true + secrets: + KEYSTORE_PROPERTIES: + required: false + KEYSTORE_JKS: + required: false + GOOGLE_PLAY_APIKEY: + required: false + APPLE_APPSTORE_APIKEY: + required: false + APPLE_CERTIFICATES_P12: + required: false + APPLE_CERTIFICATES_P12_PASSWORD: + required: false + APPLE_MOBILEPROVISION: + required: false + +jobs: + # ─────────────────────────────────────────────────────────────────── + # 1. Build, test, and export the app + # ─────────────────────────────────────────────────────────────────── + build-app: + runs-on: macos-26 + timeout-minutes: 180 + env: + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + outputs: + reltag: ${{ steps.setup.outputs.reltag }} + skip-module: ${{ steps.setup.outputs.skip-module }} + steps: + - uses: actions/checkout@v6 + with: + repository: ${{ inputs.repository || github.repository }} + submodules: 'recursive' + + - name: "Setup" + id: setup + run: | + TAG=${GITHUB_REF#refs/*/} + echo "reltag=${TAG:-'dev'}" >> $GITHUB_OUTPUT + + if [[ "${RUNNER_ARCH}" == 'ARM64' ]]; then + echo "HOMEBREW_PREFIX=/opt/homebrew" >> $GITHUB_ENV + else + echo "HOMEBREW_PREFIX=/usr/local" >> $GITHUB_ENV + fi + + echo "COMMIT_DATE=$(git log -1 --format=%ad --date=iso-strict ${TAG})" >> $GITHUB_ENV + + SKIP_MODULE=$(basename Darwin/*.xcodeproj .xcodeproj) + echo "skip-module=${SKIP_MODULE}" >> $GITHUB_OUTPUT + echo "SKIP_MODULE=${SKIP_MODULE}" >> $GITHUB_ENV + + sed -i '' "s;MARKETING_VERSION = .*;MARKETING_VERSION = $(git describe --tags --abbrev=0 --match '[0-9]*\.[0-9]*\.[0-9]*' --first-parent);g" Skip.env + sed -i '' "s;PRODUCT_VERSION = .*;PRODUCT_VERSION = $(git rev-list --count HEAD);g" Skip.env + + yq -e '.skip.mode' Sources/*/Skip/skip.yml >> /dev/null && echo "skip-fuse=true" >> $GITHUB_OUTPUT || echo "Native toolchain not needed" + + - uses: skiptools/actions/setup-skip@v1 + with: + verify-project: '.' + install-swift-android-sdk: ${{ steps.setup.outputs.skip-fuse }} + swift-android-sdk-version: nightly-6.3 + + - name: "Build Project" + #run: swift build + # not all projects build on macOS, so we need to build for iOS + run: xcrun swift build --triple arm64-apple-ios --sdk "$(xcrun --sdk iphoneos --show-sdk-path)" + + - name: "Test Project" + if: ${{ inputs.run-local-tests == true && ! startsWith(github.ref, 'refs/tags/') }} + run: test ! -d Tests || skip test + + # ideally we would skip the debug release, but if we don't do it, the debug keystore won't be automatically found and used + - name: "Export project (debug)" + run: | + skip export -v --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY + + - name: "Export project (release)" + if: startsWith(github.ref, 'refs/tags/') + run: | + # TODO: --ios-sim + skip export -v --ios --android --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY + + - name: "Build iOS Simulator app" + run: | + XCODEPROJ=$(ls -d Darwin/*.xcodeproj | head -1) + SCHEME="${SKIP_MODULE}" + DERIVED_DATA="$(pwd)/.build/DerivedData" + export SKIP_ZERO=1 + export SKIP_PLUGIN_DISABLED=1 + + xcodebuild build \ + -project "${XCODEPROJ}" \ + -scheme "${SCHEME}" \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "${DERIVED_DATA}" \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + ENABLE_CODE_SIGNING=NO \ + CODE_SIGNING_ALLOWED=NO + + # Find the .app bundle and zip it to preserve symlinks and bundle structure + # (actions/upload-artifact does not preserve symlinks) + APP_PATH=$(find "${DERIVED_DATA}" -path "*/Build/Products/Debug-iphonesimulator/*.app" -type d | head -1) + if [ -n "${APP_PATH}" ]; then + APP_NAME=$(basename "${APP_PATH}") + (cd "$(dirname "${APP_PATH}")" && zip -r -y "${OLDPWD}/skip-export/${APP_NAME}.zip" "${APP_NAME}") + echo "Simulator .app zipped: ${APP_NAME}.zip" + else + echo "::warning::No simulator .app bundle found in DerivedData" + fi + + - name: "Process Package.resolved" + run: | + cat Package.resolved + mkdir -p skip-export + cp -a Package.resolved skip-export/ + + - name: "Upload build artifacts" + uses: actions/upload-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Upload project sources" + uses: actions/upload-artifact@v6 + with: + name: project-sources + path: | + Darwin/ + Android/ + Skip.env + + # ─────────────────────────────────────────────────────────────────── + # 2a. Run iOS app on simulator with Maestro tests + # ─────────────────────────────────────────────────────────────────── + run-ios-app: + needs: build-app + runs-on: macos-26 + timeout-minutes: 60 + env: + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download project sources" + uses: actions/download-artifact@v6 + with: + name: project-sources + + - name: "Install Maestro" + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "${HOME}/.maestro/bin" >> $GITHUB_PATH + + - name: "Boot iOS Simulator" + id: simulator + run: | + # Create and boot a simulator + DEVICE_ID=$(xcrun simctl create "MaestroTest" "iPhone Air") + echo "device-id=${DEVICE_ID}" >> $GITHUB_OUTPUT + xcrun simctl boot "${DEVICE_ID}" + # Wait for the simulator to be ready + xcrun simctl bootstatus "${DEVICE_ID}" + + - name: "Install app on simulator" + run: | + # Unzip the .app bundle (zipped to preserve symlinks during artifact transfer) + APP_ZIP=$(find skip-export -name "*.app.zip" | head -1) + if [ -n "${APP_ZIP}" ]; then + unzip -o "${APP_ZIP}" -d skip-export/ + fi + + # Find the .app bundle from the export (debug build for testing) + APP_PATH=$(find skip-export -name "*.app" -type d | head -1) + if [ -z "${APP_PATH}" ]; then + echo "No .app bundle found in skip-export" + exit 1 + fi + xcrun simctl install "${{ steps.simulator.outputs.device-id }}" "${APP_PATH}" + + - name: "Ensure Maestro test flows exist" + run: | + if [ ! -d Darwin/fastlane/Maestro ] || [ -z "$(ls Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml 2>/dev/null)" ]; then + mkdir -p Darwin/fastlane/Maestro + BUNDLE_ID=$(grep "^PRODUCT_BUNDLE_IDENTIFIER" Skip.env | cut -d'=' -f2 | tr -d ' ') + cat > Darwin/fastlane/Maestro/launch-and-screenshot.yaml <> $GITHUB_OUTPUT + + - name: "Run Maestro tests for each locale" + env: + DEVICE_ID: ${{ steps.simulator.outputs.device-id }} + run: | + mkdir -p screenshots + export MAESTRO_CLI_NO_ANALYTICS=1 + LOCALES="${{ steps.locales.outputs.locales }}" + + for LOCALE in ${LOCALES}; do + echo "::group::Running Maestro tests for locale ${LOCALE}" + + # Change simulator locale and restart SpringBoard + # Convert locale format (e.g., en-US -> en_US for simctl) + LANG_CODE="${LOCALE%[-_]*}" + REGION_CODE="${LOCALE#*[-_]}" + xcrun simctl spawn "${DEVICE_ID}" defaults write -g AppleLanguages -array "${LANG_CODE}" + xcrun simctl spawn "${DEVICE_ID}" defaults write -g AppleLocale "${LANG_CODE}_${REGION_CODE}" + # Restart SpringBoard to apply locale change + xcrun simctl spawn "${DEVICE_ID}" launchctl stop com.apple.SpringBoard 2>/dev/null || true + sleep 3 + + # Run each Maestro flow + for FLOW in Darwin/fastlane/Maestro/*.yaml Darwin/fastlane/Maestro/*.yml; do + [ -f "${FLOW}" ] || continue + echo "Running flow: ${FLOW} (locale: ${LOCALE})" + maestro --platform ios --device "${DEVICE_ID}" test "${FLOW}" --test-output-dir "screenshots/${LOCALE}" + done + + echo "::endgroup::" + done + + - name: "Rename screenshots for flat upload" + run: | + find . -name '*.png' + mkdir -p ios-screenshots + for LOCALE_DIR in screenshots/*/; do + LOCALE=$(basename "${LOCALE_DIR}") + INDEX=0 + for IMG in "${LOCALE_DIR}"*.png "${LOCALE_DIR}"**/*.png; do + [ -f "${IMG}" ] || continue + PADDED=$(printf "%02d" ${INDEX}) + cp "${IMG}" "ios-screenshots/Screen-${PADDED}-iOS-${LOCALE}.png" + INDEX=$((INDEX + 1)) + done + done + # Also grab any screenshots from the Maestro output root + INDEX=0 + for IMG in screenshots/*.png; do + [ -f "${IMG}" ] || continue + PADDED=$(printf "%02d" ${INDEX}) + cp -v "${IMG}" "ios-screenshots/Screen-${PADDED}-iOS-default.png" + INDEX=$((INDEX + 1)) + done + + - name: "Shutdown simulator" + if: always() + run: xcrun simctl shutdown "${{ steps.simulator.outputs.device-id }}" 2>/dev/null || true + + - name: "Upload iOS screenshots" + uses: actions/upload-artifact@v6 + if: always() + with: + name: ios-screenshots + path: ios-screenshots/ + + # ─────────────────────────────────────────────────────────────────── + # 2b. Run Android app on emulator with Maestro tests + # ─────────────────────────────────────────────────────────────────── + run-android-app: + needs: build-app + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download project sources" + uses: actions/download-artifact@v6 + with: + name: project-sources + + - name: "Enable KVM" + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: "Install Maestro" + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "${HOME}/.maestro/bin" >> $GITHUB_PATH + + - name: "Ensure Maestro test flows exist" + run: | + if [ ! -d Android/fastlane/Maestro ] || [ -z "$(ls Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml 2>/dev/null)" ]; then + mkdir -p Android/fastlane/Maestro + #PACKAGE_NAME=$(grep "^ANDROID_PACKAGE_NAME" Skip.env | cut -d'=' -f2 | tr -d ' ') + PACKAGE_NAME=$(grep "^PRODUCT_BUNDLE_IDENTIFIER" Skip.env | cut -d'=' -f2 | tr -d ' ' | tr '-' '_') + cat > Android/fastlane/Maestro/launch-and-screenshot.yaml <> $GITHUB_OUTPUT + + - name: "Prepare Maestro script" + run: | + cat > maestro.sh <<'EOF' + export MAESTRO_CLI_NO_ANALYTICS=1 + + mkdir -p screenshots + + # Enable root access so setprop commands can modify system properties + adb root || true + adb wait-for-device + + # Install the APK + APK_PATH=$(find skip-export -name "*-release.apk" -o -name "*-debug.apk" | head -1) + if [ -z "${APK_PATH}" ]; then + APK_PATH=$(find skip-export -name "*.apk" | head -1) + fi + if [ -z "${APK_PATH}" ]; then + echo "No APK found in skip-export" + exit 1 + fi + adb install "${APK_PATH}" + + LOCALES="${{ steps.locales.outputs.locales }}" + + for LOCALE in ${LOCALES}; do + echo "::group::Running Maestro tests for locale ${LOCALE}" + + # Change emulator locale + LANG_CODE="${LOCALE%[-_]*}" + REGION_CODE="${LOCALE#*[-_]}" + + adb shell "setprop persist.sys.locale ${LANG_CODE}-${REGION_CODE}" || true + adb shell "setprop persist.sys.language ${LANG_CODE}" || true + adb shell "setprop persist.sys.country ${REGION_CODE}" || true + adb shell "settings put system system_locales ${LANG_CODE}-${REGION_CODE}" || true + + # Apply locale change + adb shell am broadcast -a android.intent.action.LOCALE_CHANGED || true + sleep 2 + + for FLOW in Android/fastlane/Maestro/*.yaml Android/fastlane/Maestro/*.yml; do + [ -f "${FLOW}" ] || continue + echo "Running flow: ${FLOW} (locale: ${LOCALE})" + maestro --platform android test "${FLOW}" --test-output-dir "screenshots/${LOCALE}" + done + + echo "::endgroup::" + done + EOF + + - name: "Install APK and run Maestro tests for each locale" + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 35 + arch: x86_64 + profile: pixel_7_pro + #profile: pixel_9 # not available? + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim + disable-animations: true + script: sh -ex maestro.sh + + - name: "Rename screenshots for flat upload" + run: | + find . -name '*.png' + mkdir -p android-screenshots + for LOCALE_DIR in screenshots/*/; do + [ -d "${LOCALE_DIR}" ] || continue + LOCALE=$(basename "${LOCALE_DIR}") + INDEX=0 + for IMG in "${LOCALE_DIR}"*.png "${LOCALE_DIR}"**/*.png; do + [ -f "${IMG}" ] || continue + PADDED=$(printf "%02d" ${INDEX}) + cp "${IMG}" "android-screenshots/Screen-${PADDED}-Android-${LOCALE}.png" + INDEX=$((INDEX + 1)) + done + done + INDEX=0 + for IMG in screenshots/*.png; do + [ -f "${IMG}" ] || continue + PADDED=$(printf "%02d" ${INDEX}) + cp -v "${IMG}" "android-screenshots/Screen-${PADDED}-Android-default.png" 2>/dev/null + INDEX=$((INDEX + 1)) + done + + - name: "Upload Android screenshots" + uses: actions/upload-artifact@v6 + if: always() + with: + name: android-screenshots + path: android-screenshots/ + + # ─────────────────────────────────────────────────────────────────── + # 3a. Sign and release iOS app + # ─────────────────────────────────────────────────────────────────── + release-ios-app: + needs: run-ios-app + if: startsWith(github.ref, 'refs/tags/') + runs-on: macos-26 + timeout-minutes: 60 + env: + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download project sources" + uses: actions/download-artifact@v6 + with: + name: project-sources + + - name: "Download iOS screenshots" + uses: actions/download-artifact@v6 + with: + name: ios-screenshots + path: ios-screenshots/ + + - name: "Check secrets availability" + id: check-secrets + env: + APPLE_CERTIFICATES_P12: ${{ secrets.APPLE_CERTIFICATES_P12 }} + APPLE_APPSTORE_APIKEY: ${{ secrets.APPLE_APPSTORE_APIKEY }} + run: | + echo "has-signing-cert=$(test -z "${APPLE_CERTIFICATES_P12}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + echo "has-appstore-key=$(test -z "${APPLE_APPSTORE_APIKEY}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + + - name: "Setup Darwin App Signing" + id: signing + if: ${{ steps.check-secrets.outputs.has-signing-cert == 'true' }} + uses: apple-actions/import-codesign-certs@v6 + with: + p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12 }} + p12-password: ${{ secrets.APPLE_CERTIFICATES_P12_PASSWORD }} + + - name: "Sign iOS app" + id: sign + if: ${{ steps.check-secrets.outputs.has-signing-cert == 'true' }} + run: | + # Re-sign the exported .xcarchive or .ipa with the imported certificate + IPA_PATH=$(find skip-export -name "*.ipa" | head -1) + if [ -n "${IPA_PATH}" ]; then + echo "ipa-path=${IPA_PATH}" >> $GITHUB_OUTPUT + echo "signed=true" >> $GITHUB_OUTPUT + else + echo "No IPA found to sign" + echo "signed=false" >> $GITHUB_OUTPUT + fi + + - name: "Submit iOS App to App Store" + if: ${{ steps.check-secrets.outputs.has-appstore-key == 'true' && steps.check-secrets.outputs.has-signing-cert == 'true' }} + working-directory: Darwin + env: + APPLE_APPSTORE_APIKEY: ${{ secrets.APPLE_APPSTORE_APIKEY }} + run: | + echo -n "${APPLE_APPSTORE_APIKEY}" | base64 --decode -o fastlane/apikey.json + # Copy screenshots into fastlane metadata structure + if [ -d ../ios-screenshots ]; then + for IMG in ../ios-screenshots/Screen-*-iOS-*.png; do + [ -f "${IMG}" ] || continue + LOCALE=$(echo "$(basename "${IMG}")" | sed 's/Screen-[0-9]*-iOS-\(.*\)\.png/\1/') + mkdir -p "fastlane/metadata/${LOCALE}/screenshots" + cp "${IMG}" "fastlane/metadata/${LOCALE}/screenshots/" + done + fi + fastlane release + + - name: "Upload signed iOS artifacts" + uses: actions/upload-artifact@v6 + if: always() + with: + name: ios-signed-artifacts + path: skip-export/*.ipa + + # ─────────────────────────────────────────────────────────────────── + # 3b. Sign and release Android app + # ─────────────────────────────────────────────────────────────────── + release-android-app: + needs: run-android-app + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download project sources" + uses: actions/download-artifact@v6 + with: + name: project-sources + + - name: "Download Android screenshots" + uses: actions/download-artifact@v6 + with: + name: android-screenshots + path: android-screenshots/ + + - name: "Check secrets availability" + id: check-secrets + env: + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + GOOGLE_PLAY_APIKEY: ${{ secrets.GOOGLE_PLAY_APIKEY }} + run: | + echo "has-keystore=$(test -z "${KEYSTORE_PROPERTIES}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + echo "has-play-key=$(test -z "${GOOGLE_PLAY_APIKEY}" && echo "false" || echo "true")" >> $GITHUB_OUTPUT + + - name: "Sign Android APK" + id: sign + if: ${{ steps.check-secrets.outputs.has-keystore == 'true' }} + env: + KEYSTORE_JKS: ${{ secrets.KEYSTORE_JKS }} + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + run: | + echo -n "${KEYSTORE_JKS}" | base64 --decode > Android/app/keystore.jks + echo -n "${KEYSTORE_PROPERTIES}" | base64 --decode > Android/app/keystore.properties + + # Sign the release APK with apksigner + APK_PATH=$(find skip-export -name "*-release-unsigned.apk" -o -name "*-release.apk" | head -1) + if [ -z "${APK_PATH}" ]; then + APK_PATH=$(find skip-export -name "*.apk" | head -1) + fi + + if [ -n "${APK_PATH}" ]; then + # Extract keystore properties + KEYSTORE_FILE="$(pwd)/Android/app/keystore.jks" + STORE_PASSWORD=$(grep 'storePassword' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') + KEY_ALIAS=$(grep 'keyAlias' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') + KEY_PASSWORD=$(grep 'keyPassword' Android/app/keystore.properties | cut -d'=' -f2 | tr -d '[:space:]') + + SIGNED_APK="${APK_PATH%.apk}-signed.apk" + cp "${APK_PATH}" "${SIGNED_APK}" + + # Use apksigner from Android SDK + APKSIGNER=$(find ${ANDROID_HOME}/build-tools -name "apksigner" | sort -V | tail -1) + if [ -n "${APKSIGNER}" ]; then + ${APKSIGNER} sign \ + --ks "${KEYSTORE_FILE}" \ + --ks-pass "pass:${STORE_PASSWORD}" \ + --ks-key-alias "${KEY_ALIAS}" \ + --key-pass "pass:${KEY_PASSWORD}" \ + "${SIGNED_APK}" + fi + + echo "signed=true" >> $GITHUB_OUTPUT + else + echo "No APK found to sign" + echo "signed=false" >> $GITHUB_OUTPUT + fi + + - name: "Submit Android App to Play Store" + if: ${{ steps.check-secrets.outputs.has-play-key == 'true' && steps.check-secrets.outputs.has-keystore == 'true' }} + working-directory: Android + env: + GOOGLE_PLAY_APIKEY: ${{ secrets.GOOGLE_PLAY_APIKEY }} + run: | + echo -n "${GOOGLE_PLAY_APIKEY}" | base64 --decode > fastlane/apikey.json + # Copy screenshots into fastlane metadata structure + if [ -d ../android-screenshots ]; then + for IMG in ../android-screenshots/Screen-*-Android-*.png; do + [ -f "${IMG}" ] || continue + LOCALE=$(echo "$(basename "${IMG}")" | sed 's/Screen-[0-9]*-Android-\(.*\)\.png/\1/') + mkdir -p "fastlane/metadata/android/${LOCALE}/images/phoneScreenshots" + cp "${IMG}" "fastlane/metadata/android/${LOCALE}/images/phoneScreenshots/" + done + fi + fastlane release + + - name: "Upload signed Android artifacts" + uses: actions/upload-artifact@v6 + if: always() + with: + name: android-signed-artifacts + path: | + skip-export/*-signed.apk + skip-export/*.aab + + # ─────────────────────────────────────────────────────────────────── + # 4. Create GitHub Release with all artifacts and screenshots + # ─────────────────────────────────────────────────────────────────── + github-release: + needs: [release-ios-app, release-android-app] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: "Download build artifacts" + uses: actions/download-artifact@v6 + with: + name: skip-export + path: skip-export/ + + - name: "Download iOS screenshots" + uses: actions/download-artifact@v6 + with: + name: ios-screenshots + path: screenshots/ + continue-on-error: true + + - name: "Download Android screenshots" + uses: actions/download-artifact@v6 + with: + name: android-screenshots + path: screenshots/ + continue-on-error: true + + - name: "Download signed iOS artifacts" + uses: actions/download-artifact@v6 + with: + name: ios-signed-artifacts + path: signed-artifacts/ + continue-on-error: true + + - name: "Download signed Android artifacts" + uses: actions/download-artifact@v6 + with: + name: android-signed-artifacts + path: signed-artifacts/ + continue-on-error: true + + - name: "Prepare release assets" + run: | + mkdir -p release-assets + + # Copy build artifacts (skip directories, only files) + find skip-export -maxdepth 1 -type f -exec cp {} release-assets/ \; + + # Copy signed artifacts if they exist + if [ -d signed-artifacts ]; then + find signed-artifacts -type f -exec cp {} release-assets/ \; + fi + + # Copy screenshots with their flat names + if [ -d screenshots ]; then + find screenshots -name "*.png" -exec cp {} release-assets/ \; + fi + + echo "Release assets:" + ls -la release-assets/ + + - name: "Create GitHub Release" + env: + GH_TOKEN: ${{ github.token }} + RELTAG: ${{ needs.build-app.outputs.reltag }} + run: | + gh release create "${RELTAG}" \ + -t "Release ${RELTAG}" \ + --generate-notes \ + --repo "${GITHUB_REPOSITORY}" \ + release-assets/* \ + || gh release upload "${RELTAG}" \ + --clobber \ + --repo "${GITHUB_REPOSITORY}" \ + release-assets/* From 8d7afbd7c3bc1b0c1eabe1a949459a97cb6c5503 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 20 Mar 2026 12:52:39 -0400 Subject: [PATCH 21/24] Add repository input to skip-app.yml --- .github/workflows/skip-app.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index af419d8..b410bc8 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -30,6 +30,10 @@ name: "Skip App CI" on: workflow_call: inputs: + repository: + required: false + type: string + description: "Repository to checkout (owner/repo). Defaults to the calling repository." brew-install: required: false type: string @@ -59,8 +63,8 @@ jobs: env: DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer steps: - - uses: actions/checkout@v6 with: + repository: ${{ inputs.repository || github.repository }} submodules: 'recursive' - name: "Setup" From e94634fc7f675cb2220d08066774ac9d0d6702cb Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 20 Mar 2026 12:53:37 -0400 Subject: [PATCH 22/24] Add repository input to skip-app.yml --- .github/workflows/skip-app.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/skip-app.yml b/.github/workflows/skip-app.yml index b410bc8..10fa643 100644 --- a/.github/workflows/skip-app.yml +++ b/.github/workflows/skip-app.yml @@ -63,6 +63,7 @@ jobs: env: DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer steps: + - uses: actions/checkout@v6 with: repository: ${{ inputs.repository || github.repository }} submodules: 'recursive' From 129cb17096175b659568f5586cbf4756c93bab50 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 20 Mar 2026 13:05:43 -0400 Subject: [PATCH 23/24] Add screenshots to summary output --- .github/workflows/skip-application.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/skip-application.yml b/.github/workflows/skip-application.yml index ed00599..172c4dd 100644 --- a/.github/workflows/skip-application.yml +++ b/.github/workflows/skip-application.yml @@ -321,6 +321,17 @@ jobs: INDEX=$((INDEX + 1)) done + - name: "Display iOS screenshots in summary" + if: always() + run: | + echo "### iOS Screenshots" >> $GITHUB_STEP_SUMMARY + for IMG in ios-screenshots/*.png; do + [ -f "${IMG}" ] || continue + IMG_BASE64=$(base64 -i "${IMG}") + echo "" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + done + - name: "Shutdown simulator" if: always() run: xcrun simctl shutdown "${{ steps.simulator.outputs.device-id }}" 2>/dev/null || true @@ -478,6 +489,17 @@ jobs: INDEX=$((INDEX + 1)) done + - name: "Display Android screenshots in summary" + if: always() + run: | + echo "### Android Screenshots" >> $GITHUB_STEP_SUMMARY + for IMG in android-screenshots/*.png; do + [ -f "${IMG}" ] || continue + IMG_BASE64=$(base64 -w 0 "${IMG}") + echo "" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + done + - name: "Upload Android screenshots" uses: actions/upload-artifact@v6 if: always() From 413e8184e7bf4693123c3c823db6edd9b2d38d19 Mon Sep 17 00:00:00 2001 From: Marc Prud'hommeaux Date: Fri, 20 Mar 2026 15:23:48 -0400 Subject: [PATCH 24/24] Use new --ios-sim export option to automatically export an iOS simulator build --- .github/workflows/skip-application.yml | 59 ++------------------------ 1 file changed, 3 insertions(+), 56 deletions(-) diff --git a/.github/workflows/skip-application.yml b/.github/workflows/skip-application.yml index 172c4dd..539bb1f 100644 --- a/.github/workflows/skip-application.yml +++ b/.github/workflows/skip-application.yml @@ -124,43 +124,12 @@ jobs: # ideally we would skip the debug release, but if we don't do it, the debug keystore won't be automatically found and used - name: "Export project (debug)" run: | - skip export -v --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY + skip export -v --ios --ios-sim --android --debug -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY - name: "Export project (release)" if: startsWith(github.ref, 'refs/tags/') run: | - # TODO: --ios-sim - skip export -v --ios --android --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY - - - name: "Build iOS Simulator app" - run: | - XCODEPROJ=$(ls -d Darwin/*.xcodeproj | head -1) - SCHEME="${SKIP_MODULE}" - DERIVED_DATA="$(pwd)/.build/DerivedData" - export SKIP_ZERO=1 - export SKIP_PLUGIN_DISABLED=1 - - xcodebuild build \ - -project "${XCODEPROJ}" \ - -scheme "${SCHEME}" \ - -configuration Debug \ - -destination 'generic/platform=iOS Simulator' \ - -derivedDataPath "${DERIVED_DATA}" \ - -skipPackagePluginValidation \ - -skipMacroValidation \ - ENABLE_CODE_SIGNING=NO \ - CODE_SIGNING_ALLOWED=NO - - # Find the .app bundle and zip it to preserve symlinks and bundle structure - # (actions/upload-artifact does not preserve symlinks) - APP_PATH=$(find "${DERIVED_DATA}" -path "*/Build/Products/Debug-iphonesimulator/*.app" -type d | head -1) - if [ -n "${APP_PATH}" ]; then - APP_NAME=$(basename "${APP_PATH}") - (cd "$(dirname "${APP_PATH}")" && zip -r -y "${OLDPWD}/skip-export/${APP_NAME}.zip" "${APP_NAME}") - echo "Simulator .app zipped: ${APP_NAME}.zip" - else - echo "::warning::No simulator .app bundle found in DerivedData" - fi + skip export -v --release -d skip-export --show-tree --summary-file=$GITHUB_STEP_SUMMARY - name: "Process Package.resolved" run: | @@ -222,7 +191,7 @@ jobs: - name: "Install app on simulator" run: | # Unzip the .app bundle (zipped to preserve symlinks during artifact transfer) - APP_ZIP=$(find skip-export -name "*.app.zip" | head -1) + APP_ZIP=$(find skip-export -name "*-Simulator-Debug.app.zip" | head -1) if [ -n "${APP_ZIP}" ]; then unzip -o "${APP_ZIP}" -d skip-export/ fi @@ -321,17 +290,6 @@ jobs: INDEX=$((INDEX + 1)) done - - name: "Display iOS screenshots in summary" - if: always() - run: | - echo "### iOS Screenshots" >> $GITHUB_STEP_SUMMARY - for IMG in ios-screenshots/*.png; do - [ -f "${IMG}" ] || continue - IMG_BASE64=$(base64 -i "${IMG}") - echo "" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - done - - name: "Shutdown simulator" if: always() run: xcrun simctl shutdown "${{ steps.simulator.outputs.device-id }}" 2>/dev/null || true @@ -489,17 +447,6 @@ jobs: INDEX=$((INDEX + 1)) done - - name: "Display Android screenshots in summary" - if: always() - run: | - echo "### Android Screenshots" >> $GITHUB_STEP_SUMMARY - for IMG in android-screenshots/*.png; do - [ -f "${IMG}" ] || continue - IMG_BASE64=$(base64 -w 0 "${IMG}") - echo "" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - done - - name: "Upload Android screenshots" uses: actions/upload-artifact@v6 if: always()