Skip to content

Commit

Permalink
Add check_certs workflow and Fastlane lane for Distribution certifica…
Browse files Browse the repository at this point in the history
…te management.

- Introduces a new GitHub Actions workflow check_certs.yml for certificate validation and renewal.
- Adds a Fastlane lane check_and_renew_certificates to handle certificate checks, expiration warnings, and flag creation for automated renewal.
- Updates create_certs.yml to respond to both workflow_dispatch and workflow_call triggers for compatibility with the new workflow.
- Certificates are renewed if less than 7 days to expiry
- Annotations added after nuke and create certs
- Nuke certs only if ENABLE_NUKE_CERTS == 'true'
- Output annotation if nuke_certs were skipped due to ENABLE_NUKE_CERTS != true

 check_certs.yml: use checkout@v4

Don't nuke certs in warning period, add optional vars.FORCE_NUKE_CERTS

- Only emit warning for certs close to expiration, do not nuke valid certs.
- Introduce optional repository variable FORCE_NUKE_CERTS
- Nuke Certs if needed, and if the repository variable ENABLE_NUKE_CERTS is set to 'true', or if FORCE_NUKE_CERTS is set to 'true', which will always force certs to be nuked
- Emit annotations for FORCE_NUKE_CERTS

Require check_certs before building

Checks if Distribution certificate is present and valid, optionally nukes and reates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'

Set error when no valid certs and ENABLE_NUKE_CERTS is not 'true'.

Remove warnings about other apps from Fastfile, as these are displayed as annotations from check_certs.yml

Annotation for valid certs

Refine error annotations for Validate Fastlane Secrets

- Include the possibility of missing signing of agreements in the check for "No code signing identity found" or "Could not install WWDR certificate".

- Break up some long annotation strings into several messages
- Add ❗️-emoji to emphasise the suggested actions to take

Remove unused env

Refactor GitHub Actions Workflows and Fastlane Configuration

Details:

Workflows:

Removed the validate job dependency where unnecessary.
Adjusted needs dependencies in check_alive_and_permissions, check_latest_from_upstream, and build jobs to optimize execution order.
Consolidated redundant steps in check_certs.yml, reducing complexity.
Enhanced clarity by explicitly listing required secrets and improving step naming.
Added annotations for better debugging and user feedback during certificate operations.
Fastlane Configuration:

Changed match to disable forced certificate updates (force: false) and enabled verbose output.
Improved certificate expiration handling and logging for better feedback.
Fixed a typo in comments regarding certificate renewal flags.

Rename to "3. Check Certificates", delete old create_certificates.yml

Rename new workflow to create_certificates.yml

Using the old filename simplifies transitions when syncing branches. New workflow names are not visible in GitHub UI unless they are in the default branch.

validate_secrets annotation improvement

Rename to original names create_certs.yml name: 3. Create Certificates

Changed to reduce need for updating docs and instructions. The workflow for users will be the same as before this PR, but missing or invalid certificates or profiles will be updated automatically.

Update job and step names in create_certs.yml

Rename job check_certs to create_certs (original name)

- Keep step name check_certs
- Update step name under nuke_certs
- add comments for set -e at fastlane nuke_certs and fastlane certs

create_certs.yml job create_certs: name: Certificates
  • Loading branch information
bjornoleh committed Jan 14, 2025
1 parent 9e07a26 commit d91da8a
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 26 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/build_loop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ jobs:
uses: ./.github/workflows/validate_secrets.yml
secrets: inherit

