From f06675bf6e9aaf207bf7cea99c66fbb53795810e Mon Sep 17 00:00:00 2001 From: bjornoleh Date: Tue, 14 Jan 2025 23:27:39 +0100 Subject: [PATCH] Automate handling of Distribution certificates and profiles --- .github/workflows/build_trio.yml | 16 ++-- .github/workflows/create_certs.yml | 109 ++++++++++++++++++++----- .github/workflows/validate_secrets.yml | 7 +- fastlane/Fastfile | 57 ++++++++++++- 4 files changed, 159 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build_trio.yml b/.github/workflows/build_trio.yml index ae2dad720..bd36c8670 100644 --- a/.github/workflows/build_trio.yml +++ b/.github/workflows/build_trio.yml @@ -18,15 +18,17 @@ env: ALIVE_BRANCH_DEV: alive-dev jobs: - validate: - name: Validate - 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: @@ -96,7 +98,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: @@ -185,7 +187,7 @@ jobs: # Builds Trio 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 diff --git a/.github/workflows/create_certs.yml b/.github/workflows/create_certs.yml index 9c4b51722..7a5b29cb0 100644 --- a/.github/workflows/create_certs.yml +++ b/.github/workflows/create_certs.yml @@ -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 @@ -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!" diff --git a/.github/workflows/validate_secrets.yml b/.github/workflows/validate_secrets.yml index 15562a740..3211b028d 100644 --- a/.github/workflows/validate_secrets.yml +++ b/.github/workflows/validate_secrets.yml @@ -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 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 543fa6129..e60aa3728 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -217,7 +217,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: [ "#{BUNDLE_ID}", @@ -271,4 +272,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 \ No newline at end of file