diff --git a/.github/workflows/build_loop.yml b/.github/workflows/build_loop.yml index 254c4015f..137b367fc 100644 --- a/.github/workflows/build_loop.yml +++ b/.github/workflows/build_loop.yml @@ -18,15 +18,17 @@ env: ALIVE_BRANCH_DEV: alive-dev jobs: - validate: - name: Validate - uses: ./.github/workflows/validate_secrets.yml + # 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 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 diff --git a/.github/workflows/create_certs.yml b/.github/workflows/create_certs.yml index 9c4b51722..025c0cb27 100644 --- a/.github/workflows/create_certs.yml +++ b/.github/workflows/create_certs.yml @@ -1,23 +1,32 @@ 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 + runs-on: macos-15 + outputs: + new_certificate_needed: ${{ steps.set_output.outputs.new_certificate_needed }} + steps: - # Uncomment to manually select latest Xcode if needed - #- name: Select Latest Xcode - # run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer" - # Checks-out the repo - name: Checkout Repo uses: actions/checkout@v4 @@ -41,13 +50,69 @@ jobs: - 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..dbbec0f5e 100644 --- a/.github/workflows/validate_secrets.yml +++ b/.github/workflows/validate_secrets.yml @@ -178,16 +178,15 @@ jobs: 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 + 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 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." + 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 "Your certificate .* is not valid" fastlane.log; then + echo "::notice::Your Distribution certificate is invalid or expired. Automated renewal of the certificate will be attempted." fi fi diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f918b3aa4..e71a3ee16 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -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", @@ -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 \ No newline at end of file diff --git a/fastlane/testflight.md b/fastlane/testflight.md index abe99314e..c344e91dc 100644 --- a/fastlane/testflight.md +++ b/fastlane/testflight.md @@ -2,14 +2,14 @@ These instructions allow you to build Loop without having access to a Mac. -* You can install Loop on phones via TestFlight that are not connected to your computer +* You can install Loop on phones using TestFlight that are not connected to your computer * You can send builds and updates to those you care for * You can install Loop on your phone using only the TestFlight app if a phone was lost or the app is accidentally deleted * You do not need to worry about specific Xcode/Mac versions for a given iOS ## **Automatic Builds** > -> This new version of the browser build **defaults to** automatically updating and building a new version of Loop according to this schedule: +> The browser build **defaults to** automatically updating and building a new version of Loop according to this schedule: > - automatically checks for updates weekly on Wednesdays and if updates are found, it will build a new version of the app > - automatically builds once a month regardless of whether there are updates on the first of the month > - with each scheduled run (weekly or monthly), a successful Build Loop log appears - if the time is very short, it did not need to build - only the long actions (>20 minutes) built a new Loop app @@ -33,6 +33,8 @@ There are more detailed instructions in LoopDocs for using GitHub for Browser Bu Note that installing with TestFlight, (in the US), requires the Apple ID account holder to be 13 years or older. For younger Loopers, an adult must log into Media & Purchase on the child's phone to install Loop. More details on this can be found in [LoopDocs](https://loopkit.github.io/loopdocs/gh-actions/gh-deploy/#install-testflight-loop-for-child). +If you build multiple apps, it is strongly recommended that you configure a free *GitHub* organization and do all your building in the organization. This means you enter items one time for the organization (6 SECRETS required to build and 1 VARIABLE required to automatically update your certificates annually). Otherwise, those 6 SECRETS must be entered for every repository. Please refer to [LoopDocs: Use a *GitHub* Organization Account](https://loopkit.github.io/loopdocs/gh-actions/gh-other-apps/#use-a-github-organization-account). + ## Prerequisites * A [GitHub account](https://github.com/signup). The free level comes with plenty of storage and free compute time to build loop, multiple times a day, if you wanted to. @@ -48,6 +50,8 @@ You require 6 Secrets (alphanumeric items) to use the GitHub build method and if * Be sure to save the 6 Secrets in a text file using a text editor - Do **NOT** use a smart editor, which might auto-correct and change case, because these Secrets are case sensitive +Refer to [LoopDocs: Make a Secrets Reference File](https://loopkit.github.io/loopdocs/gh-actions/gh-first-time/#make-a-secrets-reference-file) for a handy template to use when saving your Secrets. + ## Generate App Store Connect API Key This step is common for all GitHub Browser Builds; do this step only once. You will be saving 4 Secrets from your Apple Account in this step. @@ -61,6 +65,8 @@ This step is common for all GitHub Browser Builds; do this step only once. You w ## Create GitHub Personal Access Token +If you have previously built another app using the "browser build" method, you use the same personal access token (`GH_PAT`), so skip this step. If you use a free GitHub organization to build, you still use the same personal access token. This is created using your personal GitHub username. + Log into your GitHub account to create a personal access token; this is one of two GitHub secrets needed for your build. 1. Create a [new personal access token](https://github.com/settings/tokens/new): @@ -76,25 +82,26 @@ This is the second one of two GitHub secrets needed for your build. The first time you build with the GitHub Browser Build method for any DIY app, you will make up a password and record it as `MATCH_PASSWORD`. Note, if you later lose `MATCH_PASSWORD`, you will need to delete and make a new Match-Secrets repository (next step). -## Setup GitHub Match-Secrets Repository - -The creation of the Match-Secrets repository is a common step for all GitHub Browser Builds; do this step only once. You must be logged into your GitHub account. +## GitHub Match-Secrets Repository -1. Create a [new empty repository](https://github.com/new) titled `Match-Secrets`. It should be private. - -Once created, you will not take any direct actions with this repository; it needs to be there for the GitHub to use as you progress through the steps. +A private Match-Secrets repository is automatically created under your GitHub username the first time you run a GitHub Action. Because it is a private repository - only you can see it. You will not take any direct actions with this repository; it needs to be there for GitHub to use as you progress through the steps. ## Setup GitHub LoopWorkspace Repository -1. Fork https://github.com/LoopKit/LoopWorkspace into your account. -1. In the forked LoopWorkspace repo, go to Settings -> Secrets and variables -> Actions. -1. For each of the following secrets, tap on "New repository secret", then add the name of the secret, along with the value you recorded for it: +1. Fork https://github.com/LoopKit/LoopWorkspace into your GitHub username (using your organization if you have one). If you already have a fork of Trio in that username, you should not make another one. Do not rename the repository. You can continue to work with your existing fork, or delete that from GitHub and then fork again. +1. If you are using an organization, do this step at the organization level, e.g., username-org. If you are not using an organization, do this step at the repository level, e.g., username/LoopWorkspace: + * Go to Settings -> Secrets and variables -> Actions and make sure the Secrets tab is open +1. For each of the following secrets, tap on "New organization secret" or "New repository secret", then add the name of the secret, along with the value you recorded for it: * `TEAMID` * `FASTLANE_ISSUER_ID` * `FASTLANE_KEY_ID` * `FASTLANE_KEY` * `GH_PAT` * `MATCH_PASSWORD` +1. If you are using an organization, do this step at the organization level, e.g., username-org. If you are not using an organization, do this step at the repository level, e.g., username/LoopWorkspace: + * Go to Settings -> Secrets and variables -> Actions and make sure the Variables tab is open +1. Tap on "Create new organization variable" or "Create new repository variable", then add the name below and enter the value true. Unlike secrets variables are visible and can be edited. + * `ENABLE_NUKE_CERTS` ## Validate repository secrets @@ -106,6 +113,8 @@ This step validates most of your six Secrets and provides error messages if it d 1. Wait, and within a minute or two you should see a green checkmark indicating the workflow succeeded. 1. The workflow will check if the required secrets are added and that they are correctly formatted. If errors are detected, please check the run log for details. +There can be a delay after you start a workflow before the screen changes. Refresh your browser to see if it started. And if it seems to take a long time to finish - refresh your browser to see if it is done. + ## Add Identifiers for Loop App 1. Click on the "Actions" tab of your LoopWorkspace repository. @@ -171,10 +180,9 @@ You do not need to fill out the next form. That is for submitting to the app sto ## Create Building Certificates -1. Go back to the "Actions" tab of your LoopWorkspace repository in GitHub. -1. On the left side, select "3. Create Certificates". -1. On the right side, click "Run Workflow", and tap the green `Run workflow` button. -1. Wait, and within a minute or two you should see a green checkmark indicating the workflow succeeded. +This step is no longer required. The Build Loop function now takes care of this for you. It does not hurt to run it but is not needed. + +Once a year, you will get an email from Apple indicating your certificate will expire in 30 days. You can ignore that email. When it does expire, the next time an automatic or manual build happens, the expired certificate information will be removed (nuked) from your Match-Secrets repository and a new one created. This should happen without you needing to take any action. ## Build Loop @@ -267,3 +275,14 @@ Your build will run on the following conditions: - If you disable any automation (both variables set to `false`), no updates, keep-alive or building happens when Build Loop runs - If you disabled just scheduled synchronization (`SCHEDULED_SYNC` set to`false`), it will only run once a month, on the first of the month, no update will happen; keep-alive will run - If you disabled just scheduled build (`SCHEDULED_BUILD` set to`false`), it will run once weekly, every Wednesday, to check for changes; if there are changes, it will update and build; keep-alive will run + +## What if I build using more than one GitHub username + +This is not typical. But if you do use more than one GitHub username, follow these steps at the time of the annual certificate renewal. + +1. After the certificates were removed (nuked) from username1 Match-Secrets storage, you need to switch to username2 +1. Add the variable FORCE_NUKE_CERTS=true to the username2/Trio repository +1. Run the action Create Certificate (or Build, but Create is faster) +1. Immediately set FORCE_NUKE_CERTS=false or delete the variable + +Now certificates for username2 have been cleared out of Match-Secrets storage for username2. Building can proceed as usual for both username1 and username2.