Explorar el Código

Merge pull request #176 from bjornoleh/dev-fastlane

FastLane and GitHub Actions for Mac-free install, for (semi-)automated TestFlight distributions.
jonfawcett hace 3 años
padre
commit
a483e3d65f

+ 30 - 0
.github/workflows/add_identifiers.yml

@@ -0,0 +1,30 @@
+name: Add Identifiers
+on:
+  workflow_dispatch:
+
+jobs:
+  identifiers:
+    runs-on: macos-12
+    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@v3
+        
+      # Patch Fastlane Match to not print tables
+      - name: Patch Match Tables
+        run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
+        
+      # Create or update identifiers for app
+      - name: Fastlane Provision
+        run: fastlane identifiers
+        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 }}

+ 52 - 0
.github/workflows/build_LoopFollow.yml

@@ -0,0 +1,52 @@
+name: Build Loop Follow
+on:
+  workflow_dispatch:
+
+jobs:
+  build:
+    runs-on: macos-12
+    steps:
+      # Uncomment to manually select latest Xcode if needed
+      - name: Select Latest Xcode
+        run: "sudo xcode-select --switch /Applications/Xcode_14.1.app/Contents/Developer"
+
+      # Checks-out the repo
+      - name: Checkout Repo
+        uses: actions/checkout@v3
+        with:
+          submodules: recursive
+      
+      # Patch Fastlane Match to not print tables
+      - name: Patch Match Tables
+        run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
+      
+      # Build signed FreeAPS X IPA file
+      - name: Fastlane Build & Archive
+        run: fastlane build_LoopFollow
+        env:
+          TEAMID: ${{ secrets.TEAMID }}
+          GH_PAT: ${{ secrets.GH_PAT }}
+          FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
+          FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
+          FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
+          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
+      
+      # Upload to TestFlight
+      - name: Fastlane upload to TestFlight
+        run: fastlane release
+        env:
+          TEAMID: ${{ secrets.TEAMID }}
+          GH_PAT: ${{ secrets.GH_PAT }}
+          FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
+          FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
+          FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
+          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
+
+      # Upload IPA and Symbols
+      - name: Upload IPA and Symbol artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          name: build-artifacts
+          path: |
+            artifacts
+            buildlog

+ 30 - 0
.github/workflows/create_certs.yml

@@ -0,0 +1,30 @@
+name: Create Certificates
+on:
+  workflow_dispatch:
+
+jobs:
+  certificates:
+    runs-on: macos-12
+    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@v3
+        
+      # Patch Fastlane Match to not print tables
+      - name: Patch Match Tables
+        run: find /usr/local/lib/ruby/gems -name table_printer.rb | xargs sed -i "" "/puts(Terminal::Table.new(params))/d"
+        
+      # Create or update certificates for app
+      - name: Create Certificates
+        run: 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 }}

+ 3 - 0
Gemfile

@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gem "fastlane"

+ 215 - 0
Gemfile.lock

