Compare commits

...

9 Commits

Author SHA1 Message Date
f445730a41 ios: override iphoneos signing identity
Some checks failed
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 21:29:35 -07:00
76cb808c33 ios: use disposable keychain as ci default
Some checks failed
TestFlight Release / testflight (push) Failing after 15s
2026-06-25 21:27:19 -07:00
e167bd983f ios: use generic xcode signing selector
Some checks failed
TestFlight Release / testflight (push) Failing after 19s
2026-06-25 21:25:13 -07:00
e4dd91564f ios: unlock signing keychain before build
Some checks failed
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 21:20:31 -07:00
3bfde476a6 ios: use single identity signing p12
Some checks failed
TestFlight Release / testflight (push) Failing after 16s
2026-06-25 21:18:54 -07:00
b8676027db ios: trust Apple root in CI signing keychain
Some checks failed
TestFlight Release / testflight (push) Failing after 8s
2026-06-25 21:12:53 -07:00
d36d2c60a3 ios: install Apple WWDR intermediate in CI
Some checks failed
TestFlight Release / testflight (push) Failing after 18s
2026-06-25 21:11:01 -07:00
3d7031bb40 ios: avoid default keychain mutation in ci
Some checks failed
TestFlight Release / testflight (push) Failing after 17s
2026-06-25 21:08:32 -07:00
fa9b725c77 ios: expose signing keychain to xcodebuild
Some checks failed
TestFlight Release / testflight (push) Failing after 9s
2026-06-25 21:07:38 -07:00
4 changed files with 105 additions and 7 deletions

View File

@@ -85,15 +85,23 @@ jobs:
keychain_password="$(uuidgen)" keychain_password="$(uuidgen)"
keychain_path="${HOME}/Library/Keychains/${SIGNING_KEYCHAIN}.keychain-db" keychain_path="${HOME}/Library/Keychains/${SIGNING_KEYCHAIN}.keychain-db"
previous_default_keychain="$(security default-keychain -d user | sed 's/[ "]//g' || true)"
mkdir -p "${HOME}/Library/Keychains" "${HOME}/Library/MobileDevice/Provisioning Profiles" ios/build/secrets mkdir -p "${HOME}/Library/Keychains" "${HOME}/Library/MobileDevice/Provisioning Profiles" ios/build/secrets
printf '%s' "${APPSTORE_CERTIFICATES_FILE_BASE64}" | base64 --decode > ios/build/secrets/appstore-signing.p12 printf '%s' "${APPSTORE_CERTIFICATES_FILE_BASE64}" | base64 --decode > ios/build/secrets/appstore-signing.p12
printf '%s' "${APPSTORE_PROVISIONING_PROFILE_BASE64}" | base64 --decode > "${HOME}/Library/MobileDevice/Provisioning Profiles/Sybil_AppStore_CI.mobileprovision" printf '%s' "${APPSTORE_PROVISIONING_PROFILE_BASE64}" | base64 --decode > "${HOME}/Library/MobileDevice/Provisioning Profiles/Sybil_AppStore_CI.mobileprovision"
curl -fsSL https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer -o ios/build/secrets/AppleWWDRCAG3.cer
security create-keychain -p "${keychain_password}" "${keychain_path}" security create-keychain -p "${keychain_password}" "${keychain_path}"
security set-keychain-settings -lut 21600 "${keychain_path}" security set-keychain-settings -lut 21600 "${keychain_path}"
security unlock-keychain -p "${keychain_password}" "${keychain_path}" security unlock-keychain -p "${keychain_password}" "${keychain_path}"
security list-keychains -d user -s "${keychain_path}" $(security list-keychains -d user | sed 's/[ "]//g') security list-keychains -d user -s "${keychain_path}" $(security list-keychains -d user | sed 's/[ "]//g')
security default-keychain -d user -s "${keychain_path}"
security import ios/build/secrets/AppleWWDRCAG3.cer \
-k "${keychain_path}" \
-T /usr/bin/codesign \
-T /usr/bin/security \
-T /usr/bin/xcodebuild
security import ios/build/secrets/appstore-signing.p12 \ security import ios/build/secrets/appstore-signing.p12 \
-k "${keychain_path}" \ -k "${keychain_path}" \
-P "${APPSTORE_CERTIFICATES_PASSWORD}" \ -P "${APPSTORE_CERTIFICATES_PASSWORD}" \
@@ -101,6 +109,12 @@ jobs:
-T /usr/bin/security \ -T /usr/bin/security \
-T /usr/bin/xcodebuild -T /usr/bin/xcodebuild
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${keychain_password}" "${keychain_path}" security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${keychain_password}" "${keychain_path}"
security find-identity -v -p codesigning "${keychain_path}"
{
echo "SYBIL_SIGNING_KEYCHAIN_PATH=${keychain_path}"
echo "SYBIL_SIGNING_KEYCHAIN_PASSWORD=${keychain_password}"
echo "SYBIL_PREVIOUS_DEFAULT_KEYCHAIN=${previous_default_keychain}"
} >> "${GITHUB_ENV}"
- name: Build and upload to TestFlight - name: Build and upload to TestFlight
working-directory: ios working-directory: ios
@@ -116,6 +130,11 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
security unlock-keychain -p "${SYBIL_SIGNING_KEYCHAIN_PASSWORD}" "${SYBIL_SIGNING_KEYCHAIN_PATH}"
security list-keychains -d user -s "${SYBIL_SIGNING_KEYCHAIN_PATH}" $(security list-keychains -d user | sed 's/[ "]//g')
security default-keychain -d user -s "${SYBIL_SIGNING_KEYCHAIN_PATH}"
security find-identity -v -p codesigning "${SYBIL_SIGNING_KEYCHAIN_PATH}"
SYBIL_VERSION_TAG="${TAG_NAME}" bundle exec fastlane ios beta SYBIL_VERSION_TAG="${TAG_NAME}" bundle exec fastlane ios beta
- name: Locate IPA - name: Locate IPA
@@ -205,4 +224,7 @@ jobs:
- name: Clean up temporary keychain - name: Clean up temporary keychain
if: always() if: always()
run: | run: |
if [[ -n "${SYBIL_PREVIOUS_DEFAULT_KEYCHAIN:-}" ]]; then
security default-keychain -d user -s "${SYBIL_PREVIOUS_DEFAULT_KEYCHAIN}" || true
fi
security delete-keychain "${HOME}/Library/Keychains/${SIGNING_KEYCHAIN}.keychain-db" || true security delete-keychain "${HOME}/Library/Keychains/${SIGNING_KEYCHAIN}.keychain-db" || true

