diff --git a/.gitea/workflows/testflight.yml b/.gitea/workflows/testflight.yml index 99a9ea1..dac918f 100644 --- a/.gitea/workflows/testflight.yml +++ b/.gitea/workflows/testflight.yml @@ -1,44 +1,39 @@ name: TestFlight on: - workflow_dispatch: push: tags: - - "release/ios/v*" + - "release/ios/v*.*.*" jobs: testflight: + name: Build and upload runs-on: macos-arm64 + timeout-minutes: 90 steps: - - name: Checkout + - name: Check out the release tag uses: actions/checkout@v4 - - name: Setup Ruby + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "3.1.7" + ruby-version: "3.3.11" bundler-cache: true working-directory: ios - name: Install XcodeGen run: command -v xcodegen >/dev/null 2>&1 || brew install xcodegen - - name: Upload to TestFlight + - name: Build and upload to TestFlight working-directory: ios env: - 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 }} + ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} + ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} + ASC_KEY: ${{ secrets.ASC_KEY }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }} MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} - SYBIL_BUILD_NUMBER: ${{ github.run_number }} + CI: "true" FASTLANE_SKIP_UPDATE_CHECK: "1" - FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120" - # act_runner does not propagate setup-ruby's PATH changes into later - # steps, so put the selected Ruby back on PATH or `bundle` resolves to - # the toolcache default and misses the gems installed above. - run: | - export PATH="/Users/runner/hostedtoolcache/Ruby/3.1.7/arm64/bin:${PATH}" - bundle exec fastlane ios beta + FASTLANE_HIDE_CHANGELOG: "1" + run: bundle exec fastlane ios beta diff --git a/ios/.env.example b/ios/.env.example index 4992f4a..5ca8e90 100644 --- a/ios/.env.example +++ b/ios/.env.example @@ -1,24 +1,5 @@ -FASTLANE_APP_IDENTIFIER=net.buzzert.sybil2 -FASTLANE_TEAM_ID=DQQH5H6GBD -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= - -# 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= -APP_STORE_CONNECT_API_KEY_CONTENT= -APP_STORE_CONNECT_API_KEY_CONTENT_BASE64=false - -# Optional deployment overrides. -SYBIL_BUILD_NUMBER= -SYBIL_VERSION_TAG= +ASC_KEY_ID= +ASC_ISSUER_ID= +ASC_KEY= +MATCH_PASSWORD= +MATCH_GIT_BASIC_AUTHORIZATION= diff --git a/ios/Gemfile b/ios/Gemfile index 7a118b4..0b11579 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -gem "fastlane" +gem "fastlane", "2.237.0" diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 4130921..f5ad616 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -1,46 +1,49 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.9) + CFPropertyList (3.0.8) abbrev (0.1.2) addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.2) - aws-partitions (1.1109.0) - aws-sdk-core (3.224.1) + aws-eventstream (1.4.0) + aws-partitions (1.1274.0) + aws-sdk-core (3.254.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 + bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.101.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.188.0) - aws-sdk-core (~> 3, >= 3.224.1) + aws-sdk-s3 (1.228.1) + aws-sdk-core (~> 3, >= 3.254.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - csv (3.3.5) + csv (3.3.6) declarative (0.0.20) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.109.0) + excon (1.6.0) + logger faraday (1.10.6) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -70,42 +73,45 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.1) - fastlane (2.230.0) - CFPropertyList (>= 2.3, < 4.0.0) - abbrev (~> 0.1.2) - addressable (>= 2.8, < 3.0.0) + fastlane (2.237.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) + aws-sdk-s3 (~> 1.197) babosa (>= 1.0.3, < 2.0.0) - base64 (~> 0.2.0) - bundler (>= 1.12.0, < 3.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) colored (~> 1.2) commander (~> 4.6) csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) + excon (>= 0.71.0, < 2.0.0) faraday (~> 1.0) faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) - fastlane-sirp (>= 1.0.0) + fastlane-sirp (>= 1.1.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-env (>= 1.6.0, < 2.3.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) json (< 3.0.0) - jwt (>= 2.1.0, < 3) + jwt (>= 2.10.3, < 4) logger (>= 1.6, < 2.0) mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) multipart-post (>= 2.0.0, < 3.0.0) - mutex_m (~> 0.3.0) + mutex_m (~> 0.3) naturally (~> 2.2) - nkf (~> 0.2.0) + nkf (~> 0.2) optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.5) @@ -120,41 +126,46 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) + google-apis-androidpublisher_v3 (0.106.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.65.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) + google-logging-utils (0.2.0) + googleauth (1.17.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) os (>= 0.9, < 2.0) + pstore (~> 0.1) signet (>= 0.16, < 2.a) highline (2.0.3) http-cookie (1.0.8) @@ -162,22 +173,24 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.7.6) - jwt (2.10.3) + json (2.21.1) + jwt (3.2.0) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.15.0) + multi_json (1.21.1) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) naturally (2.3.0) - nkf (0.2.0) + nkf (0.3.0) optparse (0.8.1) os (1.1.4) + ostruct (0.6.3) plist (3.7.2) - public_suffix (5.1.1) + pstore (0.2.1) + public_suffix (7.0.5) rake (13.4.2) representable (3.2.0) declarative (< 0.1.0) @@ -189,11 +202,10 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.4.1) security (0.1.5) - signet (0.18.0) + signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) + jwt (>= 1.5, < 4.0) simctl (1.6.10) CFPropertyList naturally @@ -206,15 +218,16 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unf (0.2.0) unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.27.0) + xcodeproj (1.28.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) + base64 claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.4.0) + nkf rexml (>= 3.3.6, < 4.0) xcpretty (0.4.1) rouge (~> 3.28.0) @@ -225,7 +238,7 @@ PLATFORMS ruby DEPENDENCIES - fastlane + fastlane (= 2.237.0) BUNDLED WITH 2.5.23 diff --git a/ios/fastlane/Appfile b/ios/fastlane/Appfile new file mode 100644 index 0000000..75ebb35 --- /dev/null +++ b/ios/fastlane/Appfile @@ -0,0 +1,2 @@ +app_identifier("net.buzzert.sybil2") +team_id("DQQH5H6GBD") diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 2357402..a3c3867 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -1,164 +1,43 @@ -require "shellwords" - default_platform(:ios) -APP_IDENTIFIER = "net.buzzert.sybil2" -SCHEME = "Sybil" -TEAM_ID = "DQQH5H6GBD" -PROFILE_NAME = "Sybil AppStore CI" -CI_KEYCHAIN_NAME = "sybil_ci_keychain" -CI_KEYCHAIN_PASSWORD = "sybil-ci-keychain-password" -CI_KEYCHAIN_DB_PATH = File.expand_path("~/Library/Keychains/#{CI_KEYCHAIN_NAME}-db") -IOS_ROOT = File.expand_path("..", __dir__) -PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj") -PROJECT_SPEC = File.join(IOS_ROOT, "project.yml") -APP_PROJECT_SPEC = File.join(IOS_ROOT, "Apps/Sybil/project.yml") - -def present?(value) - !value.to_s.strip.empty? -end - -def ci? - present?(ENV["CI"]) -end - -def release_version - tag = ENV["SYBIL_VERSION_TAG"] - tag = ENV["GITHUB_REF_NAME"] 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) - match = tag.to_s.match(%r{\Arelease/ios/v(\d+\.\d+\.\d+)\z}) - - unless match - UI.user_error!("Release tag must look like release/ios/v1.2.3; got #{tag.inspect}") - end - - match[1] -end - -# App Store Connect requires CFBundleVersion to be unique and strictly -# increasing app-wide (not just per marketing version), so we derive it from -# the monotonic CI run number rather than querying TestFlight (that query can -# lag behind builds still processing and hand back a colliding value). -def build_number - value = present?(ENV["SYBIL_BUILD_NUMBER"]) ? ENV["SYBIL_BUILD_NUMBER"] : ENV["GITHUB_RUN_NUMBER"] - - unless value.to_s.match?(/\A\d+\z/) - UI.user_error!("Build number must come from SYBIL_BUILD_NUMBER/GITHUB_RUN_NUMBER; got #{value.inspect}") - end - - value.to_i -end - -def stamp_marketing_version(version) - contents = File.read(APP_PROJECT_SPEC) - updated = contents.sub(/^(\s*MARKETING_VERSION:\s*).*/, "\\1\"#{version}\"") - - if updated == contents - UI.user_error!("Could not find MARKETING_VERSION in #{APP_PROJECT_SPEC}") - end - - File.write(APP_PROJECT_SPEC, updated) -end - platform :ios do - 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"), + desc "Build a release tag and upload it to TestFlight" + lane :beta do + setup_ci + + match(type: "appstore") + + tag = ENV.fetch("GITHUB_REF_NAME") + version = tag[%r{\Arelease/ios/v(\d+\.\d+\.\d+)\z}, 1] + UI.user_error!("Expected a tag in the form release/ios/vX.Y.Z; got #{tag.inspect}") unless version + + build_number = ENV.fetch("GITHUB_RUN_NUMBER") + UI.user_error!("GITHUB_RUN_NUMBER must be a positive integer") unless build_number.match?(/\A[1-9]\d*\z/) + + project_spec = "Apps/Sybil/project.yml" + contents = File.read(project_spec) + unless contents.match?(/^\s*MARKETING_VERSION:/) && contents.match?(/^\s*CURRENT_PROJECT_VERSION:/) + UI.user_error!("Could not find version settings in #{project_spec}") + end + contents.sub!(/^(\s*MARKETING_VERSION:\s*).*/, "\\1\"#{version}\"") + contents.sub!(/^(\s*CURRENT_PROJECT_VERSION:\s*).*/, "\\1#{build_number}") + File.write(project_spec, contents) + + sh("xcodegen") + + api_key = app_store_connect_api_key( + key_id: ENV.fetch("ASC_KEY_ID"), + issuer_id: ENV.fetch("ASC_ISSUER_ID"), + key_content: ENV.fetch("ASC_KEY"), is_key_content_base64: true ) - end - # CI signs headlessly, so match needs a fresh unlocked keychain to import - # into. codesign resolves identities through the user keychain *search list* - # (first match wins; the --keychain flag does not restrict the lookup), and - # other projects' keychains on this runner hold the same identity but are - # usually locked — so ours must come first. delete_keychain in the beta - # lane's ensure removes both the keychain and its search-list entry, which - # also keeps our (later locked) copy from shadowing those other projects. - private_lane :prepare_ci_keychain do - next unless ci? - - delete_keychain(name: CI_KEYCHAIN_NAME) if File.file?(CI_KEYCHAIN_DB_PATH) - create_keychain( - name: CI_KEYCHAIN_NAME, - password: CI_KEYCHAIN_PASSWORD, - unlock: true, - timeout: 3600, - add_to_search_list: false - ) - - others = sh("security list-keychains -d user", log: false) - .scan(/"([^"]+)"/) - .flatten - .reject { |path| path.include?(CI_KEYCHAIN_NAME) } - sh("security list-keychains -d user -s #{([CI_KEYCHAIN_DB_PATH] + others).shelljoin}") - - ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_NAME - ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD - end - - private_lane :sync_signing do |options| - match( - 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) - ) - 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 - prepare_ci_keychain - - api_key = app_store_api_key - - version = release_version - stamp_marketing_version(version) - sh("xcodegen", "--spec", PROJECT_SPEC) - - increment_version_number(version_number: version, xcodeproj: PROJECT_FILE) - increment_build_number(build_number: build_number, xcodeproj: PROJECT_FILE) - - sync_signing(api_key: api_key, readonly: true) - - build_app( - project: PROJECT_FILE, - scheme: SCHEME, - export_method: "app-store", - xcargs: [ - "DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}", - "CODE_SIGN_STYLE=Manual", - "CODE_SIGN_IDENTITY=Apple\\ Distribution", - "PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}" - ].join(" "), - export_options: { - signingStyle: "manual", - teamID: TEAM_ID, - provisioningProfiles: { - APP_IDENTIFIER => PROFILE_NAME - } - } - ) + build_app(scheme: "Sybil") upload_to_testflight( api_key: api_key, - skip_waiting_for_build_processing: true + skip_waiting_for_build_processing: true, + uses_non_exempt_encryption: false ) - ensure - delete_keychain(name: CI_KEYCHAIN_NAME) if ci? && File.file?(CI_KEYCHAIN_DB_PATH) end end diff --git a/ios/fastlane/Matchfile b/ios/fastlane/Matchfile new file mode 100644 index 0000000..1de64e5 --- /dev/null +++ b/ios/fastlane/Matchfile @@ -0,0 +1,7 @@ +git_url("https://code.buzzert.dev/buzzert/fastlane-match.git") +storage_mode("git") +type("appstore") + +app_identifier(["net.buzzert.sybil2"]) +team_id("DQQH5H6GBD") +profile_name("Sybil AppStore CI")