@@ -0,0 +1,215 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    CFPropertyList (3.0.4)
+      rexml
+    addressable (2.8.0)
+      public_suffix (>= 2.0.2, < 5.0)
+    artifactory (3.0.15)
+    atomos (0.1.3)
+    aws-eventstream (1.2.0)
+    aws-partitions (1.516.0)
+    aws-sdk-core (3.121.2)
+      aws-eventstream (~> 1, >= 1.0.2)
+      aws-partitions (~> 1, >= 1.239.0)
+      aws-sigv4 (~> 1.1)
+      jmespath (~> 1.0)
+    aws-sdk-kms (1.50.0)
+      aws-sdk-core (~> 3, >= 3.121.2)
+      aws-sigv4 (~> 1.1)
+    aws-sdk-s3 (1.104.0)
+      aws-sdk-core (~> 3, >= 3.121.2)
+      aws-sdk-kms (~> 1)
+      aws-sigv4 (~> 1.4)
+    aws-sigv4 (1.4.0)
+      aws-eventstream (~> 1, >= 1.0.2)
+    babosa (1.0.4)
+    claide (1.0.3)
+    colored (1.2)
+    colored2 (3.1.2)
+    commander (4.6.0)
+      highline (~> 2.0.0)
+    declarative (0.0.20)
+    digest-crc (0.6.4)
+      rake (>= 12.0.0, < 14.0.0)
+    domain_name (0.5.20190701)
+      unf (>= 0.0.5, < 1.0.0)
+    dotenv (2.7.6)
+    emoji_regex (3.2.3)
+    excon (0.87.0)
+    faraday (1.8.0)
+      faraday-em_http (~> 1.0)
+      faraday-em_synchrony (~> 1.0)
+      faraday-excon (~> 1.1)
+      faraday-httpclient (~> 1.0.1)
+      faraday-net_http (~> 1.0)
+      faraday-net_http_persistent (~> 1.1)
+      faraday-patron (~> 1.0)
+      faraday-rack (~> 1.0)
+      multipart-post (>= 1.2, < 3)
+      ruby2_keywords (>= 0.0.4)
+    faraday-cookie_jar (0.0.7)
+      faraday (>= 0.8.0)
+      http-cookie (~> 1.0.0)
+    faraday-em_http (1.0.0)
+    faraday-em_synchrony (1.0.0)
+    faraday-excon (1.1.0)
+    faraday-httpclient (1.0.1)
+    faraday-net_http (1.0.1)
+    faraday-net_http_persistent (1.2.0)
+    faraday-patron (1.0.0)
+    faraday-rack (1.0.0)
+    faraday_middleware (1.2.0)
+      faraday (~> 1.0)
+    fastimage (2.2.5)
+    fastlane (2.196.0)
+      CFPropertyList (>= 2.3, < 4.0.0)
+      addressable (>= 2.8, < 3.0.0)
+      artifactory (~> 3.0)
+      aws-sdk-s3 (~> 1.0)
+      babosa (>= 1.0.3, < 2.0.0)
+      bundler (>= 1.12.0, < 3.0.0)
+      colored
+      commander (~> 4.6)
+      dotenv (>= 2.1.1, < 3.0.0)
+      emoji_regex (>= 0.1, < 4.0)
+      excon (>= 0.71.0, < 1.0.0)
+      faraday (~> 1.0)
+      faraday-cookie_jar (~> 0.0.6)
+      faraday_middleware (~> 1.0)
+      fastimage (>= 2.1.0, < 3.0.0)
+      gh_inspector (>= 1.1.2, < 2.0.0)
+      google-apis-androidpublisher_v3 (~> 0.3)
+      google-apis-playcustomapp_v1 (~> 0.1)
+      google-cloud-storage (~> 1.31)
+      highline (~> 2.0)
+      json (< 3.0.0)
+      jwt (>= 2.1.0, < 3)
+      mini_magick (>= 4.9.4, < 5.0.0)
+      multipart-post (~> 2.0.0)
+      naturally (~> 2.2)
+      optparse (~> 0.1.1)
+      plist (>= 3.1.0, < 4.0.0)
+      rubyzip (>= 2.0.0, < 3.0.0)
+      security (= 0.1.3)
+      simctl (~> 1.6.3)
+      terminal-notifier (>= 2.0.0, < 3.0.0)
+      terminal-table (>= 1.4.5, < 2.0.0)
+      tty-screen (>= 0.6.3, < 1.0.0)
+      tty-spinner (>= 0.8.0, < 1.0.0)
+      word_wrap (~> 1.0.0)
+      xcodeproj (>= 1.13.0, < 2.0.0)
+      xcpretty (~> 0.3.0)
+      xcpretty-travis-formatter (>= 0.0.3)
+    gh_inspector (1.1.3)
+    google-apis-androidpublisher_v3 (0.12.0)
+      google-apis-core (>= 0.4, < 2.a)
+    google-apis-core (0.4.1)
+      addressable (~> 2.5, >= 2.5.1)
+      googleauth (>= 0.16.2, < 2.a)
+      httpclient (>= 2.8.1, < 3.a)
+      mini_mime (~> 1.0)
+      representable (~> 3.0)
+      retriable (>= 2.0, < 4.a)
+      rexml
+      webrick
+    google-apis-iamcredentials_v1 (0.7.0)
+      google-apis-core (>= 0.4, < 2.a)
+    google-apis-playcustomapp_v1 (0.5.0)
+      google-apis-core (>= 0.4, < 2.a)
+    google-apis-storage_v1 (0.8.0)
+      google-apis-core (>= 0.4, < 2.a)
+    google-cloud-core (1.6.0)
+      google-cloud-env (~> 1.0)
+      google-cloud-errors (~> 1.0)
+    google-cloud-env (1.5.0)
+      faraday (>= 0.17.3, < 2.0)
+    google-cloud-errors (1.2.0)
+    google-cloud-storage (1.34.1)
+      addressable (~> 2.5)
+      digest-crc (~> 0.4)
+      google-apis-iamcredentials_v1 (~> 0.1)
+      google-apis-storage_v1 (~> 0.1)
+      google-cloud-core (~> 1.6)
+      googleauth (>= 0.16.2, < 2.a)
+      mini_mime (~> 1.0)
+    googleauth (1.0.0)
+      faraday (>= 0.17.3, < 2.0)
+      jwt (>= 1.4, < 3.0)
+      memoist (~> 0.16)
+      multi_json (~> 1.11)
+      os (>= 0.9, < 2.0)
+      signet (>= 0.16, < 2.a)
+    highline (2.0.3)
+    http-cookie (1.0.4)
+      domain_name (~> 0.5)
+    httpclient (2.8.3)
+    jmespath (1.4.0)
+    json (2.6.0)
+    jwt (2.3.0)
+    memoist (0.16.2)
+    mini_magick (4.11.0)
+    mini_mime (1.1.2)
+    multi_json (1.15.0)
+    multipart-post (2.0.0)
+    nanaimo (0.3.0)
+    naturally (2.2.1)
+    optparse (0.1.1)
+    os (1.1.1)
+    plist (3.6.0)
+    public_suffix (4.0.6)
+    rake (13.0.6)
+    representable (3.1.1)
+      declarative (< 0.1.0)
+      trailblazer-option (>= 0.1.1, < 0.2.0)
+      uber (< 0.2.0)
+    retriable (3.1.2)
+    rexml (3.2.5)
+    rouge (2.0.7)
+    ruby2_keywords (0.0.5)
+    rubyzip (2.3.2)
+    security (0.1.3)
+    signet (0.16.0)
+      addressable (~> 2.8)
+      faraday (>= 0.17.3, < 2.0)
+      jwt (>= 1.5, < 3.0)
+      multi_json (~> 1.10)
+    simctl (1.6.8)
+      CFPropertyList
+      naturally
+    terminal-notifier (2.0.0)
+    terminal-table (1.8.0)
+      unicode-display_width (~> 1.1, >= 1.1.1)
+    trailblazer-option (0.1.1)
+    tty-cursor (0.7.1)
+    tty-screen (0.8.1)
+    tty-spinner (0.9.3)
+      tty-cursor (~> 0.7)
+    uber (0.1.0)
+    unf (0.1.4)
+      unf_ext
+    unf_ext (0.0.8)
+    unicode-display_width (1.8.0)
+    webrick (1.7.0)
+    word_wrap (1.0.0)
+    xcodeproj (1.21.0)
+      CFPropertyList (>= 2.3.3, < 4.0)
+      atomos (~> 0.1.3)
+      claide (>= 1.0.2, < 2.0)
+      colored2 (~> 3.1)
+      nanaimo (~> 0.3.0)
+      rexml (~> 3.2.4)
+    xcpretty (0.3.0)
+      rouge (~> 2.0.7)
+    xcpretty-travis-formatter (1.0.1)
+      xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+  arm64-darwin-21
+  x86_64-darwin-19
+
+DEPENDENCIES
+  fastlane
+
+BUNDLED WITH
+   2.3.26

