ソースを参照

Fastlane/GH build improvements: Add sync action and keepalive action, align with Loop dev improvements (#46)

* Add update.yml. Runs nightly to check for new commits.

If new commits are found, the target branch on forks is synced with upstream, and build_iAPS.yml is launched. Updates will hence be made available in TestFlight within less than a day. Users will have to decide if they want to enable Automatic Updates in the  TestFlight app or not.

Scheduled workflows in GitHub actions will be cancelled in the event of 60 days of no repository activity. A "keep alive" action will generate empty commits on upstream if there is no activity after 50 days to avoid inactivation of the scheduled workflow. This commit will be picked up by update.yml on forks the next day, so that there is repository activity also on forks.

The reason for running the keepalive action on upstream only, is that upstream and forks should then be on the same commit ref. This would not always be the case if the keepalive action ran on the forks instead. But then the scheduled workflow must be enabled in the upstream repo (Artificial-Pancreas/iAPS).

Changes to build_iAPS.yml:
- Added "on: workflow_call" to allow it to be launched by update.yml
- Removed the "secrets" job", this can still be run from the identifiers and certificates workflows to validate repository secrets.
- Removed checkout option "submodules: recursive"

* Schedule build of iAPS at 04:30 UTC on the 1st  every month

The workflow will not run on schedule on forks until the workflow is activated. It can later easily be inactivated/re-activated without code changes to cancel scheduled runs by selecting "Disable workflow" / "Enable workflow" in Actions.

* build_iAPS: add missing env: TARGET_BRANCH

* update.yml and build_iAPS.yml: Use current branch name as TARGET_BRANCH and UPSTREAM_BRANCH

* Run the keepalive action on all repositories

This helps forks not to rely on the upstream repo to keep scheduled workflows running

* validate_secrets.yml: Migrate validation improvements from Loop

* create_certs.yml: Add branch name to workflow name, change job name

* add_identifiers.yml: Add branch name to workflow name, change job name

* build_iAPS.yml: Add validation as required job, migrate @bjornoleh build_IAPS.yml

* update.yml: Add validation as required job, migrate @bjornoleh update.yml

* Fastfile: Add matching to secrets validation lane to verify MATCH_PASSWORD

* Add update.yml. Runs nightly to check for new commits.

If new commits are found, the target branch on forks is synced with upstream, and build_iAPS.yml is launched. Updates will hence be made available in TestFlight within less than a day. Users will have to decide if they want to enable Automatic Updates in the  TestFlight app or not.

Scheduled workflows in GitHub actions will be cancelled in the event of 60 days of no repository activity. A "keep alive" action will generate empty commits on upstream if there is no activity after 50 days to avoid inactivation of the scheduled workflow. This commit will be picked up by update.yml on forks the next day, so that there is repository activity also on forks.

The reason for running the keepalive action on upstream only, is that upstream and forks should then be on the same commit ref. This would not always be the case if the keepalive action ran on the forks instead. But then the scheduled workflow must be enabled in the upstream repo (Artificial-Pancreas/iAPS).

Changes to build_iAPS.yml:
- Added "on: workflow_call" to allow it to be launched by update.yml
- Removed the "secrets" job", this can still be run from the identifiers and certificates workflows to validate repository secrets.
- Removed checkout option "submodules: recursive"

* Schedule build of iAPS at 04:30 UTC on the 1st  every month

The workflow will not run on schedule on forks until the workflow is activated. It can later easily be inactivated/re-activated without code changes to cancel scheduled runs by selecting "Disable workflow" / "Enable workflow" in Actions.

* build_iAPS: add missing env: TARGET_BRANCH

* update.yml and build_iAPS.yml: Use current branch name as TARGET_BRANCH and UPSTREAM_BRANCH

* Run the keepalive action on all repositories

This helps forks not to rely on the upstream repo to keep scheduled workflows running

* Harmonise workflows with LoopKit/LoopWorkspace

* Update Fastlane to 2.215.0

Among other improvements, this should fix the WWDR issue.

Commands used to install bundler locally and update dependencies:

sudo gem pristine ffi
sudo gem install bundler
sudo bundle install
sudo bundle update fastlane

* Automatically sync fork before building if vars.SCHEDULED_SYNC == 'true'

Automated sync of fork before building requires setting a SCHEDULED_SYNC repository variable to 'true'

Build job:
change condition from vars.SCHEDULED_SYNC != 'false' to vars.SCHEDULED_SYNC == 'true'

for the build job and following steps:

- name: Checkout Repo for syncing
- name: Sync upstream changes
- name: New commits found
- name: No new commits
- name: Show value of 'has_new_commits'

---------

Co-authored-by: Deniz Cengiz <48965855+dnzxy@users.noreply.github.com>
bjornoleh 2 年 前
コミット
1d00044597

+ 9 - 4
.github/workflows/add_identifiers.yml

@@ -1,15 +1,16 @@
 name: 2. Add Identifiers
-run-name: Add Identifiers
+run-name: Add Identifiers (${{ github.ref_name }})
 on:
   workflow_dispatch:
 
 jobs:
-  secrets:
+  validate:
+    name: Validate
     uses: ./.github/workflows/validate_secrets.yml
     secrets: inherit
 
   identifiers:
-    needs: secrets
+    needs: validate
     runs-on: macos-13
     steps:
       # Uncomment to manually select Xcode version if needed
@@ -24,9 +25,13 @@ jobs:
       - name: Patch Match Tables
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
         
+      # Install project dependencies
+      - name: Install Project Dependencies
+        run: bundle install
+
       # Create or update identifiers for app
       - name: Fastlane Provision
-        run: fastlane identifiers
+        run: bundle exec fastlane identifiers
         env:
           TEAMID: ${{ secrets.TEAMID }}
           GH_PAT: ${{ secrets.GH_PAT }}

+ 206 - 11
.github/workflows/build_iAPS.yml

@@ -6,37 +6,232 @@ on:
   ## Remove the "#" sign from the beginning of the line below to get automated builds on push (code changes in your repository)
   #push:
   
-  ## Remove the "#" sign from the beginning of the two lines below to get automated builds every two months
-  #schedule:
-    #- cron: '0 17 1 */2 *' # Runs at 17:00 UTC on the 1st in Jan, Mar, May, Jul, Sep and Nov.
+  schedule:
+    #- cron: '30 04 1 * *' # Runs at 04:30 UTC on the 1st every month
+    - cron: '0 8 * * 3' # Checks for updates at 08:00 UTC every Wednesday
+    - cron: '0 6 1 * *' # Builds the app on the 1st of every month at 06:00 UTC
 
+env:  
+  UPSTREAM_REPO: Artificial-Pancreas/iAPS
+  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, and target branch on upstream to be kept alive (replace with specific branch name if needed)
+  ALIVE_BRANCH: alive
 
 jobs:
-  secrets:
+  validate:
+    name: Validate
     uses: ./.github/workflows/validate_secrets.yml
     secrets: inherit
 
+  # Checks if GH_PAT holds workflow permissions
+  # Checks for existence of alive branch; if non-existent creates it
+  check_alive_and_permissions:
+    needs: validate
+    runs-on: ubuntu-latest
+    name: Check alive branch and permissions
+    permissions:
+      contents: write
+    outputs:
+      WORKFLOW_PERMISSION: ${{ steps.workflow-permission.outputs.has_permission }}
+    
+    steps:
+    - name: Check for workflow permissions
+      id: workflow-permission
+      env: 
+        TOKEN_TO_CHECK: ${{ secrets.GH_PAT }}
+      run: |
+        PERMISSIONS=$(curl -sS -f -I -H "Authorization: token ${{ env.TOKEN_TO_CHECK }}" https://api.github.com | grep ^x-oauth-scopes: | cut -d' ' -f2-);
+        
+        if [[ $PERMISSIONS =~ "workflow" || $PERMISSIONS == "" ]]; then
+          echo "GH_PAT holds workflow permissions or is fine-grained PAT."
+          echo "has_permission=true" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false.
+        else 
+          echo "GH_PAT lacks workflow permissions."
+          echo "Automated build features will be skipped!"
+          echo "has_permission=false" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false.
+        fi
+    
+    - name: Check for alive branch
+      if: steps.workflow-permission.outputs.has_permission == 'true'
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      run: |
+        if [[ "$(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository_owner }}/iAPS/branches | jq --raw-output 'any(.name=="alive")')" == "true" ]]; then
+          echo "Branch 'alive' exists."
+          echo "ALIVE_BRANCH_EXISTS=true" >> $GITHUB_ENV # Set ALIVE_BRANCH_EXISTS to true
+        else
+          echo "Branch 'alive' does not exist."
+          echo "ALIVE_BRANCH_EXISTS=false" >> $GITHUB_ENV # Set ALIVE_BRANCH_EXISTS to false
+        fi
+    
+    - name: Create alive branch
+      if: env.ALIVE_BRANCH_EXISTS == 'false'
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      run: |
+        # Get ref for Artificial-Pancreas/iAPS:dev
+        SHA=$(curl -sS https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs \
+          | jq '.[] | select(.ref == "refs/heads/dev" ) | .object.sha' \
+          | tr -d '"'
+        );
+        
+        # Create alive branch based on Artificial-Pancreas/iAPS:dev
+        gh api \
+          --method POST \
+          -H "Authorization: token $GITHUB_TOKEN" \
+          -H "Accept: application/vnd.github.v3+json" \
+          /repos/${{ github.repository_owner }}/iAPS/git/refs \
+          -f ref='refs/heads/alive' \
+          -f sha=$SHA
+  
+  # Checks for changes in upstream repository; if changes exist prompts sync for build
+  # Performs keepalive to avoid stale fork
+  check_latest_from_upstream:
+    needs: [validate, check_alive_and_permissions]
+    runs-on: ubuntu-latest
+    name: Check upstream and keep alive
+    outputs: 
+      NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }}
+    
+    steps:
+    - name: Checkout target repo
+      if: |
+        needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+        (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
+      uses: actions/checkout@v3
+      with:
+        token: ${{ secrets.GH_PAT }}
+        ref: alive
+    
+    - name: Sync upstream changes
+      if: | # do not run the upstream sync action on the upstream repository
+        needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+        vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'Artificial-Pancreas'
+      id: sync
+      uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
+      with:
+        target_sync_branch: ${{ env.ALIVE_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: |
+        needs.check_alive_and_permissions.outputs.WORKFLOW_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: |
+        needs.check_alive_and_permissions.outputs.WORKFLOW_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: needs.check_alive_and_permissions.outputs.WORKFLOW_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
+    
+    # Keep repository "alive": add empty commits to ALIVE_BRANCH after "time_elapsed" days of inactivity to avoid inactivation of scheduled workflows
+    - name: Keep alive
+      if: |
+        needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+        (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
+      uses: gautamkrishnar/keepalive-workflow@v1 # using the workflow with default settings
+      with:
+        time_elapsed: 20 # Time elapsed from the previous commit to trigger a new automated commit (in days)
+    
+    - name: Show scheduled build configuration message
+      if: needs.check_alive_and_permissions.outputs.WORKFLOW_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 iAPS's browser build." >> $GITHUB_STEP_SUMMARY
+        echo "Synchronizing your fork of <code>iAPS</code> with the upstream repository <code>Artificial-Pancreas/iAPS</code> will be skipped." >> $GITHUB_STEP_SUMMARY
+        echo "If you want to enable automatic builds and updates for your iAPS, please follow the instructions \
+              under the following path <code>iAPS/fastlane/testflight.md</code>." >> $GITHUB_STEP_SUMMARY
+   
+  
+  # Builds iAPS
   build:
-    needs: secrets
+    name: Build
+    needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
     runs-on: macos-13
+    permissions:
+      contents: write
+    if: | # runs if started manually, or if sync schedule is set and enabled and scheduled on the first Saturday each month, or if sync schedule is set and enabled and new commits were found
+        github.event_name == 'workflow_dispatch' ||
+        (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '0 6 1 * *') ||
+          (vars.SCHEDULED_SYNC == 'true' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
+        )
     steps:
-      # Uncomment to manually select Xcode version if needed
       - name: Select Xcode version
         run: "sudo xcode-select --switch /Applications/Xcode_15.0.1.app/Contents/Developer"
+      
+      - name: Checkout Repo for syncing
+        if: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          vars.SCHEDULED_SYNC == 'true'
+        uses: actions/checkout@v3
+        with:
+          token: ${{ secrets.GH_PAT }}
+          ref: ${{ env.TARGET_BRANCH }} 
+      
+      - name: Sync upstream changes
+        if: | # do not run the upstream sync action on the upstream repository
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          vars.SCHEDULED_SYNC == 'true' && github.repository_owner != 'Artificial-Pancreas'
+        id: sync
+        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
+        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: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
+          vars.SCHEDULED_SYNC == 'true' && steps.sync.outputs.has_new_commits == 'true'
+        run: echo "New commits were found to sync."
+    
+      - name: No new commits
+        if: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 
+          vars.SCHEDULED_SYNC == 'true' && steps.sync.outputs.has_new_commits == 'false'
+        run: echo "There were no new commits."
+      
+      - name: Show value of 'has_new_commits'
+        if: |
+          needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true'
+          && vars.SCHEDULED_SYNC == 'true'
+        run: |
+          echo ${{ steps.sync.outputs.has_new_commits }}
+          echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
 
-      # Checks-out the repo
-      - name: Checkout Repo
+      - name: Checkout Repo for building
         uses: actions/checkout@v3
         with:
+          token: ${{ secrets.GH_PAT }}
           submodules: recursive
-      
+          ref: ${{ env.TARGET_BRANCH }}
+
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
       
+      # Install project dependencies
+      - name: Install project dependencies
+        run: bundle install
+      
       # Build signed iAPS IPA file
       - name: Fastlane Build & Archive
-        run: fastlane build_iAPS
+        run: bundle exec fastlane build_iAPS        
         env:
           TEAMID: ${{ secrets.TEAMID }}
           GH_PAT: ${{ secrets.GH_PAT }}
@@ -47,7 +242,7 @@ jobs:
       
       # Upload to TestFlight
       - name: Fastlane upload to TestFlight
-        run: fastlane release
+        run: bundle exec fastlane release
         env:
           TEAMID: ${{ secrets.TEAMID }}
           GH_PAT: ${{ secrets.GH_PAT }}

+ 12 - 6
.github/workflows/create_certs.yml

@@ -1,15 +1,17 @@
 name: 3. Create Certificates
-run-name: Create Certificates
+run-name: Create Certificates (${{ github.ref_name }})
 on:
   workflow_dispatch:
 
 jobs:
-  secrets:
+  validate:
+    name: Validate
     uses: ./.github/workflows/validate_secrets.yml
     secrets: inherit
-
+  
   certificates:
-    needs: secrets
+    name: Create Certificates
+    needs: validate
     runs-on: macos-13
     steps:
       # Uncomment to manually select Xcode version if needed
@@ -23,10 +25,14 @@ jobs:
       # Patch Fastlane Match to not print tables
       - name: Patch Match Tables
         run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
-        
+
+      # Install project dependencies
+      - name: Install Project Dependencies
+        run: bundle install
+
       # Create or update certificates for app
       - name: Create Certificates
-        run: fastlane certs
+        run: bundle exec fastlane certs
         env:
           TEAMID: ${{ secrets.TEAMID }}
           GH_PAT: ${{ secrets.GH_PAT }}

+ 162 - 38
.github/workflows/validate_secrets.yml

@@ -1,70 +1,194 @@
 name: 1. Validate Secrets
-run-name: Validate Secrets
+run-name: Validate Secrets (${{ github.ref_name }})
 on: [workflow_call, workflow_dispatch]
 
 jobs:
-  validate:
+  validate-access-token:
+    name: Access
     runs-on: macos-13
+    env:
+      GH_PAT: ${{ secrets.GH_PAT }}
+      GH_TOKEN: ${{ secrets.GH_PAT }}
+    outputs:
+      HAS_WORKFLOW_PERMISSION: ${{ steps.access-token.outputs.has_workflow_permission }}
+    steps:
+      - name: Validate Access Token
+        id: access-token
+        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_workflow_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_workflow_permission=true" >> $GITHUB_OUTPUT
+            fi
+          fi
+          
+          # Exit unsuccessfully if secret validation failed.
+          if [ $failed ]; then
+            exit 2
+          fi
+  
+  validate-match-secrets:
+    name: Match-Secrets
+    needs: validate-access-token
+    runs-on: macos-13
+    env:
+      GH_TOKEN: ${{ secrets.GH_PAT }}
+    steps:
+      - name: Validate Match-Secrets
+        run: |
+          # Validate Match-Secrets
+          
+          # Ensure that gh exit codes are handled when output is piped.
+          set -o pipefail
+          
+          # If a Match-Secrets repository does not exist, attempt to create one.
+          if ! visibility=$(gh repo view ${{ github.repository_owner }}/Match-Secrets --json visibility | jq --raw-output '.visibility | ascii_downcase'); then
+            echo "A '${{ github.repository_owner }}/Match-Secrets' repository could not be found using the GH_PAT secret. Attempting to create one..."
+            
+            # Create a private Match-Secrets repository and verify that it exists and that it is private.
+            if gh repo create ${{ github.repository_owner }}/Match-Secrets --private >/dev/null && [ "$(gh repo view ${{ github.repository_owner }}/Match-Secrets --json visibility | jq --raw-output '.visibility | ascii_downcase')" == "private" ]; then
+              echo "Created a private '${{ github.repository_owner }}/Match-Secrets' repository."
+            else
+              failed=true
+              echo "::error::Unable to create a private '${{ github.repository_owner }}/Match-Secrets' repository. Create a private 'Match-Secrets' repository manually and try again. If a private 'Match-Secrets' repository already exists, verify that the token permissions of the GH_PAT are set correctly (or update them) at https://github.com/settings/tokens and try again."
+            fi
+          # Otherwise, if a Match-Secrets repository exists, but it is public, cause validation to fail.
+          elif [[ "$visibility" == "public" ]]; then
+            failed=true
+            echo "::error::A '${{ github.repository_owner }}/Match-Secrets' repository was found, but it is public. Change the repository visibility to private (or delete it) and try again. If necessary, a private repository will be created for you."
+          else
+            echo "Found a private '${{ github.repository_owner }}/Match-Secrets' repository to use."
+          fi
+          
+          # Exit unsuccessfully if secret validation failed.
+          if [ $failed ]; then
+            exit 2
+          fi
+  
+  validate-fastlane-secrets:
+    name: Fastlane
+    needs: [validate-access-token, validate-match-secrets]
+    runs-on: macos-13
+    env:
+      GH_PAT: ${{ secrets.GH_PAT }}
+      GH_TOKEN: ${{ secrets.GH_PAT }}
+      FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
+      FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
+      FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
+      MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
+      TEAMID: ${{ secrets.TEAMID }}
     steps:
-      # Checks-out the repo
       - name: Checkout Repo
         uses: actions/checkout@v3
 
-      # Validates the repo secrets
-      - name: Validate Secrets
-        run: |
-          # Validate Secrets
-          echo Validating Repository Secrets...
+      # Install project dependencies
+      - name: Install Project Dependencies
+        run: bundle install
 
+      - name: Validate Fastlane Secrets
+        run: |
+          # Validate Fastlane Secrets
+          
           # Validate TEAMID
           if [ -z "$TEAMID" ]; then
             failed=true
-            echo "::error::TEAMID secret is unset or empty. Set it and try again."
+            echo "::error::The TEAMID secret is unset or empty. Set it and try again."
           elif [ ${#TEAMID} -ne 10 ]; then
             failed=true
-            echo "::error::TEAMID secret is set but has wrong length. Verify that it is set correctly and try again."
-          fi
-
-          # Validate GH_PAT
-          if [ -z "$GH_PAT" ]; then
+            echo "::error::The TEAMID secret is set but has wrong length. Verify that it is set correctly and try again."
+          elif ! [[ $TEAMID =~ ^[A-Z0-9]+$ ]]; then
             failed=true
-            echo "::error::GH_PAT secret is unset or empty. Set it and try again."
-          elif [ "$(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository_owner }}/Match-Secrets | jq --raw-output '.permissions.push')" != "true" ]; then
+            echo "::error::The TEAMID secret is set but invalid. Verify that it is set correctly (only uppercase letters and numbers) and try again."
+          fi
+          
+          # Validate MATCH_PASSWORD
+          if [ -z "$MATCH_PASSWORD" ]; then
             failed=true
-            echo "::error::GH_PAT secret is set but invalid or lacking appropriate privileges on the ${{ github.repository_owner }}/Match-Secrets repository. Verify that it is set correctly and try again."
+            echo "::error::The MATCH_PASSWORD secret is unset or empty. Set it and try again."
           fi
-
+          
+          # Ensure that fastlane exit codes are handled when output is piped.
+          set -o pipefail
+          
           # Validate FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY
+          FASTLANE_KEY_ID_PATTERN='^[A-Z0-9]+$'
+          FASTLANE_ISSUER_ID_PATTERN='^\{?[A-F0-9a-f]{8}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{12}\}?$'
+          
           if [ -z "$FASTLANE_ISSUER_ID" ] || [ -z "$FASTLANE_KEY_ID" ] || [ -z "$FASTLANE_KEY" ]; then
             failed=true
             [ -z "$FASTLANE_ISSUER_ID" ] && echo "::error::The FASTLANE_ISSUER_ID secret is unset or empty. Set it and try again."
             [ -z "$FASTLANE_KEY_ID"    ] && echo "::error::The FASTLANE_KEY_ID secret is unset or empty. Set it and try again."
             [ -z "$FASTLANE_KEY"       ] && echo "::error::The FASTLANE_KEY secret is unset or empty. Set it and try again."
-          elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then
+          elif [ ${#FASTLANE_KEY_ID} -ne 10 ]; then
             failed=true
-            echo "::error::The FASTLANE_KEY secret is set but invalid. Verify that it is set correctly and try again."
-          elif ! fastlane validate_secrets; then
+            echo "::error::The FASTLANE_KEY_ID secret is set but has wrong length. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/api and try again."
+          elif ! [[ $FASTLANE_KEY_ID =~ $FASTLANE_KEY_ID_PATTERN ]]; then
             failed=true
-            echo "::error::Unable to create a valid authorization token for the App Store Connect API.\
-            Verify that the FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY secrets are set correctly and try again."
-          fi
-
-          # Validate MATCH_PASSWORD
-          if [ -z "$MATCH_PASSWORD" ]; then
+            echo "::error::The FASTLANE_KEY_ID secret is set but invalid. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/api and try again."
+          elif ! [[ $FASTLANE_ISSUER_ID =~ $FASTLANE_ISSUER_ID_PATTERN ]]; then
             failed=true
-            echo "::error::The MATCH_PASSWORD secret is unset or empty. Set it and try again."
+            echo "::error::The FASTLANE_ISSUER_ID secret is set but invalid. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/api and try again."
+          elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then
+            failed=true
+            echo "::error::The FASTLANE_KEY secret is set but invalid. Verify that you copied it correctly from the API Key file (*.p8) you downloaded and try again."
+          elif ! bundle exec fastlane validate_secrets 2>&1 | tee fastlane.log; then
+            if grep -q "bad decrypt" fastlane.log; then
+              failed=true
+              echo "::error::Unable to decrypt the Match-Secrets repository using the MATCH_PASSWORD secret. Verify that it is set correctly and try again."
+            elif grep -q -e "required agreement" -e "license agreement" fastlane.log; then
+              failed=true
+              echo "::error::Unable to create a valid authorization token for the App Store Connect API. Verify that the latest developer program license agreement has been accepted at https://developer.apple.com/account (review and accept any updated agreement), then wait a few minutes for changes to propagate and try again."
+            elif ! grep -q -e "No code signing identity found" -e "Could not install WWDR certificate" fastlane.log; then
+              failed=true
+              echo "::error::Unable to create a valid authorization token for the App Store Connect API. Verify that the FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY secrets are set correctly and try again."
+            fi
           fi
-
+          
           # Exit unsuccessfully if secret validation failed.
           if [ $failed ]; then
             exit 2
           fi
-        shell: bash
-        env:
-          TEAMID: ${{ secrets.TEAMID }}
-          GH_PAT: ${{ secrets.GH_PAT }}
-          FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
-          FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
-          FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
-          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
-          GH_TOKEN: ${{ secrets.GH_PAT }}

+ 72 - 68
Gemfile.lock

@@ -1,52 +1,53 @@
 GEM
   remote: https://rubygems.org/
   specs:
-    CFPropertyList (3.0.4)
+    CFPropertyList (3.0.6)
       rexml
-    addressable (2.8.0)
-      public_suffix (>= 2.0.2, < 5.0)
+    addressable (2.8.5)
+      public_suffix (>= 2.0.2, < 6.0)
     artifactory (3.0.15)
     atomos (0.1.3)
     aws-eventstream (1.2.0)
-    aws-partitions (1.516.0)
-    aws-sdk-core (3.121.2)
+    aws-partitions (1.824.0)
+    aws-sdk-core (3.181.1)
       aws-eventstream (~> 1, >= 1.0.2)
-      aws-partitions (~> 1, >= 1.239.0)
+      aws-partitions (~> 1, >= 1.651.0)
+      aws-sigv4 (~> 1.5)
+      jmespath (~> 1, >= 1.6.1)
+    aws-sdk-kms (1.71.0)
+      aws-sdk-core (~> 3, >= 3.177.0)
       aws-sigv4 (~> 1.1)
-      jmespath (~> 1.0)
-    aws-sdk-kms (1.50.0)
-      aws-sdk-core (~> 3, >= 3.121.2)
-      aws-sigv4 (~> 1.1)
-    aws-sdk-s3 (1.104.0)
-      aws-sdk-core (~> 3, >= 3.121.2)
+    aws-sdk-s3 (1.134.0)
+      aws-sdk-core (~> 3, >= 3.181.0)
       aws-sdk-kms (~> 1)
-      aws-sigv4 (~> 1.4)
-    aws-sigv4 (1.4.0)
+      aws-sigv4 (~> 1.6)
+    aws-sigv4 (1.6.0)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
-    claide (1.0.3)
+    claide (1.1.0)
     colored (1.2)
     colored2 (3.1.2)
     commander (4.6.0)
       highline (~> 2.0.0)
     declarative (0.0.20)
-    digest-crc (0.6.4)
+    digest-crc (0.6.5)
       rake (>= 12.0.0, < 14.0.0)
     domain_name (0.5.20190701)
       unf (>= 0.0.5, < 1.0.0)
-    dotenv (2.7.6)
+    dotenv (2.8.1)
     emoji_regex (3.2.3)
-    excon (0.87.0)
-    faraday (1.8.0)
+    excon (0.103.0)
+    faraday (1.10.3)
       faraday-em_http (~> 1.0)
       faraday-em_synchrony (~> 1.0)
       faraday-excon (~> 1.1)
-      faraday-httpclient (~> 1.0.1)
+      faraday-httpclient (~> 1.0)
+      faraday-multipart (~> 1.0)
       faraday-net_http (~> 1.0)
-      faraday-net_http_persistent (~> 1.1)
+      faraday-net_http_persistent (~> 1.0)
       faraday-patron (~> 1.0)
       faraday-rack (~> 1.0)
-      multipart-post (>= 1.2, < 3)
+      faraday-retry (~> 1.0)
       ruby2_keywords (>= 0.0.4)
     faraday-cookie_jar (0.0.7)
       faraday (>= 0.8.0)
@@ -55,14 +56,17 @@ GEM
     faraday-em_synchrony (1.0.0)
     faraday-excon (1.1.0)
     faraday-httpclient (1.0.1)
+    faraday-multipart (1.0.4)
+      multipart-post (~> 2)
     faraday-net_http (1.0.1)
     faraday-net_http_persistent (1.2.0)
     faraday-patron (1.0.0)
     faraday-rack (1.0.0)
+    faraday-retry (1.0.3)
     faraday_middleware (1.2.0)
       faraday (~> 1.0)
-    fastimage (2.2.5)
-    fastlane (2.196.0)
+    fastimage (2.2.7)
+    fastlane (2.215.0)
       CFPropertyList (>= 2.3, < 4.0.0)
       addressable (>= 2.8, < 3.0.0)
       artifactory (~> 3.0)
@@ -83,10 +87,11 @@ GEM
       google-apis-playcustomapp_v1 (~> 0.1)
       google-cloud-storage (~> 1.31)
       highline (~> 2.0)
+      http-cookie (~> 1.0.5)
       json (< 3.0.0)
       jwt (>= 2.1.0, < 3)
       mini_magick (>= 4.9.4, < 5.0.0)
-      multipart-post (~> 2.0.0)
+      multipart-post (>= 2.0.0, < 3.0.0)
       naturally (~> 2.2)
       optparse (~> 0.1.1)
       plist (>= 3.1.0, < 4.0.0)
@@ -94,7 +99,7 @@ GEM
       security (= 0.1.3)
       simctl (~> 1.6.3)
       terminal-notifier (>= 2.0.0, < 3.0.0)
-      terminal-table (>= 1.4.5, < 2.0.0)
+      terminal-table (~> 3)
       tty-screen (>= 0.6.3, < 1.0.0)
       tty-spinner (>= 0.8.0, < 1.0.0)
       word_wrap (~> 1.0.0)
@@ -102,9 +107,9 @@ GEM
       xcpretty (~> 0.3.0)
       xcpretty-travis-formatter (>= 0.0.3)
     gh_inspector (1.1.3)
-    google-apis-androidpublisher_v3 (0.12.0)
-      google-apis-core (>= 0.4, < 2.a)
-    google-apis-core (0.4.1)
+    google-apis-androidpublisher_v3 (0.49.0)
+      google-apis-core (>= 0.11.0, < 2.a)
+    google-apis-core (0.11.1)
       addressable (~> 2.5, >= 2.5.1)
       googleauth (>= 0.16.2, < 2.a)
       httpclient (>= 2.8.1, < 3.a)
@@ -113,74 +118,72 @@ GEM
       retriable (>= 2.0, < 4.a)
       rexml
       webrick
-    google-apis-iamcredentials_v1 (0.7.0)
-      google-apis-core (>= 0.4, < 2.a)
-    google-apis-playcustomapp_v1 (0.5.0)
-      google-apis-core (>= 0.4, < 2.a)
-    google-apis-storage_v1 (0.8.0)
-      google-apis-core (>= 0.4, < 2.a)
+    google-apis-iamcredentials_v1 (0.17.0)
+      google-apis-core (>= 0.11.0, < 2.a)
+    google-apis-playcustomapp_v1 (0.13.0)
+      google-apis-core (>= 0.11.0, < 2.a)
+    google-apis-storage_v1 (0.19.0)
+      google-apis-core (>= 0.9.0, < 2.a)
     google-cloud-core (1.6.0)
       google-cloud-env (~> 1.0)
       google-cloud-errors (~> 1.0)
-    google-cloud-env (1.5.0)
-      faraday (>= 0.17.3, < 2.0)
-    google-cloud-errors (1.2.0)
-    google-cloud-storage (1.34.1)
-      addressable (~> 2.5)
+    google-cloud-env (1.6.0)
+      faraday (>= 0.17.3, < 3.0)
+    google-cloud-errors (1.3.1)
+    google-cloud-storage (1.44.0)
+      addressable (~> 2.8)
       digest-crc (~> 0.4)
       google-apis-iamcredentials_v1 (~> 0.1)
-      google-apis-storage_v1 (~> 0.1)
+      google-apis-storage_v1 (~> 0.19.0)
       google-cloud-core (~> 1.6)
       googleauth (>= 0.16.2, < 2.a)
       mini_mime (~> 1.0)
-    googleauth (1.0.0)
-      faraday (>= 0.17.3, < 2.0)
+    googleauth (1.8.0)
+      faraday (>= 0.17.3, < 3.a)
       jwt (>= 1.4, < 3.0)
-      memoist (~> 0.16)
       multi_json (~> 1.11)
       os (>= 0.9, < 2.0)
       signet (>= 0.16, < 2.a)
     highline (2.0.3)
-    http-cookie (1.0.4)
+    http-cookie (1.0.5)
       domain_name (~> 0.5)
     httpclient (2.8.3)
-    jmespath (1.4.0)
-    json (2.6.0)
-    jwt (2.3.0)
-    memoist (0.16.2)
-    mini_magick (4.11.0)
-    mini_mime (1.1.2)
+    jmespath (1.6.2)
+    json (2.6.3)
+    jwt (2.7.1)
+    mini_magick (4.12.0)
+    mini_mime (1.1.5)
     multi_json (1.15.0)
-    multipart-post (2.0.0)
+    multipart-post (2.3.0)
     nanaimo (0.3.0)
     naturally (2.2.1)
     optparse (0.1.1)
-    os (1.1.1)
-    plist (3.6.0)
-    public_suffix (4.0.6)
+    os (1.1.4)
+    plist (3.7.0)
+    public_suffix (5.0.3)
     rake (13.0.6)
-    representable (3.1.1)
+    representable (3.2.0)
       declarative (< 0.1.0)
       trailblazer-option (>= 0.1.1, < 0.2.0)
       uber (< 0.2.0)
     retriable (3.1.2)
-    rexml (3.2.5)
+    rexml (3.2.6)
     rouge (2.0.7)
     ruby2_keywords (0.0.5)
     rubyzip (2.3.2)
     security (0.1.3)
-    signet (0.16.0)
+    signet (0.18.0)
       addressable (~> 2.8)
-      faraday (>= 0.17.3, < 2.0)
+      faraday (>= 0.17.5, < 3.a)
       jwt (>= 1.5, < 3.0)
       multi_json (~> 1.10)
-    simctl (1.6.8)
+    simctl (1.6.10)
       CFPropertyList
       naturally
     terminal-notifier (2.0.0)
-    terminal-table (1.8.0)
-      unicode-display_width (~> 1.1, >= 1.1.1)
-    trailblazer-option (0.1.1)
+    terminal-table (3.0.2)
+      unicode-display_width (>= 1.1.1, < 3)
+    trailblazer-option (0.1.2)
     tty-cursor (0.7.1)
     tty-screen (0.8.1)
     tty-spinner (0.9.3)
@@ -188,11 +191,11 @@ GEM
     uber (0.1.0)
     unf (0.1.4)
       unf_ext
-    unf_ext (0.0.8)
-    unicode-display_width (1.8.0)
-    webrick (1.7.0)
+    unf_ext (0.0.8.2)
+    unicode-display_width (2.4.2)
+    webrick (1.8.1)
     word_wrap (1.0.0)
-    xcodeproj (1.21.0)
+    xcodeproj (1.22.0)
       CFPropertyList (>= 2.3.3, < 4.0)
       atomos (~> 0.1.3)
       claide (>= 1.0.2, < 2.0)
@@ -206,10 +209,11 @@ GEM
 
 PLATFORMS
   arm64-darwin-21
+  arm64-darwin-22
   x86_64-darwin-19
 
 DEPENDENCIES
   fastlane
 
 BUNDLED WITH
-   2.3.26
+   2.4.19

+ 6 - 0
fastlane/Fastfile

@@ -204,6 +204,12 @@ platform :ios do
     end
 
     find_bundle_id("ru.artpancreas.#{TEAMID}.FreeAPS")
+
+    match(
+      type: "appstore",
+      git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"),
+      app_identifier: [],
+    )
   end
 
   desc "Nuke Certs"