validate_secrets.yml 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. name: 1. Validate Secrets
  2. run-name: Validate Secrets (${{ github.ref_name }})
  3. on: [workflow_call, workflow_dispatch]
  4. jobs:
  5. validate-access-token:
  6. name: Access
  7. runs-on: macos-14
  8. env:
  9. GH_PAT: ${{ secrets.GH_PAT }}
  10. GH_TOKEN: ${{ secrets.GH_PAT }}
  11. outputs:
  12. HAS_WORKFLOW_PERMISSION: ${{ steps.access-token.outputs.has_workflow_permission }}
  13. steps:
  14. - name: Validate Access Token
  15. id: access-token
  16. run: |
  17. # Validate Access Token
  18. # Ensure that gh exit codes are handled when output is piped.
  19. set -o pipefail
  20. # Define patterns to validate the access token (GH_PAT) and distinguish between classic and fine-grained tokens.
  21. GH_PAT_CLASSIC_PATTERN='^ghp_[a-zA-Z0-9]{36}$'
  22. GH_PAT_FINE_GRAINED_PATTERN='^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$'
  23. # Validate Access Token (GH_PAT)
  24. if [ -z "$GH_PAT" ]; then
  25. failed=true
  26. echo "::error::The GH_PAT secret is unset or empty. Set it and try again."
  27. else
  28. if [[ $GH_PAT =~ $GH_PAT_CLASSIC_PATTERN ]]; then
  29. provides_scopes=true
  30. echo "The GH_PAT secret is a structurally valid classic token."
  31. elif [[ $GH_PAT =~ $GH_PAT_FINE_GRAINED_PATTERN ]]; then
  32. echo "The GH_PAT secret is a structurally valid fine-grained token."
  33. else
  34. unknown_format=true
  35. echo "The GH_PAT secret does not have a known token format."
  36. fi
  37. # Attempt to capture the x-oauth-scopes scopes of the token.
  38. 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
  39. failed=true
  40. if [ $unknown_format ]; then
  41. 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."
  42. else
  43. 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."
  44. fi
  45. elif [[ $scopes =~ workflow ]]; then
  46. echo "The GH_PAT secret has repo and workflow permissions."
  47. echo "has_workflow_permission=true" >> $GITHUB_OUTPUT
  48. elif [[ $scopes =~ repo ]]; then
  49. echo "The GH_PAT secret has repo (but not workflow) permissions."
  50. elif [ $provides_scopes ]; then
  51. failed=true
  52. if [ -z "$scopes" ]; then
  53. echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide any permission scopes."
  54. else
  55. echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it only provides the following permission scopes: $scopes"
  56. fi
  57. 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."
  58. else
  59. 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."
  60. echo "has_workflow_permission=true" >> $GITHUB_OUTPUT
  61. fi
  62. fi
  63. # Exit unsuccessfully if secret validation failed.
  64. if [ $failed ]; then
  65. exit 2
  66. fi
  67. validate-match-secrets:
  68. name: Match-Secrets
  69. needs: validate-access-token
  70. runs-on: macos-14
  71. env:
  72. GH_TOKEN: ${{ secrets.GH_PAT }}
  73. steps:
  74. - name: Validate Match-Secrets
  75. run: |
  76. # Validate Match-Secrets
  77. # Ensure that gh exit codes are handled when output is piped.
  78. set -o pipefail
  79. # If a Match-Secrets repository does not exist, attempt to create one.
  80. if ! visibility=$(gh repo view ${{ github.repository_owner }}/Match-Secrets --json visibility | jq --raw-output '.visibility | ascii_downcase'); then
  81. echo "A '${{ github.repository_owner }}/Match-Secrets' repository could not be found using the GH_PAT secret. Attempting to create one..."
  82. # Create a private Match-Secrets repository and verify that it exists and that it is private.
  83. 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
  84. echo "Created a private '${{ github.repository_owner }}/Match-Secrets' repository."
  85. else
  86. failed=true
  87. 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."
  88. fi
  89. # Otherwise, if a Match-Secrets repository exists, but it is public, cause validation to fail.
  90. elif [[ "$visibility" == "public" ]]; then
  91. failed=true
  92. 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."
  93. else
  94. echo "Found a private '${{ github.repository_owner }}/Match-Secrets' repository to use."
  95. fi
  96. # Exit unsuccessfully if secret validation failed.
  97. if [ $failed ]; then
  98. exit 2
  99. fi
  100. validate-fastlane-secrets:
  101. name: Fastlane
  102. needs: [validate-access-token, validate-match-secrets]
  103. runs-on: macos-14
  104. env:
  105. GH_PAT: ${{ secrets.GH_PAT }}
  106. GH_TOKEN: ${{ secrets.GH_PAT }}
  107. FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
  108. FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
  109. FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
  110. MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
  111. TEAMID: ${{ secrets.TEAMID }}
  112. steps:
  113. - name: Checkout Repo
  114. uses: actions/checkout@v4
  115. - name: Install Project Dependencies
  116. run: bundle install
  117. # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
  118. - name: Sync clock
  119. run: sudo sntp -sS time.windows.com
  120. - name: Validate Fastlane Secrets
  121. run: |
  122. # Validate Fastlane Secrets
  123. # Validate TEAMID
  124. if [ -z "$TEAMID" ]; then
  125. failed=true
  126. echo "::error::The TEAMID secret is unset or empty. Set it and try again."
  127. elif [ ${#TEAMID} -ne 10 ]; then
  128. failed=true
  129. echo "::error::The TEAMID secret is set but has wrong length. Verify that it is set correctly and try again."
  130. elif ! [[ $TEAMID =~ ^[A-Z0-9]+$ ]]; then
  131. failed=true
  132. echo "::error::The TEAMID secret is set but invalid. Verify that it is set correctly (only uppercase letters and numbers) and try again."
  133. fi
  134. # Validate MATCH_PASSWORD
  135. if [ -z "$MATCH_PASSWORD" ]; then
  136. failed=true
  137. echo "::error::The MATCH_PASSWORD secret is unset or empty. Set it and try again."
  138. fi
  139. # Ensure that fastlane exit codes are handled when output is piped.
  140. set -o pipefail
  141. # Validate FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY
  142. FASTLANE_KEY_ID_PATTERN='^[A-Z0-9]+$'
  143. 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}\}?$'
  144. if [ -z "$FASTLANE_ISSUER_ID" ] || [ -z "$FASTLANE_KEY_ID" ] || [ -z "$FASTLANE_KEY" ]; then
  145. failed=true
  146. [ -z "$FASTLANE_ISSUER_ID" ] && echo "::error::The FASTLANE_ISSUER_ID secret is unset or empty. Set it and try again."
  147. [ -z "$FASTLANE_KEY_ID" ] && echo "::error::The FASTLANE_KEY_ID secret is unset or empty. Set it and try again."
  148. [ -z "$FASTLANE_KEY" ] && echo "::error::The FASTLANE_KEY secret is unset or empty. Set it and try again."
  149. elif [ ${#FASTLANE_KEY_ID} -ne 10 ]; then
  150. failed=true
  151. 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/integrations/api and try again."
  152. elif ! [[ $FASTLANE_KEY_ID =~ $FASTLANE_KEY_ID_PATTERN ]]; then
  153. failed=true
  154. 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/integrations/api and try again."
  155. elif ! [[ $FASTLANE_ISSUER_ID =~ $FASTLANE_ISSUER_ID_PATTERN ]]; then
  156. failed=true
  157. 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/integrations/api and try again."
  158. elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then
  159. failed=true
  160. 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."
  161. elif ! (bundle exec fastlane validate_secrets 2>&1 || true) | tee fastlane.log; then # ignore "fastlane validate_secrets" errors and continue on errors without annotating an exit code
  162. if grep -q "bad decrypt" fastlane.log; then
  163. failed=true
  164. echo "::error::Unable to decrypt the Match-Secrets repository using the MATCH_PASSWORD secret. Verify that it is set correctly and try again."
  165. elif grep -q -e "required agreement" -e "license agreement" fastlane.log; then
  166. failed=true
  167. echo "::error::❗️ 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 take effect and try again."
  168. elif grep -q "Your certificate .* is not valid" fastlane.log; then
  169. echo "::notice::Your Distribution certificate is invalid or expired. Automated renewal of the certificate will be attempted."
  170. fi
  171. fi
  172. # Exit unsuccessfully if secret validation failed.
  173. if [ $failed ]; then
  174. exit 2
  175. fi