Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ jobs:
security set-key-partition-list -S apple-tool:,apple: -k "$password" "$keychain" > /dev/null
security list-keychain -d user -s "$keychain" login.keychain-db

# this is the only certificate the build has - a development one here
# would archive fine and fail the export twenty minutes in
security find-identity -v -p codesigning "$keychain"
if ! security find-identity -v -p codesigning "$keychain" | grep -q "Apple Distribution\|iPhone Distribution"; then
echo "::error::SIGNING_CERTIFICATE_P12 holds no Apple Distribution certificate (see README)"
exit 1
fi

# one step for both: a dry run builds the same thing and only stops short
# of the upload
- name: build and upload to App Store Connect
Expand All @@ -131,12 +139,22 @@ jobs:
ODR_DRY_RUN: ${{ env.dry_run }}
run: bundle exec fastlane ${{ matrix.flavor == 'pro' && 'deployPro' || 'deployLite' }}

# gym's log only says the export failed; the reason is in an
# .xcdistributionlogs bundle under $TMPDIR, which dies with the runner
- name: collect distribution logs
if: failure()
run: |
mkdir -p distribution-logs
find "${TMPDIR:-/tmp}" -maxdepth 1 -name '*.xcdistributionlogs' \
-exec cp -R {} distribution-logs/ \;

- uses: actions/upload-artifact@v7
if: always()
with:
name: build-log-${{ matrix.flavor }}
path: |
~/Library/Logs/gym
distribution-logs
*.ipa
*.app.dSYM.zip
if-no-files-found: warn
2 changes: 0 additions & 2 deletions OpenDocumentReader.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,6 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 5LS6X97G6J;
Expand Down Expand Up @@ -832,7 +831,6 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 5LS6X97G6J;
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,17 @@ It needs these repository secrets:
| `SIGNING_CERTIFICATE_PASSWORD` | password of that `.p12` |

The certificate is imported into a temporary keychain that is discarded with the
runner. Provisioning profiles are created on demand via
`-allowProvisioningUpdates`.
runner, and signing is manual: fastlane downloads the App Store provisioning
profile for the bundle id, and both the archive and the export use that
certificate and profile. Automatic signing would instead have Xcode mint
distribution assets of its own, which only an Admin key may do - anything less
fails the export with "Cloud signing permission error".

Downloading a profile is something any key may do; creating one wants an Admin
key. So a lesser key works as long as both apps have an App Store profile
already - the run says so in its first seconds otherwise, and either an Admin key
or a profile made by hand in the developer portal gets past it. Profiles expire
after a year, which is the other moment this matters.

The same lanes work locally once those variables are exported, and take the
version and the dry run the same way the workflow hands them over:
Expand Down
45 changes: 32 additions & 13 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ platform :ios do
# ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_CONTENT, the last being the base64 of
# the .p8 file.
#
# The key has to hit the disk because two unrelated consumers need it: the
# Connect API calls fastlane makes itself, and the xcodebuild subprocess that
# build_app spawns. xcodebuild knows nothing about fastlane's key -- see
# -authenticationKeyPath in `xcodebuild -help` -- so it gets the file.
# It is written to a private temporary file because that is the shape
# app_store_connect_api_key takes it in; the lane removes it again when it is
# done.
private_lane :api_key_file do
path = File.join(Dir.mktmpdir("asc-api-key"), "AuthKey_#{ENV.fetch('ASC_KEY_ID')}.p8")
File.write(path, Base64.decode64(ENV.fetch("ASC_KEY_CONTENT")))
Expand Down Expand Up @@ -82,6 +81,7 @@ platform :ios do
end

key_path = api_key_file
profile_dir = nil

begin
key = app_store_connect_api_key(
Expand All @@ -102,16 +102,29 @@ platform :ios do

UI.message("building #{options[:scheme]} #{version.empty? ? '(unversioned)' : version} as build #{build_number}")

# build_app is gym, which has no api_key option -- passing one aborts the
# lane before xcodebuild ever runs. Automatic signing is authenticated
# through xcargs instead: -allowProvisioningUpdates on its own needs an
# Apple account configured in Xcode, which the release runner does not
# have.
# Signing is manual from here on. Automatic signing has xcodebuild mint
# distribution assets of its own -- cloud signing, which only an Admin key
# may do, so the export failed with "Cloud signing permission error" while
# the imported certificate went unused. sigh downloads the App Store
# profile matching that certificate instead, and both xcodebuild passes
# are handed it. Downloading one is something any key may do; creating the
# profile, which this also does when the account has none, wants an Admin
# key - and fails here, before the build, rather than after it.
profile_dir = Dir.mktmpdir("provisioning-profile")
get_provisioning_profile(
Comment thread
andiwand marked this conversation as resolved.
api_key: key,
app_identifier: options[:app_identifier],
# out of the working tree; sigh installs a copy where xcodebuild looks
output_path: profile_dir
)
profile_name = lane_context[SharedValues::SIGH_NAME]
UI.user_error!("sigh returned no profile name") if profile_name.to_s.empty?

xcargs = [
"-allowProvisioningUpdates",
"-authenticationKeyPath", Shellwords.escape(key_path),
"-authenticationKeyID", Shellwords.escape(ENV.fetch("ASC_KEY_ID")),
"-authenticationKeyIssuerID", Shellwords.escape(ENV.fetch("ASC_ISSUER_ID")),
# the archive is signed the same way the export re-signs it
"CODE_SIGN_STYLE=Manual",
"CODE_SIGN_IDENTITY=#{Shellwords.escape('Apple Distribution')}",
"PROVISIONING_PROFILE_SPECIFIER=#{Shellwords.escape(profile_name)}",
# passed on the command line rather than written into project.pbxproj,
# so a release never leaves the working tree dirty
"CURRENT_PROJECT_VERSION=#{build_number}",
Expand All @@ -123,6 +136,11 @@ platform :ios do
build_app(
project: "OpenDocumentReader.xcodeproj",
scheme: options[:scheme],
export_method: "app-store",
export_options: {
signingStyle: "manual",
provisioningProfiles: { options[:app_identifier] => profile_name },
},
xcargs: xcargs.join(" ")
)

Expand All @@ -142,6 +160,7 @@ platform :ios do
end
ensure
FileUtils.remove_entry(File.dirname(key_path), true)
FileUtils.remove_entry(profile_dir, true) if profile_dir
end
end
end