diff --git a/.env.example b/.env.example index 083b2d9..ee769f6 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -APP_STORE_CONNECT_KEY_ID= -APP_STORE_CONNECT_ISSUER_ID= -APP_STORE_CONNECT_KEY_CONTENT= +APP_STORE_CONNECT_API_KEY_ID= +APP_STORE_CONNECT_API_ISSUER_ID= +APP_STORE_CONNECT_API_KEY= MATCH_PASSWORD= MATCH_GIT_BASIC_AUTHORIZATION= diff --git a/.gitea/workflows/testflight.yml b/.gitea/workflows/testflight.yml index 120e811..c43676d 100644 --- a/.gitea/workflows/testflight.yml +++ b/.gitea/workflows/testflight.yml @@ -10,39 +10,25 @@ jobs: name: Build and upload runs-on: macos-arm64 timeout-minutes: 90 - defaults: - run: - shell: bash steps: - name: Check out the release tag uses: actions/checkout@v4 - - name: Verify the Apple build toolchain - run: | - export PATH="/Users/runner/hostedtoolcache/Ruby/3.3.11/arm64/bin:${PATH}" - ruby --version - bundle --version - xcodebuild -version - - - name: Install Fastlane - run: | - export PATH="/Users/runner/hostedtoolcache/Ruby/3.3.11/arm64/bin:${PATH}" - bundle config set --local path "$HOME/.bundle/xion-control-panel" - bundle install --jobs 4 --retry 3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3.11" + bundler-cache: true - name: Build and upload to TestFlight 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 }} + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} + APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} + APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }} - RELEASE_TAG: ${{ github.ref_name }} - GITEA_BUILD_NUMBER: ${{ github.run_number }} + CI: "true" FASTLANE_SKIP_UPDATE_CHECK: "1" FASTLANE_HIDE_CHANGELOG: "1" - FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120" - run: | - export PATH="/Users/runner/hostedtoolcache/Ruby/3.3.11/arm64/bin:${PATH}" - bundle exec fastlane ios release + run: bundle exec fastlane ios beta diff --git a/README.md b/README.md index 128cf3c..1828c15 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ git push origin release/v2.0.0 The workflow uses the `macos-arm64` runner and Fastlane Match. It expects these repository Actions secrets: -- `APP_STORE_CONNECT_KEY_ID` -- `APP_STORE_CONNECT_ISSUER_ID` -- `APP_STORE_CONNECT_KEY_CONTENT` +- `APP_STORE_CONNECT_API_KEY_ID` +- `APP_STORE_CONNECT_API_ISSUER_ID` +- `APP_STORE_CONNECT_API_KEY` - `MATCH_PASSWORD` - `MATCH_GIT_BASIC_AUTHORIZATION` diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a37a8e9..e9fd4a3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,170 +1,40 @@ -require "shellwords" - default_platform(:ios) -APP_IDENTIFIER = "net.buzzert.XaibatsuControlPanel" -TEAM_ID = "DQQH5H6GBD" -ROOT_DIR = File.expand_path("..", __dir__) -PROJECT = File.join(ROOT_DIR, "XIONControlPanel.xcodeproj") -SCHEME = "XIONControlPanel" -INFO_PLIST = "XIONControlPanel/SupportingFiles/Info.plist" -ARCHIVE_PATH = File.join(ROOT_DIR, "build/XaibatsuControlPanel.xcarchive") -IPA_PATH = File.join(ROOT_DIR, "build/XaibatsuControlPanel.ipa") -PROFILE_NAME = "match AppStore #{APP_IDENTIFIER}" -CI_KEYCHAIN_NAME = "xaibatsu_control_panel_ci_keychain" -CI_KEYCHAIN_PASSWORD = "xaibatsu-control-panel-ci-keychain-password" -CI_KEYCHAIN_DB_PATH = File.expand_path("~/Library/Keychains/#{CI_KEYCHAIN_NAME}-db") - -def present?(value) - !value.to_s.strip.empty? -end - -def ci? - present?(ENV["CI"]) -end - -def read_plist_value(path, key) - Fastlane::Actions.sh( - "/usr/bin/plutil -extract #{key.shellescape} raw #{path.shellescape}", - log: false - ).strip -end - -def app_store_connect_key - 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, - duration: 1_200, - in_house: false - ) -end - -def release_metadata - tag = ENV["RELEASE_TAG"] || ENV["GITHUB_REF_NAME"] - match = tag&.match(%r{\Arelease/v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\z}) - - UI.user_error!("Expected a release tag in the form release/vX.X.X; got #{tag.inspect}") unless match - - build_number = ENV["GITEA_BUILD_NUMBER"] || ENV["GITHUB_RUN_NUMBER"] - unless build_number&.match?(/\A[1-9]\d*\z/) - UI.user_error!("The Gitea run number must be a positive integer; got #{build_number.inspect}") - end - - { - version: "#{match[1]}.#{match[2]}.#{match[3]}", - build_number: build_number - } -end - platform :ios do - # Keep this app's unlocked signing identity first in the shared runner's - # keychain search list. Other projects use the same distribution identity, - # and their locked keychains can otherwise shadow this one. - 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: 3_600, - add_to_search_list: false - ) - - other_keychains = 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] + other_keychains).shelljoin}") - - ENV["MATCH_KEYCHAIN_NAME"] = CI_KEYCHAIN_NAME - ENV["MATCH_KEYCHAIN_PASSWORD"] = CI_KEYCHAIN_PASSWORD - end - - desc "Create or renew the App Store provisioning profile in the Match repository" - lane :prepare_signing do - match( - type: "appstore", - app_identifier: APP_IDENTIFIER, - team_id: TEAM_ID, - api_key: app_store_connect_key, - readonly: false - ) - end - desc "Build a release tag and upload it to TestFlight" - lane :release do |options| - metadata = release_metadata - api_key = app_store_connect_key + lane :beta do + setup_ci - prepare_ci_keychain + match(type: "appstore") - match( - type: "appstore", - app_identifier: APP_IDENTIFIER, - team_id: TEAM_ID, - api_key: api_key, - readonly: true - ) + tag = ENV.fetch("GITHUB_REF_NAME") + version = tag[%r{\Arelease/v(\d+\.\d+\.\d+)\z}, 1] + UI.user_error!("Expected a tag in the form release/vX.X.X; 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/) update_info_plist( - plist_path: INFO_PLIST, + plist_path: "XIONControlPanel/SupportingFiles/Info.plist", block: proc do |plist| - plist["CFBundleShortVersionString"] = metadata[:version] - plist["CFBundleVersion"] = metadata[:build_number] + plist["CFBundleShortVersionString"] = version + plist["CFBundleVersion"] = build_number end ) - stamped_version = get_info_plist_value(path: INFO_PLIST, key: "CFBundleShortVersionString").to_s - stamped_build = get_info_plist_value(path: INFO_PLIST, key: "CFBundleVersion").to_s - UI.user_error!("Failed to stamp the release version") unless stamped_version == metadata[:version] - UI.user_error!("Failed to stamp the Gitea build number") unless stamped_build == metadata[:build_number] - - build_app( - project: PROJECT, - scheme: SCHEME, - configuration: "Release", - clean: true, - archive_path: ARCHIVE_PATH, - output_directory: File.dirname(IPA_PATH), - output_name: File.basename(IPA_PATH), - export_method: "app-store", - export_options: { - signingStyle: "manual", - provisioningProfiles: { - APP_IDENTIFIER => PROFILE_NAME - }, - manageAppVersionAndBuildNumber: false - } + api_key = app_store_connect_api_key( + key_id: ENV.fetch("APP_STORE_CONNECT_API_KEY_ID"), + issuer_id: ENV.fetch("APP_STORE_CONNECT_API_ISSUER_ID"), + key_content: ENV.fetch("APP_STORE_CONNECT_API_KEY"), + is_key_content_base64: true ) - archived_plist = File.join( - ARCHIVE_PATH, - "Products", - "Applications", - "XION.app", - "Info.plist" - ) - archived_version = read_plist_value(archived_plist, "CFBundleShortVersionString") - archived_build = read_plist_value(archived_plist, "CFBundleVersion") - UI.user_error!("Archived version does not match the release tag") unless archived_version == metadata[:version] - UI.user_error!("Archived build does not match Gitea") unless archived_build == metadata[:build_number] + build_app(scheme: "XIONControlPanel") - if options[:skip_upload] - UI.important("Skipping the TestFlight upload for local validation") - else - upload_to_testflight( - api_key: api_key, - app_identifier: APP_IDENTIFIER, - ipa: IPA_PATH, - skip_waiting_for_build_processing: true, - uses_non_exempt_encryption: false - ) - end - ensure - delete_keychain(name: CI_KEYCHAIN_NAME) if ci? && File.file?(CI_KEYCHAIN_DB_PATH) + upload_to_testflight( + api_key: api_key, + skip_waiting_for_build_processing: true, + uses_non_exempt_encryption: false + ) end end diff --git a/fastlane/README.md b/fastlane/README.md index 25cc8f6..cf025da 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -15,18 +15,10 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do ## iOS -### ios prepare_signing +### ios beta ```sh -[bundle exec] fastlane ios prepare_signing -``` - -Create or renew the App Store provisioning profile in the Match repository - -### ios release - -```sh -[bundle exec] fastlane ios release +[bundle exec] fastlane ios beta ``` Build a release tag and upload it to TestFlight