Skip to content

Commit a9e4404

Browse files
authored
GH Actions: Improve secrets validation (#86)
* validate_secrets.yml: Pass a "Could not install WWDR certificate" error through validation * validate_secrets.yml: Improve annotation when a public Match-Secrets repo exists * validate_secrets.yml: Rewrite Match-Secrets validation to be explicit about the Match-Secrets repository that will be used When the GH account that the GH_PAT token was created under does not match the repository_owner of the LoopWorkspace repository, the validation routine used a different Match-Secrets repository than fastlane. * validate_secrets.yml: Rewrite GH_PAT validation to capture scopes and distinguish between classic and fine-grained access tokens * validate_secrets.yml: Fix syntax error in Match-Secrets validation job * validate_secrets.yml: Depend less on patterns / read scopes from any token that provides them * Provide HAS_WORKFLOW_PERMISSION as an output * validate_secrets.yml: Annotate failures from unaccepted Apple PLAs * validate_secrets.yml: Fix typo and improve annotation when GH_PAT is invalid * validate_secrets.yml: Improve annotation when authorization fails and token format is unknown * validate_secrets.yml: Minor wording tweak
1 parent 59d7e50 commit a9e4404

File tree

1 file changed

+71
-17
lines changed

1 file changed

+71
-17
lines changed

.github/workflows/validate_secrets.yml

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,61 @@ jobs:
99
env:
1010
GH_PAT: ${{ secrets.GH_PAT }}
1111
GH_TOKEN: ${{ secrets.GH_PAT }}
12+
outputs:
13+
HAS_WORKFLOW_PERMISSION: ${{ steps.access-token.outputs.has_workflow_permission }}
1214
steps:
1315
- name: Validate Access Token
16+
id: access-token
1417
run: |
15-
# Validate Fastlane Access Token (GH_PAT)
18+
# Validate Access Token
19+
20+
# Ensure that gh exit codes are handled when output is piped.
21+
set -o pipefail
22+
23+
# Define patterns to validate the access token (GH_PAT) and distinguish between classic and fine-grained tokens.
24+
GH_PAT_CLASSIC_PATTERN='^ghp_[a-zA-Z0-9]{36}$'
25+
GH_PAT_FINE_GRAINED_PATTERN='^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$'
26+
27+
# Validate Access Token (GH_PAT)
1628
if [ -z "$GH_PAT" ]; then
1729
failed=true
1830
echo "::error::The GH_PAT secret is unset or empty. Set it and try again."
19-
elif [ "$(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository_owner }}/LoopWorkspace | jq --raw-output '.permissions.push')" != "true" ]; then
20-
failed=true
21-
echo "::error::The GH_PAT secret is set but invalid or lacking at least 'repo' permission scope ('repo, workflow' is okay too).\
22-
Verify that token permissions are set correctly (or update them) at https://github.com/settings/tokens and try again."
31+
else
32+
if [[ $GH_PAT =~ $GH_PAT_CLASSIC_PATTERN ]]; then
33+
provides_scopes=true
34+
echo "The GH_PAT secret is a structurally valid classic token."
35+
elif [[ $GH_PAT =~ $GH_PAT_FINE_GRAINED_PATTERN ]]; then
36+
echo "The GH_PAT secret is a structurally valid fine-grained token."
37+
else
38+
unknown_format=true
39+
echo "The GH_PAT secret does not have a known token format."
40+
fi
41+
42+
# Attempt to capture the x-oauth-scopes scopes of the token.
43+
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
44+
failed=true
45+
if [ $unknown_format ]; then
46+
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."
47+
else
48+
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."
49+
fi
50+
elif [[ $scopes =~ workflow ]]; then
51+
echo "The GH_PAT secret has repo and workflow permissions."
52+
echo "has_workflow_permission=true" >> $GITHUB_OUTPUT
53+
elif [[ $scopes =~ repo ]]; then
54+
echo "The GH_PAT secret has repo (but not workflow) permissions."
55+
elif [ $provides_scopes ]; then
56+
failed=true
57+
if [ -z "$scopes" ]; then
58+
echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide any permission scopes."
59+
else
60+
echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it only provides the following permission scopes: $scopes"
61+
fi
62+
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."
63+
else
64+
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."
65+
echo "has_workflow_permission=true" >> $GITHUB_OUTPUT
66+
fi
2367
fi
2468
2569
# Exit unsuccessfully if secret validation failed.
@@ -37,19 +81,27 @@ jobs:
3781
- name: Validate Match-Secrets
3882
run: |
3983
# Validate Match-Secrets
40-
if [ "$(gh repo list --json name | jq --raw-output 'any(.name=="Match-Secrets")')" != "true" ]; then
41-
echo "A 'Match-Secrets' repository could not be found. Attempting to create one...";
84+
85+
# Ensure that gh exit codes are handled when output is piped.
86+
set -o pipefail
87+
88+
# If a Match-Secrets repository does not exist, attempt to create one.
89+
if ! visibility=$(gh repo view ${{ github.repository_owner }}/Match-Secrets --json visibility | jq --raw-output '.visibility | ascii_downcase'); then
90+
echo "A '${{ github.repository_owner }}/Match-Secrets' repository could not be found using the GH_PAT secret. Attempting to create one..."
4291
43-
if gh repo create Match-Secrets --private >/dev/null && [ "$(gh repo list --json name,visibility | jq --raw-output '.[] | select(.name=="Match-Secrets") | .visibility == "PRIVATE"')" == "true" ]; then
44-
echo "Created a private 'Match-Secrets' repository."
92+
# Create a private Match-Secrets repository and verify that it exists and that it is private.
93+
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
94+
echo "Created a private '${{ github.repository_owner }}/Match-Secrets' repository."
4595
else
4696
failed=true
47-
echo "::error::Cannot access or create a private 'Match-Secrets' repository. The GH_PAT secret is lacking at least the 'repo' permission scope required to access or create the repository.\
48-
Verify that token permissions are set correctly (or update them) at https://github.com/settings/tokens and try again."
97+
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."
4998
fi
50-
elif [ "$(gh repo list --json name,visibility | jq --raw-output '.[] | select(.name=="Match-Secrets") | .visibility == "PUBLIC"')" == "true" ]; then
99+
# Otherwise, if a Match-Secrets repository exists, but it is public, cause validation to fail.
100+
elif [[ "$visibility" == "public" ]]; then
51101
failed=true
52-
echo "::error::A 'Match-Secrets' repository was found, but it is is public. Delete it and try again (a private repository will be created for you)."
102+
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."
103+
else
104+
echo "Found a private '${{ github.repository_owner }}/Match-Secrets' repository to use."
53105
fi
54106
55107
# Exit unsuccessfully if secret validation failed.
@@ -59,7 +111,7 @@ jobs:
59111
60112
validate-fastlane-secrets:
61113
name: Fastlane
62-
needs: validate-match-secrets
114+
needs: [validate-access-token, validate-match-secrets]
63115
runs-on: macos-13
64116
env:
65117
GH_PAT: ${{ secrets.GH_PAT }}
@@ -123,10 +175,12 @@ jobs:
123175
if grep -q "bad decrypt" fastlane.log; then
124176
failed=true
125177
echo "::error::Unable to decrypt the Match-Secrets repository using the MATCH_PASSWORD secret. Verify that it is set correctly and try again."
126-
elif ! grep -q "No code signing identity found" fastlane.log; then
178+
elif grep -q -e "required agreement" -e "license agreement" fastlane.log; then
179+
failed=true
180+
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."
181+
elif ! grep -q -e "No code signing identity found" -e "Could not install WWDR certificate" fastlane.log; then
127182
failed=true
128-
echo "::error::Unable to create a valid authorization token for the App Store Connect API.\
129-
Verify that the FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY secrets are set correctly and try again."
183+
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."
130184
fi
131185
fi
132186

0 commit comments

Comments
 (0)