Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate handling of Distribution certificates and profiles #453

Merged
merged 3 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .github/workflows/build_trio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
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!"
9 changes: 4 additions & 5 deletions .github/workflows/validate_secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
57 changes: 55 additions & 2 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down Expand Up @@ -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
37 changes: 26 additions & 11 deletions fastlane/testflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ This method for building without a Mac was ported from Loop. If you have used th

There are more detailed instructions in LoopDocs for doing Browser Builds of Loop and other apps, including troubleshooting and build errors. Please refer to [LoopDocs](https://loopkit.github.io/loopdocs/gh-actions/gh-other-apps/) for more details.

If you build multiple apps, you may want to use a free *GitHub* organization. Please refer to [LoopDocs: Use a *GitHub* Organization Account](https://loopkit.github.io/loopdocs/gh-actions/gh-other-apps/#use-a-github-organization-account).
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

Expand Down Expand Up @@ -62,7 +62,7 @@ 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 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.

Expand All @@ -85,15 +85,20 @@ The first time you build with the GitHub Browser Build method for any DIY app, y

## Setup Github Trio repository

1. Fork https://github.com/nightscout/Trio into your account. If you already have a fork of Trio in GitHub, you can't make another one. You can continue to work with your existing fork, or delete that from GitHub and then fork https://github.com/nightscout/Trio.
1. In the forked Trio 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/nightscout/Trio 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/Trio:
* 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/Trio:
* 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 these variables are visible and can be edited.
* `ENABLE_NUKE_CERTS`

## Validate repository secrets

Expand Down Expand Up @@ -191,7 +196,7 @@ There is an additional identifier, but it does not need the App Group added to i

## Create Trio App in App Store Connect

If you created a Trio app in App Store Connect before, skip ahead to [Create Building Certficates](#create-building-certficates).
If you created a Trio app in App Store Connect before, skip ahead to [Create Building Certificates](#create-building-certificates).

1. Go to the [apps list](https://appstoreconnect.apple.com/apps) on App Store Connect and click the blue "plus" icon to create a New App.
* Select "iOS".
Expand All @@ -205,12 +210,11 @@ If you created a Trio app in App Store Connect before, skip ahead to [Create Bui

You do not need to fill out the next form. That is for submitting to the app store.

## Create Building Certficates
## Create Building Certificates

1. Go back to the "Actions" tab of your Trio repository in github.
1. Select "3. Create Certificates".
1. Click "Run Workflow", and tap the green 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 Trio 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 Trio!

Expand Down Expand Up @@ -304,3 +308,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 Trio 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.