+ 2 - 0
LoopFollow/Info.plist

@@ -87,5 +87,7 @@
 		<string>UIInterfaceOrientationLandscapeLeft</string>
 		<string>UIInterfaceOrientationLandscapeRight</string>
 	</array>
+	<key>ITSAppUsesNonExemptEncryption</key>
+<false/>
 </dict>
 </plist>

+ 180 - 0
fastlane/Fastfile

@@ -0,0 +1,180 @@
+# This file contains the fastlane.tools configuration
+# You can find the documentation at https://docs.fastlane.tools
+#
+# For a list of all available actions, check out
+#
+#     https://docs.fastlane.tools/actions
+#
+# For a list of all available plugins, check out
+#
+#     https://docs.fastlane.tools/plugins/available-plugins
+#
+
+default_platform(:ios)
+
+TEAMID = ENV["TEAMID"]
+GH_PAT = ENV["GH_PAT"]
+GITHUB_WORKSPACE = ENV["GITHUB_WORKSPACE"]
+GITHUB_REPOSITORY_OWNER = ENV["GITHUB_REPOSITORY_OWNER"]
+FASTLANE_KEY_ID = ENV["FASTLANE_KEY_ID"]
+FASTLANE_ISSUER_ID = ENV["FASTLANE_ISSUER_ID"]
+FASTLANE_KEY = ENV["FASTLANE_KEY"]
+DEVICE_NAME = ENV["DEVICE_NAME"]
+DEVICE_ID = ENV["DEVICE_ID"]
+ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120"
+
+platform :ios do
+  desc "Build Loop Follow"
+  lane :build_LoopFollow do
+    setup_ci if ENV['CI']
+    
+    update_project_team(
+      path: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj",
+      teamid: "#{TEAMID}"
+    )
+
+    api_key = app_store_connect_api_key(
+      key_id: "#{FASTLANE_KEY_ID}",
+      issuer_id: "#{FASTLANE_ISSUER_ID}",
+      key_content: "#{FASTLANE_KEY}"
+    )
+
+    previous_build_number = latest_testflight_build_number(
+      app_identifier: "com.#{TEAMID}.LoopFollow",
+      api_key: api_key,
+    )
+
+    current_build_number = previous_build_number + 1
+
+    increment_build_number(
+      xcodeproj: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj",
+      build_number: current_build_number
+    )      
+    
+    match(
+      type: "appstore",
+      git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"),
+      app_identifier: [
+        "com.#{TEAMID}.LoopFollow"
+      ]
+    )
+
+    previous_build_number = latest_testflight_build_number(
+      app_identifier: "com.#{TEAMID}.LoopFollow",
+      api_key: api_key,
+    )
+
+    current_build_number = previous_build_number + 1
+
+    increment_build_number(
+      xcodeproj: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj",
+      build_number: current_build_number
+    )
+    
+    mapping = Actions.lane_context[
+      SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING
+    ]
+
+    update_code_signing_settings(
+      path: "#{GITHUB_WORKSPACE}/LoopFollow.xcodeproj",
+      profile_name: mapping["com.#{TEAMID}.LoopFollow"],
+      code_sign_identity: "iPhone Distribution",
+      targets: ["LoopFollow"]
+    )
+
+    gym(
+      export_method: "app-store",
+      scheme: "LoopFollow",
+      output_name: "LoopFollow.ipa",
+      configuration: "Release",
+      destination: 'generic/platform=iOS',
+      buildlog_path: 'buildlog'
+    )
+
+    copy_artifacts(
+      target_path: "artifacts",
+      artifacts: ["*.mobileprovision", "*.ipa", "*.dSYM.zip"]
+    )
+  end
+
+  desc "Push to TestFlight"
+  lane :release do
+    api_key = app_store_connect_api_key(
+      key_id: "#{FASTLANE_KEY_ID}",
+      issuer_id: "#{FASTLANE_ISSUER_ID}",
+      key_content: "#{FASTLANE_KEY}"
+    )
+    
+    upload_to_testflight(
+      api_key: api_key,
+      skip_submission: false,
+      ipa: "LoopFollow.ipa",
+      skip_waiting_for_build_processing: true,
+    )
+  end
+
+  desc "Provision Identifiers and Certificates"
+  lane :identifiers do
+    setup_ci if ENV['CI']
+    ENV["MATCH_READONLY"] = false.to_s
+    
+    app_store_connect_api_key(
+      key_id: "#{FASTLANE_KEY_ID}",
+      issuer_id: "#{FASTLANE_ISSUER_ID}",
+      key_content: "#{FASTLANE_KEY}"
+    )
+
+    def configure_bundle_id(name, identifier, capabilities)
+      bundle_id = Spaceship::ConnectAPI::BundleId.find(identifier) || Spaceship::ConnectAPI::BundleId.create(name: name, identifier: identifier)
+      capabilities.each { |capability|
+        bundle_id.create_capability(capability)
+      }
+    end
+
+    configure_bundle_id("LoopFollow", "com.#{TEAMID}.LoopFollow", [
+      Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS,
+      Spaceship::ConnectAPI::BundleIdCapability::Type::HEALTHKIT
+    ])
+    
+  end
+
+  desc "Provision Certificates"
+  lane :certs do
+    setup_ci if ENV['CI']
+    ENV["MATCH_READONLY"] = false.to_s
+    
+    app_store_connect_api_key(
+      key_id: "#{FASTLANE_KEY_ID}",
+      issuer_id: "#{FASTLANE_ISSUER_ID}",
+      key_content: "#{FASTLANE_KEY}"
+    )
+    
+    match(
+      type: "appstore",
+      force: true,
+      git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"),
+      app_identifier: [
+        "com.#{TEAMID}.LoopFollow",
+      ]
+    )
+  end
+
+  desc "Nuke Certs"
+  lane :nuke_certs do
+    setup_ci if ENV['CI']
+    ENV["MATCH_READONLY"] = false.to_s
+    
+    app_store_connect_api_key(
+      key_id: "#{FASTLANE_KEY_ID}",
+      issuer_id: "#{FASTLANE_ISSUER_ID}",
+      key_content: "#{FASTLANE_KEY}"
+    )
+    
+    match_nuke(
+      type: "appstore",
+      team_id: "#{TEAMID}",
+      skip_confirmation: true,
+      git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}")
+    )
+  end
+end