# Checks if Distribution certificate is present and valid, optionally nukes and
# creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
check_certs:
name: Check certificates
uses: ./.github/workflows/create_certs.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
needs: check_certs
runs-on: ubuntu-latest
name: Check alive branch and permissions
permissions:
Expand Down Expand Up @@ -96,7 +103,7 @@ jobs:
# 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]
needs: [check_certs, check_alive_and_permissions]
runs-on: ubuntu-latest
name: Check upstream and keep alive
outputs:
Expand Down Expand Up @@ -185,7 +192,7 @@ jobs:
# Builds Loop
build:
name: Build
needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
needs: [check_certs, check_alive_and_permissions, check_latest_from_upstream]
runs-on: macos-14
permissions:
contents: write
Expand Down
109 changes: 90 additions & 19 deletions .github/workflows/create_certs.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
name: 3. Create Certificates
run-name: Create Certificates (${{ github.ref_name }})
on:
workflow_dispatch:

on: [workflow_call, workflow_dispatch]

env:
TEAMID: ${{ secrets.TEAMID }}
GH_PAT: ${{ secrets.GH_PAT }}
GH_TOKEN: ${{ secrets.GH_PAT }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}

jobs:
validate:
name: Validate
uses: ./.github/workflows/validate_secrets.yml
secrets: inherit
certificates:
name: Create Certificates

create_certs:
name: Certificates
needs: validate
runs-on: macos-14
outputs:
new_certificate_needed: ${{ steps.set_output.outputs.new_certificate_needed }}

steps:
# Uncomment to manually select latest Xcode if needed
#- name: Select Latest Xcode
Expand All @@ -37,17 +49,76 @@ jobs:
- 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