View File

@@ -6,6 +6,7 @@ SYBIL_APP_STORE_APPLE_ID=6759442828
SYBIL_PROVIDER_PUBLIC_ID=c043d167-ad88-4036-84ea-76c223f1b1b2 SYBIL_PROVIDER_PUBLIC_ID=c043d167-ad88-4036-84ea-76c223f1b1b2
SYBIL_PROVISIONING_PROFILE_SPECIFIER=Sybil AppStore CI SYBIL_PROVISIONING_PROFILE_SPECIFIER=Sybil AppStore CI
SYBIL_CODE_SIGN_IDENTITY=Apple Distribution: James Magahern (DQQH5H6GBD) SYBIL_CODE_SIGN_IDENTITY=Apple Distribution: James Magahern (DQQH5H6GBD)
SYBIL_XCODE_CODE_SIGN_IDENTITY=Apple Distribution
SYBIL_SIGNING_CERTIFICATE_ID= SYBIL_SIGNING_CERTIFICATE_ID=
SYBIL_SIGNING_KEYCHAIN= SYBIL_SIGNING_KEYCHAIN=

View File

@@ -14,10 +14,12 @@ git push origin release/v1.10.0
``` ```
The release job runs on the `xcode` runner label, imports the signing p12 into The release job runs on the `xcode` runner label, imports the signing p12 into
a temporary keychain, installs the App Store provisioning profile, builds and a temporary per-user keychain, makes that keychain the user default for the
duration of the job, installs the App Store provisioning profile, builds and
uploads the app with fastlane, then creates or updates the matching Gitea uploads the app with fastlane, then creates or updates the matching Gitea
release with the generated IPA as an asset. The job deletes the temporary release with the generated IPA as an asset. The job restores the previous user
signing keychain in an `always()` cleanup step. default keychain and deletes the temporary signing keychain in an `always()`
cleanup step.
Required repository secrets: Required repository secrets:
@@ -42,6 +44,11 @@ certificate and provisioning profile values into the repository secrets listed
above. The workflow uses the `Sybil AppStore CI` provisioning profile name by above. The workflow uses the `Sybil AppStore CI` provisioning profile name by
default. default.
Fastlane keeps two signing names separate. `SYBIL_CODE_SIGN_IDENTITY` is the
exact certificate common name used when exporting a local p12 for secrets, while
`SYBIL_XCODE_CODE_SIGN_IDENTITY` defaults to the generic `Apple Distribution`
selector that Xcode uses during archive/export.
If the Apple team has reached the Distribution certificate limit, set If the Apple team has reached the Distribution certificate limit, set
`SYBIL_SIGNING_CERTIFICATE_ID` to the portal id for a certificate whose private `SYBIL_SIGNING_CERTIFICATE_ID` to the portal id for a certificate whose private
key exists in the local login keychain before running `create_ci_signing`. The key exists in the local login keychain before running `create_ci_signing`. The