+ 10 - 0
fastlane/Matchfile

@@ -0,0 +1,10 @@
+
+GITHUB_REPOSITORY_OWNER ||= ENV["GITHUB_REPOSITORY_OWNER"]
+
+git_url("https://github.com/#{GITHUB_REPOSITORY_OWNER}/Match-Secrets.git")
+
+storage_mode("git")
+
+type("appstore")
+
+# The docs are available on https://docs.fastlane.tools/actions/match

+ 81 - 0
fastlane/testflight.md

@@ -0,0 +1,81 @@
+# Using Github Actions + FastLane to deploy to TestFlight
+
+These instructions allow you to build Loop Follow without having access to a Mac. They also allow you to easily install Loop Follow on phones that are not connected to your computer. So you can send builds and updates to those you care for easily, or have an easy to access backup if you run Loop Follow for yourself. You do not need to worry about correct Xcode/Mac versions either. An app built using this method can easily be deployed to newer versions of iOS, as soon as they are available.
+The setup steps are somewhat involved, but nearly all are one time steps. Subsequent builds are trivial.  Note that TestFlight requires apple id accounts 13 years or older. Your app must be updated once every 90 days, but it's a simple click to make a new build and can be done from anywhere.
+
+There are more detailed instructions in LoopDocs for building Loop with Github Actions + FastLane, including troubleshooting and build errors. Please refer to [LoopDocs](https://loopkit.github.io/loopdocs/gh-actions/gh-first-time/#build-loop-using-github-actions) for more details.
+
+## Prerequisites
+
+* A [github account](https://github.com/signup). The free level comes with plenty of storage and free compute time to build Loop Follow, multiple times a day, if you wanted to.
+* A paid [Apple Developer account](https://developer.apple.com). You may be able to use the free version, but that has not been tested.
+* Some time. Set aside a couple of hours to perform the setup.
+
+
+## Generate App Store Connect API Key
+
+1. Sign in to the [Apple developer portal page](https://developer.apple.com/account/resources/certificates/list).
+1. Copy the team id from the upper right of the screen. Record this as your `TEAMID`.
+1. Go to the [App Store Connect](https://appstoreconnect.apple.com/access/api) interface, click the "Keys" tab, and create a new key with "Admin" access. Give it a name like "FastLane API Key".
+1. Record the key id; this will be used for `FASTLANE_KEY_ID`.
+1. Record the issuer id; this will be used for `FASTLANE_ISSUER_ID`.
+1. Download the API key itself, and open it in a text editor. The contents of this file will be used for `FASTLANE_KEY`. Copy the full text, including the "-----BEGIN PRIVATE KEY-----" and "-----END PRIVATE KEY-----" lines.
+
+## Setup Github
+1. Create a [new empty repository](https://github.com/new) titled `Match-Secrets`. It should be private.
+1. Fork https://github.com/jonfawcett/LoopFollow into your account. If you already have a fork of Loop Follow in GitHub, you can't make another one. You can continue to work with your existing fork, or delete that from GitHub and then and fork https://github.com/jonfawcett/LoopFollow.
+1. Create a [new personal access token](https://github.com/settings/tokens/new):
+    * Enter a name for your token. Something like "FastLane Access Token".
+    * 30 days is fine, or you can select longer if you'd like.
+    * Select the `repo` permission scope.
+    * Click "Generate token".
+    * Copy the token and record it. It will be used below as `GH_PAT`.
+1. In the forked Loop Follow repo, go to Settings -> Secrets -> 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:
+    * `TEAMID`
+    * `FASTLANE_KEY_ID`
+    * `FASTLANE_ISSUER_ID`
+    * `FASTLANE_KEY`
+    * `GH_PAT`
+    * `MATCH_PASSWORD` - just make up a password for this
+
+## Add Identifiers for Loop Follow App
+
+1. Click on the "Actions" tab of your Loop Follow repository.
+1. Select "Add Identifiers".
+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.
+
+
+## Create Loop Follow App in App Store Connect
+
+If you have created a Loop Follow app in App Store Connect before, you can skip this section as well.
+
+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".
+    * Select a name: this will have to be unique, so you may have to try a few different names here, but it will not be the name you see on your phone, so it's not that important.
+    * Select your primary language.
+    * Choose the bundle ID that matches `com.TEAMID.LoopFollow`, with TEAMID matching your team id.
+    * SKU can be anything; e.g. "123".
+    * Select "Full Access".
+1. Click Create
+
+You do not need to fill out the next form. That is for submitting to the app store.
+
+## Create Building Certficates
+
+1. Go back to the "Actions" tab of your Loop Follow repository in GitHub.
+1. Select "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.
+
+## Build Loop Follow!
+
+1. Click on the "Actions" tab of your Loop Follow repository.
+1. Select "Build LoopFollow". _Are you working on a previuos fork of Loop Follow and not seeing any GitHub workflows in the Actions tab? You may have to change the default branch so that it contains the .github/workflows files, or merge these changes into your default branch (typically 'master' or 'main')._
+1. Click "Run Workflow", select your branch, and tap the green button.
+1. You have some time now. Go enjoy a coffee. The build should take about 15 minutes.
+1. Your app should eventually appear on [App Store Connect](https://appstoreconnect.apple.com/apps).
+1. For each phone/person you would like to support Loop Follow on:
+    * Add them in [Users and Access](https://appstoreconnect.apple.com/access/users) on App Store Connect.
+    * Add them to your TestFlight Internal Testing group.