ci: put CI keychain first in codesign search list
Some checks failed
TestFlight / testflight (push) Failing after 19s

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 13:01:42 -07:00
parent 20a310a4b1
commit e7b81f24bd
2 changed files with 59 additions and 70 deletions

View File

@@ -9,15 +9,10 @@ on:
jobs: jobs:
testflight: testflight:
runs-on: macos-arm64 runs-on: macos-arm64
defaults:
run:
shell: bash
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
@@ -27,19 +22,7 @@ jobs:
working-directory: ios working-directory: ios
- name: Install XcodeGen - name: Install XcodeGen
run: | run: command -v xcodegen >/dev/null 2>&1 || brew install xcodegen
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
- name: Upload to TestFlight - name: Upload to TestFlight
working-directory: ios working-directory: ios
@@ -53,7 +36,4 @@ jobs:
SYBIL_BUILD_NUMBER: ${{ github.run_number }} SYBIL_BUILD_NUMBER: ${{ github.run_number }}
FASTLANE_SKIP_UPDATE_CHECK: "1" FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120" FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: "120"
run: | run: bundle exec fastlane ios beta
export PATH="/Users/runner/hostedtoolcache/Ruby/3.1.7/arm64/bin:${PATH}"
ruby --version
bundle exec fastlane ios beta

View File

@@ -6,6 +6,9 @@ APP_IDENTIFIER = "net.buzzert.sybil2"
SCHEME = "Sybil" SCHEME = "Sybil"
TEAM_ID = "DQQH5H6GBD" TEAM_ID = "DQQH5H6GBD"
PROFILE_NAME = "Sybil AppStore CI" 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__) IOS_ROOT = File.expand_path("..", __dir__)
PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj") PROJECT_FILE = File.join(IOS_ROOT, "Sybil.xcodeproj")
PROJECT_SPEC = File.join(IOS_ROOT, "project.yml") PROJECT_SPEC = File.join(IOS_ROOT, "project.yml")
@@ -68,20 +71,33 @@ platform :ios do
) )
end 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 private_lane :prepare_ci_keychain do
next nil unless ci? next unless ci?
run_token = "#{Time.now.to_i}_#{Process.pid}" delete_keychain(name: CI_KEYCHAIN_NAME) if File.file?(CI_KEYCHAIN_DB_PATH)
keychain_name = "fastlane_ci_#{run_token}" create_keychain(
name: CI_KEYCHAIN_NAME,
setup_ci( password: CI_KEYCHAIN_PASSWORD,
force: true, unlock: true,
keychain_name: keychain_name, timeout: 3600,
timeout: 7_200 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 end
private_lane :sync_signing do |options| private_lane :sync_signing do |options|
@@ -106,50 +122,43 @@ platform :ios do
desc "Build and upload to TestFlight" desc "Build and upload to TestFlight"
lane :beta do lane :beta do
ci_keychain_name = nil prepare_ci_keychain
begin api_key = app_store_api_key
ci_keychain_name = prepare_ci_keychain
api_key = app_store_api_key version = release_version
stamp_marketing_version(version)
sh("xcodegen", "--spec", PROJECT_SPEC)
version = release_version increment_version_number(version_number: version, xcodeproj: PROJECT_FILE)
stamp_marketing_version(version) increment_build_number(build_number: build_number, xcodeproj: PROJECT_FILE)
sh("xcodegen", "--spec", PROJECT_SPEC)
increment_version_number(version_number: version, xcodeproj: PROJECT_FILE) sync_signing(api_key: api_key, readonly: true)
increment_build_number(build_number: build_number, xcodeproj: PROJECT_FILE)
sync_signing(api_key: api_key, readonly: true) build_app(
project: PROJECT_FILE,
sh("security find-identity -v -p codesigning") if ci_keychain_name scheme: SCHEME,
export_method: "app-store",
build_app( xcargs: [
project: PROJECT_FILE, "DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}",
scheme: SCHEME, "CODE_SIGN_STYLE=Manual",
export_method: "app-store", "CODE_SIGN_IDENTITY=Apple\\ Distribution",
codesigning_identity: "Apple Distribution", "PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}"
xcargs: [ ].join(" "),
"DEVELOPMENT_TEAM=#{TEAM_ID.shellescape}", export_options: {
"CODE_SIGN_STYLE=Manual", signingStyle: "manual",
"CODE_SIGN_IDENTITY=Apple\\ Distribution", teamID: TEAM_ID,
"PROVISIONING_PROFILE_SPECIFIER=#{PROFILE_NAME.shellescape}" provisioningProfiles: {
].join(" "), APP_IDENTIFIER => PROFILE_NAME
export_options: {
signingStyle: "manual",
teamID: TEAM_ID,
provisioningProfiles: {
APP_IDENTIFIER => PROFILE_NAME
}
} }
) }
)
upload_to_testflight( upload_to_testflight(
api_key: api_key, api_key: api_key,
skip_waiting_for_build_processing: true skip_waiting_for_build_processing: true
) )
ensure ensure
delete_keychain(name: ci_keychain_name) if ci_keychain_name delete_keychain(name: CI_KEYCHAIN_NAME) if ci? && File.file?(CI_KEYCHAIN_DB_PATH)
end
end end
end end