View File

@@ -20,6 +20,7 @@ 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") PROVIDER_PUBLIC_ID = ENV.fetch("SYBIL_PROVIDER_PUBLIC_ID", "c043d167-ad88-4036-84ea-76c223f1b1b2")
PROFILE_SPECIFIER = ENV["SYBIL_PROVISIONING_PROFILE_SPECIFIER"].to_s.strip.empty? ? "Sybil AppStore CI" : ENV["SYBIL_PROVISIONING_PROFILE_SPECIFIER"] PROFILE_SPECIFIER = ENV["SYBIL_PROVISIONING_PROFILE_SPECIFIER"].to_s.strip.empty? ? "Sybil AppStore CI" : ENV["SYBIL_PROVISIONING_PROFILE_SPECIFIER"]
SIGNING_CERTIFICATE_NAME = ENV["SYBIL_CODE_SIGN_IDENTITY"].to_s.strip.empty? ? "Apple Distribution: James Magahern (DQQH5H6GBD)" : ENV["SYBIL_CODE_SIGN_IDENTITY"] SIGNING_CERTIFICATE_NAME = ENV["SYBIL_CODE_SIGN_IDENTITY"].to_s.strip.empty? ? "Apple Distribution: James Magahern (DQQH5H6GBD)" : ENV["SYBIL_CODE_SIGN_IDENTITY"]
XCODE_CODE_SIGN_IDENTITY = ENV["SYBIL_XCODE_CODE_SIGN_IDENTITY"].to_s.strip.empty? ? "Apple Distribution" : ENV["SYBIL_XCODE_CODE_SIGN_IDENTITY"]
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")
@@ -77,7 +78,7 @@ def release_version
end end
def xcode_build_setting(key, value) def xcode_build_setting(key, value)
"#{key}=#{value.to_s.shellescape}" "#{key.to_s.shellescape}=#{value.to_s.shellescape}"
end end
def env_line(key, value) def env_line(key, value)
@@ -188,6 +189,67 @@ def recreate_app_store_profile(certificate_id)
profile_path profile_path
end end
def signing_identity_p12(p12_path, p12_password)
work_dir = File.join(SIGNING_OUTPUT_DIR, "single-identity")
FileUtils.rm_rf(work_dir)
FileUtils.mkdir_p(work_dir)
all_pem = File.join(work_dir, "all.pem")
stdout, stderr, status = Open3.capture3(
"openssl", "pkcs12",
"-in", p12_path,
"-nodes",
"-passin", "pass:#{p12_password}",
"-out", all_pem
)
UI.user_error!("Could not inspect exported p12\n#{stderr}\n#{stdout}") unless status.success?
records = File.read(all_pem).split(/(?=Bag Attributes\n)/)
cert_record = records.find do |record|
record.include?("friendlyName: #{SIGNING_CERTIFICATE_NAME}") && record.include?("-----BEGIN CERTIFICATE-----")
end
UI.user_error!("Could not find #{SIGNING_CERTIFICATE_NAME} certificate in exported p12") unless cert_record
local_key_id = cert_record[/localKeyID: (.+)/, 1].to_s.strip
UI.user_error!("Could not resolve localKeyID for #{SIGNING_CERTIFICATE_NAME}") unless present?(local_key_id)
key_record = records.find do |record|
record.include?("localKeyID: #{local_key_id}") && record.include?("-----BEGIN PRIVATE KEY-----")
end
UI.user_error!("Could not find private key for #{SIGNING_CERTIFICATE_NAME}") unless key_record
cert_path = File.join(work_dir, "identity.cer.pem")
key_path = File.join(work_dir, "identity.key.pem")
File.write(cert_path, cert_record[/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m])
File.write(key_path, key_record[/-----BEGIN PRIVATE KEY-----.*?-----END PRIVATE KEY-----/m])
root_cer = File.join(work_dir, "AppleIncRootCertificate.cer")
wwdr_cer = File.join(work_dir, "AppleWWDRCAG3.cer")
root_pem = File.join(work_dir, "AppleIncRootCertificate.pem")
wwdr_pem = File.join(work_dir, "AppleWWDRCAG3.pem")
chain_pem = File.join(work_dir, "chain.pem")
run_silent("curl", "-fsSL", "https://www.apple.com/appleca/AppleIncRootCertificate.cer", "-o", root_cer, error_message: "Could not download Apple root certificate")
run_silent("curl", "-fsSL", "https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer", "-o", wwdr_cer, error_message: "Could not download Apple WWDR G3 certificate")
run_silent("openssl", "x509", "-inform", "DER", "-in", root_cer, "-out", root_pem, error_message: "Could not convert Apple root certificate")
run_silent("openssl", "x509", "-inform", "DER", "-in", wwdr_cer, "-out", wwdr_pem, error_message: "Could not convert Apple WWDR G3 certificate")
File.write(chain_pem, File.read(wwdr_pem) + File.read(root_pem))
single_p12_path = File.join(work_dir, "appstore-signing.p12")
run_silent(
"openssl", "pkcs12", "-export",
"-inkey", key_path,
"-in", cert_path,
"-certfile", chain_pem,
"-name", SIGNING_CERTIFICATE_NAME,
"-out", single_p12_path,
"-passout", "pass:#{p12_password}",
error_message: "Could not create single-identity p12"
)
FileUtils.cp(single_p12_path, p12_path)
ensure
FileUtils.rm_rf(work_dir) if present?(work_dir)
end
def app_store_connect_key_options def app_store_connect_key_options
key_id = ENV["APP_STORE_CONNECT_API_KEY_ID"] key_id = ENV["APP_STORE_CONNECT_API_KEY_ID"]
issuer_id = ENV["APP_STORE_CONNECT_API_ISSUER_ID"] issuer_id = ENV["APP_STORE_CONNECT_API_ISSUER_ID"]
@@ -293,6 +355,7 @@ platform :ios do
end end
UI.user_error!("Could not find exported p12 at #{p12_path}") unless File.exist?(p12_path) UI.user_error!("Could not find exported p12 at #{p12_path}") unless File.exist?(p12_path)
signing_identity_p12(p12_path, p12_password)
profile_path = recreate_app_store_profile(cert_id) profile_path = recreate_app_store_profile(cert_id)
UI.user_error!("Could not resolve generated provisioning profile path") unless present?(profile_path) && File.exist?(profile_path) UI.user_error!("Could not resolve generated provisioning profile path") unless present?(profile_path) && File.exist?(profile_path)
@@ -348,8 +411,13 @@ platform :ios do
xcode_build_setting("CODE_SIGN_STYLE", "Manual"), xcode_build_setting("CODE_SIGN_STYLE", "Manual"),
xcode_build_setting("DEVELOPMENT_TEAM", TEAM_ID), xcode_build_setting("DEVELOPMENT_TEAM", TEAM_ID),
xcode_build_setting("PROVISIONING_PROFILE_SPECIFIER", PROFILE_SPECIFIER), xcode_build_setting("PROVISIONING_PROFILE_SPECIFIER", PROFILE_SPECIFIER),
xcode_build_setting("CODE_SIGN_IDENTITY", SIGNING_CERTIFICATE_NAME) xcode_build_setting("CODE_SIGN_IDENTITY", XCODE_CODE_SIGN_IDENTITY),
].join(" ") xcode_build_setting("CODE_SIGN_IDENTITY[sdk=iphoneos*]", XCODE_CODE_SIGN_IDENTITY)
]
if present?(ENV["SYBIL_SIGNING_KEYCHAIN_PATH"])
xcode_args << xcode_build_setting("OTHER_CODE_SIGN_FLAGS", "--keychain #{ENV.fetch("SYBIL_SIGNING_KEYCHAIN_PATH")}")
end
xcode_args = xcode_args.join(" ")
ipa_path = build_app( ipa_path = build_app(
project: PROJECT_FILE, project: PROJECT_FILE,
@@ -367,7 +435,7 @@ platform :ios do
provisioningProfiles: { provisioningProfiles: {
APP_IDENTIFIER => PROFILE_SPECIFIER APP_IDENTIFIER => PROFILE_SPECIFIER
}, },
signingCertificate: SIGNING_CERTIFICATE_NAME, signingCertificate: XCODE_CODE_SIGN_IDENTITY,
teamID: TEAM_ID, teamID: TEAM_ID,
manageAppVersionAndBuildNumber: false, manageAppVersionAndBuildNumber: false,
uploadSymbols: true, uploadSymbols: true,