# Create or update certificates for app
- name: Create Certificates
run: bundle exec fastlane certs
env:
TEAMID: ${{ secrets.TEAMID }}
GH_PAT: ${{ secrets.GH_PAT }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
# Create or update Distribution certificate and provisioning profiles
- name: Check and create or update Distribution certificate and profiles if needed
run: |
echo "Running Fastlane certs lane..."
bundle exec fastlane certs || true # ignore and continue on errors without annotating an exit code
- name: Check Distribution certificate and launch Nuke certificates if needed
run: bundle exec fastlane check_and_renew_certificates
id: check_certs

- name: Set output and annotations based on Fastlane result
id: set_output
run: |
CERT_STATUS_FILE="${{ github.workspace }}/fastlane/new_certificate_needed.txt"
ENABLE_NUKE_CERTS=${{ vars.ENABLE_NUKE_CERTS }}
if [ -f "$CERT_STATUS_FILE" ]; then
CERT_STATUS=$(cat "$CERT_STATUS_FILE" | tr -d '\n' | tr -d '\r') # Read file content and strip newlines
echo "new_certificate_needed: $CERT_STATUS"
echo "new_certificate_needed=$CERT_STATUS" >> $GITHUB_OUTPUT
else
echo "Certificate status file not found. Defaulting to false."
echo "new_certificate_needed=false" >> $GITHUB_OUTPUT
fi
# Check if ENABLE_NUKE_CERTS is not set to true when certs are valid
if [ "$CERT_STATUS" != "true" ] && [ "$ENABLE_NUKE_CERTS" != "true" ]; then
echo "::notice::🔔 Automated renewal of certificates is disabled because the repository variable ENABLE_NUKE_CERTS is not set to 'true'."
fi
# Check if ENABLE_NUKE_CERTS is not set to true when certs are not valid
if [ "$CERT_STATUS" = "true" ] && [ "$ENABLE_NUKE_CERTS" != "true" ]; then
echo "::error::❌ No valid distribution certificate found. Automated renewal of certificates was skipped because the repository variable ENABLE_NUKE_CERTS is not set to 'true'."
exit 1
fi
# Check if vars.FORCE_NUKE_CERTS is not set to true
if [ vars.FORCE_NUKE_CERTS = "true" ]; then
echo "::warning::‼️ Nuking of certificates was forced because the repository variable FORCE_NUKE_CERTS is set to 'true'."
fi
# Nuke Certs if needed, and if the repository variable ENABLE_NUKE_CERTS is set to 'true', or if FORCE_NUKE_CERTS is set to 'true', which will always force certs to be nuked
nuke_certs:
name: Nuke certificates
needs: [validate, create_certs]
runs-on: macos-14
if: ${{ (needs.create_certs.outputs.new_certificate_needed == 'true' && vars.ENABLE_NUKE_CERTS == 'true') || vars.FORCE_NUKE_CERTS == 'true' }}
steps:
- name: Output from step id 'check_certs'
run: echo "new_certificate_needed=${{ needs.create_certs.outputs.new_certificate_needed }}"

- name: Checkout repository
uses: actions/checkout@v4

- name: Install dependencies
run: bundle install

- name: Run Fastlane nuke_certs
run: |
set -e # Set error immediately after this step if error occurs
bundle exec fastlane nuke_certs
- name: Recreate Distribution certificate after nuking
run: |
set -e # Set error immediately after this step if error occurs
bundle exec fastlane certs
- name: Add success annotations for nuke and certificate recreation
if: ${{ success() }}
run: |
echo "::warning::⚠️ All Distribution certificates and TestFlight profiles have been revoked and recreated."
echo "::warning::❗️ If you have other apps being distributed by GitHub Actions / Fastlane / TestFlight that does not renew certificates automatically, please run the '3. Create Certificates' workflow for each of these apps to allow these apps to be built."
echo "::warning::✅ But don't worry about your existing TestFlight builds, they will keep working!"
7 changes: 5 additions & 2 deletions .github/workflows/validate_secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,13 @@ jobs:
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."
echo "::error::Unable to create a valid authorization token for the App Store Connect API."
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."
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."
echo "::error::Unable to create a valid authorization token for the App Store Connect API."
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."
echo "::error::❗️ If you created a new FASTLANE KEY or have not previously succeeded with validate secrets, then check that FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY secrets were entered correctly."
fi
fi
Expand Down
57 changes: 55 additions & 2 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ platform :ios do

match(
type: "appstore",
force: true,
force: false,
verbose: true,
git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"),
app_identifier: [
"com.#{TEAMID}.loopkit.Loop",
Expand Down Expand Up @@ -276,4 +277,56 @@ platform :ios do
git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}")
)
end
end

desc "Check Certificates and Trigger Workflow for Expired or Missing Certificates"
lane :check_and_renew_certificates do
setup_ci if ENV['CI']
ENV["MATCH_READONLY"] = false.to_s

# Authenticate using App Store Connect API Key
api_key = app_store_connect_api_key(
key_id: ENV["FASTLANE_KEY_ID"],
issuer_id: ENV["FASTLANE_ISSUER_ID"],
key_content: ENV["FASTLANE_KEY"] # Ensure valid key content
)

# Initialize flag to track if renewal of certificates is needed
new_certificate_needed = false

# Fetch all certificates
certificates = Spaceship::ConnectAPI::Certificate.all

# Filter for Distribution Certificates
distribution_certs = certificates.select { |cert| cert.certificate_type == "DISTRIBUTION" }

# Handle case where no distribution certificates are found
if distribution_certs.empty?
puts "No Distribution certificates found! Triggering action to create certificate."
new_certificate_needed = true
else
# Check for expiration
distribution_certs.each do |cert|
expiration_date = Time.parse(cert.expiration_date)

puts "Current Distribution Certificate: #{cert.id}, Expiration date: #{expiration_date}"

if expiration_date < Time.now
puts "Distribution Certificate #{cert.id} is expired! Triggering action to renew certificate."
new_certificate_needed = true
else
puts "Distribution certificate #{cert.id} is valid. No action required."
end
end
end

# Write result to new_certificate_needed.txt
file_path = File.expand_path('new_certificate_needed.txt')
File.write(file_path, new_certificate_needed ? 'true' : 'false')

# Log the absolute path and contents of the new_certificate_needed.txt file
puts ""
puts "Absolute path of new_certificate_needed.txt: #{file_path}"
new_certificate_needed_content = File.read(file_path)
puts "Certificate creation or renewal needed: #{new_certificate_needed_content}"
end
end

0 comments on commit d91da8a

Please sign in to comment.