From e7b81f24bd9115b8c863036bd0dfd72dc9d10c47 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 11 Jul 2026 13:01:42 -0700 Subject: [PATCH] ci: put CI keychain first in codesign search list codesign resolves signing identities through the user keychain search list (first match wins) and ignores --keychain for the lookup. This runner hosts another project whose keychain holds the same Apple Distribution identity, so when that keychain is locked, codesign fails with errSecInternalComponent no matter how correctly our own keychain is set up. Prepend the fresh CI keychain to the search list for the build and always delete it afterward, which restores the original list. Also drop the runner-diagnostics step, the GitHub-hosted Ruby PATH export, and the duplicate CODE_SIGN_IDENTITY param. Co-Authored-By: Claude Fable 5 --- .gitea/workflows/testflight.yml | 24 +------- ios/fastlane/Fastfile | 105 +++++++++++++++++--------------- 2 files changed, 59 insertions(+), 70 deletions(-) diff --git a/.gitea/workflows/testflight.yml b/.gitea/workflows/testflight.yml index a5544b3..00cd919 100644 --- a/.gitea/workflows/testflight.yml +++ b/.gitea/workflows/testflight.yml @@ -9,15 +9,10 @@ on: jobs: testflight: runs-on: macos-arm64 - defaults: - run: - shell: bash steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup Ruby uses: ruby/setup-ruby@v1 @@ -27,19 +22,7 @@ jobs: working-directory: ios - name: Install XcodeGen - run: | - set -euo pipefail - if ! command -v xcodegen >/dev/null 2>&1; then - brew install xcodegen - fi - - - name: Runner diagnostics - run: | - set -euo pipefail - whoami - printf 'HOME=%s\n' "$HOME" - security default-keychain -d user || true - security list-keychains -d user || true + run: command -v xcodegen >/dev/null 2>&1 || brew install xcodegen - name: Upload to TestFlight working-directory: ios @@ -53,7 +36,4 @@ jobs: SYBIL_BUILD_NUMBER: ${{ github.run_number }} 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 + run: bundle exec fastlane ios beta diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 6047fdd..2357402 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -6,6 +6,9 @@ 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") @@ -68,20 +71,33 @@ platform :ios do ) end - # CI uses a throwaway keychain that is deleted after the lane exits. + # 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 nil unless ci? + next unless ci? - run_token = "#{Time.now.to_i}_#{Process.pid}" - keychain_name = "fastlane_ci_#{run_token}" - - setup_ci( - force: true, - keychain_name: keychain_name, - timeout: 7_200 + 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 ) - keychain_name + 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| @@ -106,50 +122,43 @@ platform :ios do desc "Build and upload to TestFlight" lane :beta do - ci_keychain_name = nil + prepare_ci_keychain - begin - ci_keychain_name = prepare_ci_keychain + api_key = app_store_api_key - api_key = app_store_api_key + version = release_version + stamp_marketing_version(version) + sh("xcodegen", "--spec", PROJECT_SPEC) - 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) - 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) - sync_signing(api_key: api_key, readonly: true) - - sh("security find-identity -v -p codesigning") if ci_keychain_name - - build_app( - project: PROJECT_FILE, - scheme: SCHEME, - export_method: "app-store", - codesigning_identity: "Apple Distribution", - 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( + 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 } - ) + } + ) - upload_to_testflight( - api_key: api_key, - skip_waiting_for_build_processing: true - ) - ensure - delete_keychain(name: ci_keychain_name) if ci_keychain_name - end + upload_to_testflight( + api_key: api_key, + skip_waiting_for_build_processing: true + ) + ensure + delete_keychain(name: CI_KEYCHAIN_NAME) if ci? && File.file?(CI_KEYCHAIN_DB_PATH) end end