From 4e7ea7ab6878e7c8555a93e086278dff351abe77 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 26 Jun 2026 00:28:27 -0700 Subject: [PATCH] ios: get TestFlight CI signing working Replace the old testflight-release workflow with a single `testflight.yml` Gitea Actions workflow and rework the fastlane `beta` lane: match-based app-store signing into a disposable CI keychain, XcodeGen project generation, version/build-number bumping, and upload to TestFlight. Pin Ruby to 3.1.7 for the runner. Squashes the iterative CI signing debugging history into one commit. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/testflight-release.yml | 187 --------------- .gitea/workflows/testflight.yml | 70 ++++++ .gitignore | 3 +- ios/.env.example | 12 +- ios/Apps/Sybil/project.yml | 6 + ios/Gemfile | 2 +- ios/Gemfile.lock | 2 +- ios/fastlane/Appfile | 9 - ios/fastlane/CI.md | 32 --- ios/fastlane/Fastfile | 300 +++++++++++++----------- ios/fastlane/README.md | 40 ---- 11 files changed, 252 insertions(+), 411 deletions(-) delete mode 100644 .gitea/workflows/testflight-release.yml create mode 100644 .gitea/workflows/testflight.yml delete mode 100644 ios/fastlane/Appfile delete mode 100644 ios/fastlane/CI.md delete mode 100644 ios/fastlane/README.md diff --git a/.gitea/workflows/testflight-release.yml b/.gitea/workflows/testflight-release.yml deleted file mode 100644 index e930a4d..0000000 --- a/.gitea/workflows/testflight-release.yml +++ /dev/null @@ -1,187 +0,0 @@ -name: TestFlight Release - -on: - push: - tags: - - "release/v*.*.*" - -permissions: - contents: write - -jobs: - testflight: - runs-on: xcode - - defaults: - run: - shell: bash - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Validate release tag - run: | - set -euo pipefail - - tag_name="${GITHUB_REF#refs/tags/}" - if [[ ! "$tag_name" =~ ^release/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Release tag must match release/vN.N.N; got ${tag_name}" >&2 - exit 1 - fi - - release_version="${tag_name#release/v}" - { - echo "TAG_NAME=${tag_name}" - echo "RELEASE_VERSION=${release_version}" - } >> "${GITHUB_ENV}" - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: "3.3" - - - name: Install Ruby gems - working-directory: ios - run: bundle install - - - name: Install release tools - run: | - set -euo pipefail - - missing_tools=() - for tool in xcodegen jq; do - if ! command -v "${tool}" >/dev/null 2>&1; then - missing_tools+=("${tool}") - fi - done - - if [[ "${#missing_tools[@]}" -eq 0 ]]; then - exit 0 - fi - - if ! command -v brew >/dev/null 2>&1; then - echo "Missing required tools: ${missing_tools[*]}; Homebrew is not available to install them" >&2 - exit 1 - fi - - brew install "${missing_tools[@]}" - - - name: Import code signing certificates - uses: Apple-Actions/import-codesign-certs@v3 - with: - p12-file-base64: ${{ secrets.APPSTORE_CERTIFICATES_FILE_BASE64 }} - p12-password: ${{ secrets.APPSTORE_CERTIFICATES_PASSWORD }} - - - name: Create fastlane environment - working-directory: ios - env: - FASTLANE_USER: ${{ secrets.FASTLANE_USER }} - FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} - run: | - set -euo pipefail - - : "${FASTLANE_USER:?FASTLANE_USER secret is required}" - : "${FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD:?FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD secret is required}" - - { - printf 'FASTLANE_USER=%s\n' "${FASTLANE_USER}" - printf 'FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=%s\n' "${FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD}" - printf 'FASTLANE_SKIP_UPDATE_CHECK=1\n' - printf 'FASTLANE_HIDE_CHANGELOG=1\n' - } > .env - - - name: Build and upload to TestFlight - working-directory: ios - env: - FASTLANE_DONT_STORE_PASSWORD: "1" - run: | - set -euo pipefail - - SYBIL_VERSION_TAG="${TAG_NAME}" bundle exec fastlane ios beta - - - name: Locate IPA - run: | - set -euo pipefail - - ipa_path="$(find ios/build/fastlane -maxdepth 1 -type f -name '*.ipa' -print | sort | tail -n 1)" - if [[ -z "${ipa_path}" ]]; then - echo "No IPA found under ios/build/fastlane" >&2 - exit 1 - fi - - { - echo "IPA_PATH=${ipa_path}" - echo "IPA_NAME=$(basename "${ipa_path}")" - } >> "${GITHUB_ENV}" - - - name: Publish Gitea release asset - env: - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - RELEASE_API_URL: ${{ github.api_url }} - RELEASE_REPOSITORY: ${{ github.repository }} - RELEASE_SHA: ${{ github.sha }} - run: | - set -euo pipefail - - : "${GITEA_TOKEN:?GITEA_TOKEN is required}" - - api_url="${RELEASE_API_URL:-https://code.buzzert.dev/api/v1}" - repository="${RELEASE_REPOSITORY:-buzzert/Sybil-2}" - sha="${RELEASE_SHA:-${GITHUB_SHA:-}}" - release_name="Sybil v${RELEASE_VERSION}" - release_body="Automated TestFlight release for ${TAG_NAME}." - release_payload="$(jq -nc \ - --arg tag "${TAG_NAME}" \ - --arg name "${release_name}" \ - --arg body "${release_body}" \ - --arg target "${sha}" \ - '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false} + - (if $target == "" then {} else {target_commitish: $target} end)')" - - response_file="$(mktemp)" - status="$(curl -sS -o "${response_file}" -w "%{http_code}" \ - -X POST "${api_url}/repos/${repository}/releases" \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - --data "${release_payload}")" - - if [[ "${status}" == "201" ]]; then - release_id="$(jq -r '.id' "${response_file}")" - elif [[ "${status}" == "409" ]]; then - release_id="$(curl -fsS \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${api_url}/repos/${repository}/releases?limit=100" | - jq -r --arg tag "${TAG_NAME}" '.[] | select(.tag_name == $tag) | .id' | - head -n 1)" - else - cat "${response_file}" >&2 - exit 1 - fi - - if [[ -z "${release_id}" || "${release_id}" == "null" ]]; then - echo "Could not resolve Gitea release id for ${TAG_NAME}" >&2 - exit 1 - fi - - existing_asset_id="$(curl -fsS \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${api_url}/repos/${repository}/releases/${release_id}/assets" | - jq -r --arg name "${IPA_NAME}" '.[] | select(.name == $name) | .id' | - head -n 1)" - - if [[ -n "${existing_asset_id}" && "${existing_asset_id}" != "null" ]]; then - curl -fsS -X DELETE \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${api_url}/repos/${repository}/releases/${release_id}/assets/${existing_asset_id}" - fi - - asset_name="$(jq -rn --arg value "${IPA_NAME}" '$value | @uri')" - curl -fsS -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -F "attachment=@${IPA_PATH}" \ - "${api_url}/repos/${repository}/releases/${release_id}/assets?name=${asset_name}" >/dev/null - - echo "Published ${IPA_NAME} to ${release_name}" diff --git a/.gitea/workflows/testflight.yml b/.gitea/workflows/testflight.yml new file mode 100644 index 0000000..eba1396 --- /dev/null +++ b/.gitea/workflows/testflight.yml @@ -0,0 +1,70 @@ +name: TestFlight + +on: + workflow_dispatch: + push: + tags: + - "v*" + +jobs: + testflight: + runs-on: xcode + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.1.7" + bundler-cache: true + working-directory: ios + + - name: Install XcodeGen + run: | + set -euo pipefail + if ! command -v xcodegen >/dev/null 2>&1; then + brew install xcodegen + fi + + - name: Prepare Runner Keychain + env: + HOME: /var/lib/act_runner + run: | + set -euo pipefail + mkdir -p "${HOME}/Library/Keychains" + + login_keychain="${HOME}/Library/Keychains/login.keychain" + if [ ! -f "${login_keychain}-db" ]; then + security create-keychain -p "" "${login_keychain}" + fi + + security unlock-keychain -p "" "${login_keychain}" 2>/dev/null || \ + security unlock-keychain -p "sybil-ci-keychain-password" "${login_keychain}" 2>/dev/null || true + security default-keychain -d user -s "${login_keychain}" + security list-keychains -d user -s "${login_keychain}-db" + security delete-keychain "${HOME}/Library/Keychains/sybil_ci_keychain" >/dev/null 2>&1 || true + rm -f "${HOME}/Library/Keychains/sybil_ci_keychain" "${HOME}/Library/Keychains/sybil_ci_keychain-db" + + - name: Upload to TestFlight + working-directory: ios + env: + HOME: /var/lib/act_runner + APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} + APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} + APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} + MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} + FASTLANE_SKIP_UPDATE_CHECK: "1" + FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120" + run: | + export PATH="/Users/runner/hostedtoolcache/Ruby/3.1.7/arm64/bin:${PATH}" + ruby --version + bundle exec fastlane ios beta diff --git a/.gitignore b/.gitignore index 30bd623..60541b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env - +ios/fastlane/README.md +ios/fastlane/report.xml diff --git a/ios/.env.example b/ios/.env.example index 51ccba4..4992f4a 100644 --- a/ios/.env.example +++ b/ios/.env.example @@ -1,14 +1,18 @@ FASTLANE_APP_IDENTIFIER=net.buzzert.sybil2 FASTLANE_TEAM_ID=DQQH5H6GBD -FASTLANE_USER=you@example.com -FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx FASTLANE_SKIP_UPDATE_CHECK=1 FASTLANE_HIDE_CHANGELOG=1 SYBIL_APP_STORE_APPLE_ID=6759442828 SYBIL_PROVIDER_PUBLIC_ID=c043d167-ad88-4036-84ea-76c223f1b1b2 +SYBIL_PROVISIONING_PROFILE_SPECIFIER=Sybil AppStore CI +SYBIL_PROVISIONING_PROFILE_UUID= +SYBIL_CODE_SIGN_IDENTITY=Apple Distribution: James Magahern (DQQH5H6GBD) +SYBIL_XCODE_CODE_SIGN_IDENTITY=6B74B268C4761720FB2051D01D8BB3E47B55D9F5 +SYBIL_EXPORT_SIGNING_CERTIFICATE=Apple Distribution +SYBIL_SIGNING_CERTIFICATE_ID= +SYBIL_SIGNING_KEYCHAIN= -# Optional App Store Connect API key settings for non-interactive upload and -# TestFlight build-number lookup. +# App Store Connect API key settings for TestFlight upload and signing setup. APP_STORE_CONNECT_API_KEY_ID= APP_STORE_CONNECT_API_ISSUER_ID= APP_STORE_CONNECT_API_KEY_PATH= diff --git a/ios/Apps/Sybil/project.yml b/ios/Apps/Sybil/project.yml index 4bfc36b..9c19df9 100644 --- a/ios/Apps/Sybil/project.yml +++ b/ios/Apps/Sybil/project.yml @@ -32,6 +32,12 @@ targets: INFOPLIST_KEY_UILaunchScreen_Generation: YES INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: UIInterfaceOrientationPortrait INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + configs: + Release: + CODE_SIGN_STYLE: Manual + CODE_SIGN_IDENTITY: Apple Distribution + "CODE_SIGN_IDENTITY[sdk=iphoneos*]": Apple Distribution + PROVISIONING_PROFILE_SPECIFIER: Sybil AppStore CI schemes: Sybil: diff --git a/ios/Gemfile b/ios/Gemfile index 81a4d85..7a118b4 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -gem "fastlane", "~> 2.227" +gem "fastlane" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 5f2b8f2..4130921 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -225,7 +225,7 @@ PLATFORMS ruby DEPENDENCIES - fastlane (~> 2.227) + fastlane BUNDLED WITH 2.5.23 diff --git a/ios/fastlane/Appfile b/ios/fastlane/Appfile deleted file mode 100644 index 6941f1b..0000000 --- a/ios/fastlane/Appfile +++ /dev/null @@ -1,9 +0,0 @@ -require "dotenv" - -Dotenv.load(File.expand_path("../.env", __dir__)) - -app_identifier(ENV.fetch("FASTLANE_APP_IDENTIFIER", "net.buzzert.sybil2")) -team_id(ENV.fetch("FASTLANE_TEAM_ID", "DQQH5H6GBD")) - -apple_id(ENV["FASTLANE_USER"]) if ENV["FASTLANE_USER"].to_s.strip.length.positive? -itc_team_id(ENV["FASTLANE_ITC_TEAM_ID"]) if ENV["FASTLANE_ITC_TEAM_ID"].to_s.strip.length.positive? diff --git a/ios/fastlane/CI.md b/ios/fastlane/CI.md deleted file mode 100644 index b1f2fcb..0000000 --- a/ios/fastlane/CI.md +++ /dev/null @@ -1,32 +0,0 @@ -# TestFlight Release CI - -Gitea Actions publishes iOS releases from tags that match: - -```sh -release/vN.N.N -``` - -For example: - -```sh -git tag release/v1.10.0 -git push origin release/v1.10.0 -``` - -The release job runs on the `xcode` runner label, imports the signing p12 with -`Apple-Actions/import-codesign-certs`, builds and uploads the app with fastlane, -then creates or updates the matching Gitea release with the generated IPA as an -asset. - -Required repository secrets: - -```text -APPSTORE_CERTIFICATES_FILE_BASE64 -APPSTORE_CERTIFICATES_PASSWORD -FASTLANE_USER -FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD -``` - -The workflow uses Gitea's built-in `GITEA_TOKEN` for release creation and asset -upload, with `contents: write` permissions. In Gitea this covers release asset -publication. diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index f344f1e..162f322 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -1,177 +1,205 @@ -require "dotenv" -require "open3" +require "fileutils" require "shellwords" -require "yaml" - -Dotenv.load(File.expand_path("../.env", __dir__)) default_platform(:ios) -APP_IDENTIFIER = ENV.fetch("FASTLANE_APP_IDENTIFIER", "net.buzzert.sybil2") -TEAM_ID = ENV.fetch("FASTLANE_TEAM_ID", "DQQH5H6GBD") -APP_STORE_APPLE_ID = ENV.fetch("SYBIL_APP_STORE_APPLE_ID", "6759442828") -PROVIDER_PUBLIC_ID = ENV.fetch("SYBIL_PROVIDER_PUBLIC_ID", "c043d167-ad88-4036-84ea-76c223f1b1b2") +APP_IDENTIFIER = "net.buzzert.sybil2" +SCHEME = "Sybil" +TEAM_ID = "DQQH5H6GBD" +PROFILE_NAME = "Sybil AppStore CI" +SIGNING_IDENTITY = "Apple Distribution: James Magahern (DQQH5H6GBD)" +CI_KEYCHAIN_NAME = "sybil_ci_keychain" +CI_KEYCHAIN_PASSWORD = "sybil-ci-keychain-password" IOS_ROOT = File.expand_path("..", __dir__) PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj") PROJECT_SPEC = File.join(IOS_ROOT, "project.yml") -APP_SPEC = File.join(IOS_ROOT, "Apps/Sybil/project.yml") -SCHEME = "Sybil" -TARGET = "SybilApp" +CI_KEYCHAIN_PATH = File.join(File.expand_path("~/Library/Keychains"), CI_KEYCHAIN_NAME) +CI_KEYCHAIN_DB_PATH = "#{CI_KEYCHAIN_PATH}-db" +LOGIN_KEYCHAIN_PATH = File.expand_path("~/Library/Keychains/login.keychain") +LOGIN_KEYCHAIN_DB_PATH = "#{LOGIN_KEYCHAIN_PATH}-db" def present?(value) !value.to_s.strip.empty? end -def capture(command) - stdout, stderr, status = Open3.capture3(command) - return stdout.strip if status.success? +def release_version + tag = ENV["SYBIL_VERSION_TAG"].to_s + tag = ENV["GITHUB_REF_NAME"].to_s if !present?(tag) + tag = ENV["GITHUB_REF"].to_s.sub(%r{\Arefs/tags/}, "") if !present?(tag) + tag = sh("git describe --tags --abbrev=0").strip if !present?(tag) + version = tag.sub(%r{\Arelease/}, "").sub(/\Av/, "") - UI.user_error!("Command failed: #{command}\n#{stderr.strip}") -end - -def app_project_settings - YAML.safe_load(File.read(APP_SPEC)).fetch("targets").fetch(TARGET).fetch("settings").fetch("base") -end - -def local_marketing_version - app_project_settings.fetch("MARKETING_VERSION").to_s -end - -def local_build_number - app_project_settings.fetch("CURRENT_PROJECT_VERSION").to_i -end - -def normalize_version_tag(tag) - version = tag.to_s.strip.sub(%r{\Arelease/}, "").sub(/\Av/, "") unless version.match?(/\A\d+\.\d+\.\d+\z/) - UI.user_error!("Release tag #{tag.inspect} must look like release/v1.10.0") + UI.user_error!("Release tag must look like v1.2.3; got #{tag.inspect}") end + version end -def release_version - tag = ENV["SYBIL_VERSION_TAG"] - tag = capture("git describe --tags --abbrev=0") unless present?(tag) - normalize_version_tag(tag) +def ci? + present?(ENV["CI"]) end -def xcode_build_setting(key, value) - "#{key}=#{value.to_s.shellescape}" -end - -def app_store_connect_key_options - key_id = ENV["APP_STORE_CONNECT_API_KEY_ID"] - issuer_id = ENV["APP_STORE_CONNECT_API_ISSUER_ID"] - return nil unless present?(key_id) && present?(issuer_id) - - key_path = ENV["APP_STORE_CONNECT_API_KEY_PATH"] - key_content = ENV["APP_STORE_CONNECT_API_KEY_CONTENT"] - if present?(key_path) - { - key_id: key_id, - issuer_id: issuer_id, - key_filepath: key_path - } - elsif present?(key_content) - { - key_id: key_id, - issuer_id: issuer_id, - key_content: key_content, - is_key_content_base64: ENV["APP_STORE_CONNECT_API_KEY_CONTENT_BASE64"].to_s == "true" - } - end +def ci_keychain_path + File.file?(CI_KEYCHAIN_DB_PATH) ? CI_KEYCHAIN_DB_PATH : CI_KEYCHAIN_PATH end platform :ios do - desc "Show the version Fastlane will stamp into the next TestFlight archive" - lane :version do - UI.message("Git tag version: #{release_version}") - UI.message("Checked-in app version: #{local_marketing_version}") - UI.message("Checked-in build number: #{local_build_number}") + private_lane :app_store_api_key do + app_store_connect_api_key( + key_id: ENV.fetch("APP_STORE_CONNECT_KEY_ID"), + issuer_id: ENV.fetch("APP_STORE_CONNECT_ISSUER_ID"), + key_content: ENV.fetch("APP_STORE_CONNECT_KEY_CONTENT"), + is_key_content_base64: true + ) end - desc "Build Sybil and upload it to TestFlight" + private_lane :setup_ci_signing do + next unless ci? + + FileUtils.mkdir_p(File.dirname(CI_KEYCHAIN_PATH)) + sh("security delete-keychain #{CI_KEYCHAIN_PATH.shellescape} || true", log: false) + FileUtils.rm_f(CI_KEYCHAIN_PATH) + FileUtils.rm_f(CI_KEYCHAIN_DB_PATH) + + create_keychain( + path: CI_KEYCHAIN_PATH, + password: CI_KEYCHAIN_PASSWORD, + default_keychain: false, + unlock: true, + timeout: 3600, + lock_when_sleeps: true, + add_to_search_list: false + ) + + sh("security default-keychain -d user -s #{CI_KEYCHAIN_PATH.shellescape}", log: false) + sh("security list-keychains -d user -s #{ci_keychain_path.shellescape}", log: false) + sh("security list-keychains -d dynamic -s #{ci_keychain_path.shellescape} || true", log: false) + sh("security list-keychains -d common -s #{ci_keychain_path.shellescape} || true", log: false) + + ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_PATH + ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD + ENV["MATCH_READONLY"] = "true" + end + + private_lane :cleanup_ci_signing do + next unless ci? + + if File.file?(LOGIN_KEYCHAIN_DB_PATH) || File.file?(LOGIN_KEYCHAIN_PATH) + sh("security default-keychain -d user -s #{LOGIN_KEYCHAIN_PATH.shellescape} || true", log: false) + sh("security list-keychains -d user -s #{LOGIN_KEYCHAIN_DB_PATH.shellescape} || true", log: false) + end + sh("security delete-keychain #{ci_keychain_path.shellescape} || true", log: false) + FileUtils.rm_f(CI_KEYCHAIN_PATH) + FileUtils.rm_f(CI_KEYCHAIN_DB_PATH) + rescue => error + UI.message("Unable to delete temporary CI keychain: #{error.message}") + ensure + ENV.delete("MATCH_KEYCHAIN_NAME") + ENV.delete("MATCH_KEYCHAIN_PASSWORD") + ENV.delete("MATCH_READONLY") + end + + private_lane :sync_signing do |options| + match_options = { + type: "appstore", + readonly: options.fetch(:readonly), + app_identifier: APP_IDENTIFIER, + team_id: TEAM_ID, + profile_name: PROFILE_NAME, + git_url: ENV.fetch("MATCH_GIT_URL"), + git_branch: "master", + git_full_name: "Sybil Release Bot", + git_user_email: "james.magahern@me.com", + api_key: options.fetch(:api_key) + } + match_options[:keychain_name] = ENV["MATCH_KEYCHAIN_NAME"] if present?(ENV["MATCH_KEYCHAIN_NAME"]) + match_options[:keychain_password] = ENV["MATCH_KEYCHAIN_PASSWORD"] if ENV.key?("MATCH_KEYCHAIN_PASSWORD") + + match(match_options) + end + + private_lane :verify_ci_signing do + next unless ci? + + if File.file?(ci_keychain_path) + password = ENV.fetch("MATCH_KEYCHAIN_PASSWORD", "") + sh("security unlock-keychain -p #{password.shellescape} #{ci_keychain_path.shellescape}", log: false) + sh("security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k #{password.shellescape} #{ci_keychain_path.shellescape}", log: false) + end + + identities = sh("security find-identity -v -p codesigning #{ci_keychain_path.shellescape}", log: false) + UI.message(identities) + + unless identities.include?(SIGNING_IDENTITY) + UI.user_error!("The CI keychain search list does not contain #{SIGNING_IDENTITY}") + end + end + + desc "Create or update match signing assets" + lane :setup_signing do + sync_signing(api_key: app_store_api_key, readonly: false) + end + + desc "Build and upload to TestFlight" lane :beta do - version = release_version - build_number = ENV["SYBIL_BUILD_NUMBER"].to_s - api_key = nil + setup_ci_signing - if app_store_connect_key_options - api_key = app_store_connect_api_key(app_store_connect_key_options) - end - - unless present?(build_number) - build_number = (local_build_number + 1).to_s - - if api_key - begin - latest = latest_testflight_build_number( - app_identifier: APP_IDENTIFIER, - version: version, - api_key: api_key, - initial_build_number: local_build_number - ).to_i - build_number = [latest + 1, local_build_number + 1].max.to_s - rescue StandardError => e - UI.important("Could not look up TestFlight build number: #{e.message}") - UI.important("Using checked-in build number + 1: #{build_number}") - end - end - end - - UI.user_error!("Build number must be a positive integer") unless build_number.match?(/\A[1-9]\d*\z/) + api_key = app_store_api_key sh("xcodegen --spec #{PROJECT_SPEC.shellescape}") - xcode_args = [ - "-allowProvisioningUpdates", - xcode_build_setting("MARKETING_VERSION", version), - xcode_build_setting("CURRENT_PROJECT_VERSION", build_number) - ].join(" ") + increment_version_number( + version_number: release_version, + xcodeproj: PROJECT_FILE + ) - ipa_path = build_app( + latest_build_number = latest_testflight_build_number( + app_identifier: APP_IDENTIFIER, + api_key: api_key, + initial_build_number: 0 + ) + + increment_build_number( + build_number: latest_build_number + 1, + xcodeproj: PROJECT_FILE + ) + + sync_signing(api_key: api_key, readonly: true) + verify_ci_signing + + xcargs = [ + "DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}", + "CODE_SIGN_STYLE=Manual", + "CODE_SIGN_IDENTITY=Apple\\ Distribution", + "PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}" + ] + + if ci? + xcargs << "CODE_SIGN_KEYCHAIN=#{ci_keychain_path.shellescape}" + xcargs << "OTHER_CODE_SIGN_FLAGS=#{("--keychain #{ci_keychain_path}").shellescape}" + end + + build_app( project: PROJECT_FILE, scheme: SCHEME, - clean: true, - sdk: "iphoneos", export_method: "app-store", - output_directory: File.join(IOS_ROOT, "build/fastlane"), - output_name: "Sybil-#{version}-#{build_number}.ipa", - xcargs: xcode_args, - export_xcargs: "-allowProvisioningUpdates", + codesigning_identity: "Apple Distribution", + xcargs: xcargs.join(" "), export_options: { - method: "app-store-connect", - destination: "export", - signingStyle: "automatic", + signingStyle: "manual", teamID: TEAM_ID, - manageAppVersionAndBuildNumber: false, - uploadSymbols: true, - stripSwiftSymbols: true + provisioningProfiles: { + APP_IDENTIFIER => PROFILE_NAME + } } ) - ipa_path ||= lane_context[SharedValues::IPA_OUTPUT_PATH] - UI.user_error!("IPA export failed; no IPA path was returned") unless present?(ipa_path) && File.exist?(ipa_path) - - password = ENV["FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD"] - UI.user_error!("FASTLANE_USER is required for altool upload") unless present?(ENV["FASTLANE_USER"]) - UI.user_error!("FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD is required for altool upload") unless present?(password) - UI.user_error!("SYBIL_APP_STORE_APPLE_ID is required for altool upload") unless present?(APP_STORE_APPLE_ID) - UI.user_error!("SYBIL_PROVIDER_PUBLIC_ID is required for altool upload") unless present?(PROVIDER_PUBLIC_ID) - - ENV["ITMS_TRANSPORTER_PASSWORD"] = password - sh([ - "xcrun altool", - "--upload-package #{ipa_path.shellescape}", - "--platform ios", - "--apple-id #{APP_STORE_APPLE_ID.shellescape}", - "--bundle-id #{APP_IDENTIFIER.shellescape}", - "--bundle-version #{build_number.shellescape}", - "--bundle-short-version-string #{version.shellescape}", - "--provider-public-id #{PROVIDER_PUBLIC_ID.shellescape}", - "--username #{ENV.fetch("FASTLANE_USER").shellescape}", - "--password @env:ITMS_TRANSPORTER_PASSWORD", - "--show-progress" - ].join(" ")) + upload_to_testflight( + api_key: api_key, + skip_waiting_for_build_processing: true + ) + ensure + cleanup_ci_signing end end diff --git a/ios/fastlane/README.md b/ios/fastlane/README.md deleted file mode 100644 index 3679069..0000000 --- a/ios/fastlane/README.md +++ /dev/null @@ -1,40 +0,0 @@ -fastlane documentation ----- - -# Installation - -Make sure you have the latest version of the Xcode command line tools installed: - -```sh -xcode-select --install -``` - -For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) - -# Available Actions - -## iOS - -### ios version - -```sh -[bundle exec] fastlane ios version -``` - -Show the version Fastlane will stamp into the next TestFlight archive - -### ios beta - -```sh -[bundle exec] fastlane ios beta -``` - -Build Sybil and upload it to TestFlight - ----- - -This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. - -More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). - -The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).