diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ed079c..8aa6aeb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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 diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index 03d8b75..132e9c0 100644 --- a/OpenDocumentReader.xcodeproj/project.pbxproj +++ b/OpenDocumentReader.xcodeproj/project.pbxproj @@ -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; @@ -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; diff --git a/README.md b/README.md index 54c24a6..5b1be87 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5b51048..47519e2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -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"))) @@ -82,6 +81,7 @@ platform :ios do end key_path = api_key_file + profile_dir = nil begin key = app_store_connect_api_key( @@ -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( + 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}", @@ -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(" ") ) @@ -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