name: 4. Build LoopFollow run-name: Build LoopFollow (${{ github.ref_name }}) on: workflow_dispatch: schedule: # Check for updates every Sunday # Later logic builds if there are updates or if it is the 2nd Sunday of the month - cron: "17 10 * * 0" # Sunday at UTC 10:17 env: GH_PAT: ${{ secrets.GH_PAT }} UPSTREAM_REPO: loopandlearn/LoopFollow UPSTREAM_BRANCH: ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed) TARGET_BRANCH: ${{ github.ref_name }} # target branch on fork to be kept in sync jobs: # use a single runner for these sequential steps check_status: runs-on: ubuntu-latest name: Check status to decide whether to build permissions: contents: write outputs: NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }} IS_SECOND_IN_MONTH: ${{ steps.date-check.outputs.is_second_instance }} # Check GH_PAT, sync repository, check day in month steps: - name: Access id: workflow-permission run: | # Validate Access Token # Ensure that gh exit codes are handled when output is piped. set -o pipefail # Define patterns to validate the access token (GH_PAT) and distinguish between classic and fine-grained tokens. GH_PAT_CLASSIC_PATTERN='^ghp_[a-zA-Z0-9]{36}$' GH_PAT_FINE_GRAINED_PATTERN='^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$' # Validate Access Token (GH_PAT) if [ -z "$GH_PAT" ]; then failed=true echo "::error::The GH_PAT secret is unset or empty. Set it and try again." else if [[ $GH_PAT =~ $GH_PAT_CLASSIC_PATTERN ]]; then provides_scopes=true echo "The GH_PAT secret is a structurally valid classic token." elif [[ $GH_PAT =~ $GH_PAT_FINE_GRAINED_PATTERN ]]; then echo "The GH_PAT secret is a structurally valid fine-grained token." else unknown_format=true echo "The GH_PAT secret does not have a known token format." fi # Attempt to capture the x-oauth-scopes scopes of the token. if ! scopes=$(curl -sS -f -I -H "Authorization: token $GH_PAT" https://api.github.com | { grep -i '^x-oauth-scopes:' || true; } | cut -d ' ' -f2- | tr -d '\r'); then failed=true if [ $unknown_format ]; then echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that it is set correctly (including the 'ghp_' or 'github_pat_' prefix) and try again." else echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that the token exists and has not expired at https://github.com/settings/tokens. If necessary, regenerate or create a new token (and update the secret), then try again." fi elif [[ $scopes =~ workflow ]]; then echo "The GH_PAT secret has repo and workflow permissions." echo "has_permission=true" >> $GITHUB_OUTPUT elif [[ $scopes =~ repo ]]; then echo "The GH_PAT secret has repo (but not workflow) permissions." elif [ $provides_scopes ]; then failed=true if [ -z "$scopes" ]; then echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide any permission scopes." else echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it only provides the following permission scopes: $scopes" fi echo "::error::The GH_PAT secret is lacking at least the 'repo' permission scope required to access the Match-Secrets repository. Update the token permissions at https://github.com/settings/tokens (to include the 'repo' and 'workflow' scopes) and try again." else echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide inspectable scopes. Assuming that the 'repo' and 'workflow' permission scopes required to access the Match-Secrets repository and perform automations are present." echo "has_permission=true" >> $GITHUB_OUTPUT fi fi # Exit unsuccessfully if secret validation failed. if [ $failed ]; then exit 2 fi - name: Checkout target repo if: | steps.workflow-permission.outputs.has_permission == 'true' && (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false') uses: actions/checkout@v5 with: token: ${{ secrets.GH_PAT }} # This syncs any target branch to upstream branch of the same name - name: Sync upstream changes if: | # do not run the upstream sync action on the upstream repository steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'loopandlearn' id: sync uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.2 with: target_sync_branch: ${{ env.TARGET_BRANCH }} shallow_since: 6 months ago target_repo_token: ${{ secrets.GH_PAT }} upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }} upstream_sync_repo: ${{ env.UPSTREAM_REPO }} # Display a sample message based on the sync output var 'has_new_commits' - name: New commits found if: | steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true' run: echo "New commits were found to sync." - name: No new commits if: | steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false' run: echo "There were no new commits." - name: Show value of 'has_new_commits' if: steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false' run: | echo ${{ steps.sync.outputs.has_new_commits }} echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT - name: Show scheduled build configuration message if: steps.workflow-permission.outputs.has_permission != 'true' run: | echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY echo "You have not yet configured the scheduled sync and build for ${{ github.event.repository.name }}'s browser build." >> $GITHUB_STEP_SUMMARY echo "Synchronizing your fork of ${{ github.event.repository.name }} with the upstream repository ${{ env.UPSTREAM_REPO }} will be skipped." >> $GITHUB_STEP_SUMMARY echo "If you want to enable automatic builds and updates for your ${{ github.event.repository.name }}, please follow the instructions \ under the following path ${{ github.event.repository.name }}/fastlane/testflight.md." >> $GITHUB_STEP_SUMMARY # Set a logic flag if this is the second instance of this day-of-week in this month - name: Check if this is the second time this day-of-week happens this month id: date-check run: | DAY_OF_MONTH=$(date +%-d) WEEK_OF_MONTH=$(( ($(date +%-d) - 1) / 7 + 1 )) if [[ $WEEK_OF_MONTH -eq 2 ]]; then echo "is_second_instance=true" >> "$GITHUB_OUTPUT" else echo "is_second_instance=false" >> "$GITHUB_OUTPUT" fi # When a scheduled run finds nothing new to build, cancel the run so it shows # as "cancelled" instead of "success" — a green check should always mean a # new build was made. cancel_when_no_build: needs: check_status name: Cancel run when there is nothing to build runs-on: ubuntu-latest permissions: actions: write if: | github.event_name == 'schedule' && !(vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') && !(vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true') steps: - name: Cancel the run env: GH_TOKEN: ${{ github.token }} run: | echo "Repository is up to date and no monthly build is due — cancelling this run so it is not reported as a successful build." >> "$GITHUB_STEP_SUMMARY" gh run cancel ${{ github.run_id }} --repo ${{ github.repository }} # Keep the job alive until the cancellation lands, so the run ends as # "cancelled" rather than completing successfully first. sleep 300 # Checks if Distribution certificate is present and valid, optionally nukes and # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true' # only run if a build is planned check_certs: needs: [check_status] name: Check certificates uses: ./.github/workflows/create_certs.yml secrets: inherit if: | github.event_name == 'workflow_dispatch' || (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') || (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' ) # Builds the app build: name: Build needs: [check_certs, check_status] runs-on: macos-26 permissions: contents: write if: | # builds with manual start; if scheduled: once a month or when new commits are found github.event_name == 'workflow_dispatch' || (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') || (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' ) steps: - name: Select Xcode version run: "sudo xcode-select --switch /Applications/Xcode_26.2.app/Contents/Developer" - name: Checkout Repo for building uses: actions/checkout@v5 with: token: ${{ secrets.GH_PAT }} submodules: recursive ref: ${{ env.TARGET_BRANCH }} # Patch Fastlane Match to not print tables - name: Patch Match Tables run: | TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb if [ -f "$TABLE_PRINTER_PATH" ]; then sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH" else echo "table_printer.rb not found" exit 1 fi # Install project dependencies - name: Install Project Dependencies run: bundle install # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996) - name: Sync clock run: sudo sntp -sS time.windows.com # Build signed IPA file - name: Fastlane Build & Archive run: bundle exec fastlane build_LoopFollow env: TEAMID: ${{ secrets.TEAMID }} GH_PAT: ${{ secrets.GH_PAT }} FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} # Upload to TestFlight - name: Fastlane upload to TestFlight run: bundle exec fastlane release env: TEAMID: ${{ secrets.TEAMID }} GH_PAT: ${{ secrets.GH_PAT }} FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} # Upload Build artifacts - name: Upload build log, IPA and Symbol artifacts if: always() uses: actions/upload-artifact@v6 with: name: build-artifacts path: | artifacts buildlog