From fa75b6207fd39be9f96674f35491bb89852e9a03 Mon Sep 17 00:00:00 2001 From: Alexander Goodkind Date: Sat, 8 Aug 2026 15:09:20 -0700 Subject: [PATCH 1/3] Import each signing certificate a build passes, rather than one named for Developer ID A repository whose CI runner is not a registered device signs CI with Apple Distribution and releases with Developer ID, and there was one certificate slot for both, named for Developer ID and read by the release workflow too, so it could hold one certificate or the other and never both. Each certificate is now its own input and its own secret, a caller passes whichever ones it signs with, and signing-identity-name selects which one a given build uses. --- .github/actions/ci-gate/action.yml | 28 ++++-- .../actions/import-signing-cert/action.yml | 98 ++++++++++++++++--- .../workflow-helper/workflow-helper.swift | 46 +++++++-- .github/workflows/_ci.yml | 22 +++++ .github/workflows/_release.yml | 6 +- 5 files changed, 170 insertions(+), 30 deletions(-) diff --git a/.github/actions/ci-gate/action.yml b/.github/actions/ci-gate/action.yml index 861b8c13..08254644 100644 --- a/.github/actions/ci-gate/action.yml +++ b/.github/actions/ci-gate/action.yml @@ -69,13 +69,21 @@ inputs: required: false default: "1" apple-developer-id-p12-base64: - description: Base64-encoded Developer ID certificate. + description: Base64-encoded Developer ID certificate. Pass it when this build signs with Developer ID. required: false default: "" apple-developer-id-p12-password: description: Password for the Developer ID certificate. required: false default: "" + apple-distribution-p12-base64: + description: Base64-encoded Apple Distribution certificate. Pass it when this build runs where development provisioning cannot work, so only App Store profiles sign. + required: false + default: "" + apple-distribution-p12-password: + description: Password for the Apple Distribution certificate. + required: false + default: "" apple-developer-id-profile-base64: description: Base64-encoded Developer ID provisioning profile. required: false @@ -174,8 +182,10 @@ runs: SWIFT_MK_HELPER_ROOT: ${{ github.repository == 'agoodkind/swift-makefile' && '.' || '.swift-makefile' }} IMPORT_SIGNING_CERT: ${{ inputs.import-signing-cert }} INSTALL_PROVISIONING_PROFILE: ${{ inputs.install-provisioning-profile }} - HAS_SIGNING_CERT: ${{ inputs.apple-developer-id-p12-base64 != '' }} - HAS_SIGNING_PASSWORD: ${{ inputs.apple-developer-id-p12-password != '' }} + HAS_DEVELOPER_ID_CERT: ${{ inputs.apple-developer-id-p12-base64 != '' }} + HAS_DEVELOPER_ID_PASSWORD: ${{ inputs.apple-developer-id-p12-password != '' }} + HAS_DISTRIBUTION_CERT: ${{ inputs.apple-distribution-p12-base64 != '' }} + HAS_DISTRIBUTION_PASSWORD: ${{ inputs.apple-distribution-p12-password != '' }} HAS_PROVISIONING_PROFILE: ${{ inputs.apple-developer-id-profile-base64 != '' }} run: swift "${SWIFT_MK_HELPER_ROOT}/.github/actions/workflow-helper/workflow-helper.swift" validate-signing-inputs @@ -184,8 +194,10 @@ runs: if: ${{ inputs.run != 'false' && (inputs.gate != 'extra-targets' || steps.resolve.outputs.count != '0') && inputs.import-signing-cert == 'true' && github.repository == 'agoodkind/swift-makefile' }} uses: ./.github/actions/import-signing-cert with: - p12-base64: ${{ inputs.apple-developer-id-p12-base64 }} - p12-password: ${{ inputs.apple-developer-id-p12-password }} + developer-id-p12-base64: ${{ inputs.apple-developer-id-p12-base64 }} + developer-id-p12-password: ${{ inputs.apple-developer-id-p12-password }} + distribution-p12-base64: ${{ inputs.apple-distribution-p12-base64 }} + distribution-p12-password: ${{ inputs.apple-distribution-p12-password }} identity-name: ${{ inputs.signing-identity-name }} - name: Install signing certificate @@ -193,8 +205,10 @@ runs: if: ${{ inputs.run != 'false' && (inputs.gate != 'extra-targets' || steps.resolve.outputs.count != '0') && inputs.import-signing-cert == 'true' && github.repository != 'agoodkind/swift-makefile' }} uses: agoodkind/swift-makefile/.github/actions/import-signing-cert@main with: - p12-base64: ${{ inputs.apple-developer-id-p12-base64 }} - p12-password: ${{ inputs.apple-developer-id-p12-password }} + developer-id-p12-base64: ${{ inputs.apple-developer-id-p12-base64 }} + developer-id-p12-password: ${{ inputs.apple-developer-id-p12-password }} + distribution-p12-base64: ${{ inputs.apple-distribution-p12-base64 }} + distribution-p12-password: ${{ inputs.apple-distribution-p12-password }} identity-name: ${{ inputs.signing-identity-name }} - name: Install provisioning profile (swift-makefile) diff --git a/.github/actions/import-signing-cert/action.yml b/.github/actions/import-signing-cert/action.yml index 8fbab7f0..798d2660 100644 --- a/.github/actions/import-signing-cert/action.yml +++ b/.github/actions/import-signing-cert/action.yml @@ -1,15 +1,25 @@ -name: Import Developer ID signing certificate -description: Import the Developer ID Application certificate with apple-actions/import-codesign-certs and resolve its codesigning identity SHA-1. +name: Import signing certificates +description: Import the signing certificates a build needs into one keychain and resolve the codesigning identity SHA-1 for the identity the caller names. inputs: - p12-base64: - description: Base64-encoded Developer ID Application .p12. - required: true - p12-password: - description: Import password for the .p12. - required: true + developer-id-p12-base64: + description: Base64-encoded Developer ID Application .p12. Pass it when the build signs anything with Developer ID. + required: false + default: "" + developer-id-p12-password: + description: Import password for the Developer ID .p12. + required: false + default: "" + distribution-p12-base64: + description: Base64-encoded Apple Distribution .p12. Pass it when the build signs on a machine that is not a registered device, where only App Store profiles work. + required: false + default: "" + distribution-p12-password: + description: Import password for the Apple Distribution .p12. + required: false + default: "" identity-name: - description: 'Full identity name to resolve, e.g. "Developer ID Application: Name (TEAMID)".' + description: 'Full identity name to resolve, e.g. "Developer ID Application: Name (TEAMID)" or "Apple Distribution: Name (TEAMID)". It decides which imported certificate signs.' required: true outputs: @@ -49,17 +59,72 @@ runs: printf 'path_noext=%s\n' "$keychain_base" >> "$GITHUB_OUTPUT" printf 'path=%s\n' "$keychain_path" >> "$GITHUB_OUTPUT" + - name: Require at least one certificate + shell: bash + env: + DEVELOPER_ID_P12: ${{ inputs.developer-id-p12-base64 }} + DISTRIBUTION_P12: ${{ inputs.distribution-p12-base64 }} + run: | + set -euo pipefail + + if [[ -z "$DEVELOPER_ID_P12" && -z "$DISTRIBUTION_P12" ]]; then + echo "signing was requested but neither a Developer ID nor an Apple Distribution certificate was passed" >&2 + exit 1 + fi + - name: Remove stale signing keychain for this runner shell: bash env: KEYCHAIN_PATH: ${{ steps.keychain.outputs.path }} run: security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true - - uses: apple-actions/import-codesign-certs@v3 + # The keychain is created here rather than by an import step so that each + # import is guarded only by whether its own certificate was passed. Letting + # the first import create it would make the steps order-dependent: whichever + # ran first would have to create, and the other would have to not, which + # cannot be expressed when either may be absent. + - name: Create the signing keychain + id: create-keychain + shell: bash + env: + KEYCHAIN_NOEXT: ${{ steps.keychain.outputs.path_noext }} + KEYCHAIN_PATH: ${{ steps.keychain.outputs.path }} + run: | + set -euo pipefail + + keychain_password="$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | dd bs=1 count=40 2>/dev/null)" + # Masked before it reaches any later step's environment, so no log can + # echo it even if a step runs with the shell tracing on. + echo "::add-mask::${keychain_password}" + + security create-keychain -p "$keychain_password" "$KEYCHAIN_NOEXT" + # Keep it unlocked for the job: the default is a six-hour idle relock, + # which a long build can cross, and a relocked keychain fails signing + # with a prompt nobody can answer. + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$keychain_password" "$KEYCHAIN_PATH" + + printf 'password=%s\n' "$keychain_password" >> "$GITHUB_OUTPUT" + + - name: Import the Developer ID certificate + if: ${{ inputs.developer-id-p12-base64 != '' }} + uses: apple-actions/import-codesign-certs@v3 + with: + p12-file-base64: ${{ inputs.developer-id-p12-base64 }} + p12-password: ${{ inputs.developer-id-p12-password }} + keychain: ${{ steps.keychain.outputs.path_noext }} + create-keychain: "false" + keychain-password: ${{ steps.create-keychain.outputs.password }} + + - name: Import the Apple Distribution certificate + if: ${{ inputs.distribution-p12-base64 != '' }} + uses: apple-actions/import-codesign-certs@v3 with: - p12-file-base64: ${{ inputs.p12-base64 }} - p12-password: ${{ inputs.p12-password }} + p12-file-base64: ${{ inputs.distribution-p12-base64 }} + p12-password: ${{ inputs.distribution-p12-password }} keychain: ${{ steps.keychain.outputs.path_noext }} + create-keychain: "false" + keychain-password: ${{ steps.create-keychain.outputs.password }} - name: Scope the user keychain search list to the signing keychain shell: bash @@ -78,7 +143,7 @@ runs: # "No certificate for team". Set the list explicitly to the signing # keychain plus the System keychain: the signing keychain supplies the # identity and the System keychain supplies the trust anchors that - # validate the Developer ID chain. + # validate the certificate chain. security list-keychains -d user -s "$KEYCHAIN_PATH" /Library/Keychains/System.keychain - name: Resolve identity SHA-1 @@ -99,7 +164,12 @@ runs: )" if [[ -z "$identity_sha1" ]]; then - echo "Developer ID Application identity '$IDENTITY_NAME' not found in $KEYCHAIN_PATH" >&2 + echo "signing identity '$IDENTITY_NAME' not found in $KEYCHAIN_PATH" >&2 + echo "identities that were imported:" >&2 + # Names only. This is what tells a reader whether the wrong + # certificate was supplied rather than the right one failing to + # validate, which is otherwise indistinguishable from this failure. + security find-identity -p codesigning "$KEYCHAIN_PATH" >&2 exit 1 fi diff --git a/.github/actions/workflow-helper/workflow-helper.swift b/.github/actions/workflow-helper/workflow-helper.swift index 8f9a7795..047d66fb 100644 --- a/.github/actions/workflow-helper/workflow-helper.swift +++ b/.github/actions/workflow-helper/workflow-helper.swift @@ -23,6 +23,7 @@ private enum WorkflowHelperError: LocalizedError { case invalidJSONShape(label: String) case invalidJSONElement(label: String) case failedCommand(command: String, exitStatus: Int32) + case missingSigningCertificate case missingSigningSecret(String) var errorDescription: String? { @@ -41,6 +42,14 @@ private enum WorkflowHelperError: LocalizedError { return "\(label) must contain only strings" case let .failedCommand(command, exitStatus): return "\(command) exited with status \(exitStatus)" + case .missingSigningCertificate: + return + "workflow-helper: signed CI needs a certificate, and neither " + + "APPLE_DEVELOPER_ID_P12_BASE64 nor APPLE_DISTRIBUTION_P12_BASE64 is set. " + + "Set the one this repository signs CI with: Apple Distribution when the " + + "runner is not a registered device, Developer ID otherwise. " + + "If this fails in a Dependabot run, check that the same secret name is " + + "available as a Dependabot secret" case let .missingSigningSecret(name): return "workflow-helper: \(name) is required for signed CI; " @@ -142,20 +151,43 @@ private func requireSigningSecret(_ present: Bool, name: String) throws { private func validateSigningInputs(environment: Environment) throws { if environment.bool("IMPORT_SIGNING_CERT") { + try requireSigningCertificate(environment: environment) + } + + if environment.bool("INSTALL_PROVISIONING_PROFILE") { try requireSigningSecret( - environment.bool("HAS_SIGNING_CERT"), - name: "APPLE_DEVELOPER_ID_P12_BASE64" + environment.bool("HAS_PROVISIONING_PROFILE"), + name: "APPLE_DEVELOPER_ID_PROFILE_BASE64" ) + } +} + +/// A signed build needs at least one certificate, and each certificate it does +/// supply needs its password. +/// +/// Which certificate is right depends on where the build runs. A machine that is +/// not a registered device cannot use development provisioning, so it signs with +/// Apple Distribution and App Store profiles; a build producing something a person +/// downloads signs with Developer ID. A repository that does both supplies both, +/// and `signing-identity-name` decides which one a given build uses. So this +/// refuses only the case where neither was supplied, rather than naming one. +private func requireSigningCertificate(environment: Environment) throws { + let hasDeveloperID = environment.bool("HAS_DEVELOPER_ID_CERT") + let hasDistribution = environment.bool("HAS_DISTRIBUTION_CERT") + + guard hasDeveloperID || hasDistribution else { + throw WorkflowHelperError.missingSigningCertificate + } + if hasDeveloperID { try requireSigningSecret( - environment.bool("HAS_SIGNING_PASSWORD"), + environment.bool("HAS_DEVELOPER_ID_PASSWORD"), name: "APPLE_DEVELOPER_ID_P12_PASSWORD" ) } - - if environment.bool("INSTALL_PROVISIONING_PROFILE") { + if hasDistribution { try requireSigningSecret( - environment.bool("HAS_PROVISIONING_PROFILE"), - name: "APPLE_DEVELOPER_ID_PROFILE_BASE64" + environment.bool("HAS_DISTRIBUTION_PASSWORD"), + name: "APPLE_DISTRIBUTION_P12_PASSWORD" ) } } diff --git a/.github/workflows/_ci.yml b/.github/workflows/_ci.yml index c9762ed9..bc2654e4 100644 --- a/.github/workflows/_ci.yml +++ b/.github/workflows/_ci.yml @@ -92,10 +92,24 @@ on: type: string default: agk-local-macos-26 secrets: + # The two certificates a build can sign with. Set whichever ones this + # repository actually signs with; `signing-identity-name` decides which one + # a given build uses, so passing both is normal for a repository that signs + # its CI and its release differently. + # + # A repository whose CI runner is not a registered device cannot use + # development provisioning, so its CI signs with Apple Distribution and App + # Store profiles, which carry no device list. It still releases with + # Developer ID, because a downloadable app cannot be App Store signed. Such a + # repository sets both. APPLE_DEVELOPER_ID_P12_BASE64: required: false APPLE_DEVELOPER_ID_P12_PASSWORD: required: false + APPLE_DISTRIBUTION_P12_BASE64: + required: false + APPLE_DISTRIBUTION_P12_PASSWORD: + required: false APPLE_DEVELOPER_ID_PROFILE_BASE64: required: false # App Store Connect API key for iOS automatic signing @@ -353,6 +367,8 @@ jobs: fetch-depth: ${{ inputs.fetch-depth }} apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }} apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }} + apple-distribution-p12-base64: ${{ secrets.APPLE_DISTRIBUTION_P12_BASE64 }} + apple-distribution-p12-password: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }} apple-developer-id-profile-base64: ${{ secrets.APPLE_DEVELOPER_ID_PROFILE_BASE64 }} apple-notary-key-base64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }} apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }} @@ -400,6 +416,8 @@ jobs: fetch-depth: ${{ inputs.fetch-depth }} apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }} apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }} + apple-distribution-p12-base64: ${{ secrets.APPLE_DISTRIBUTION_P12_BASE64 }} + apple-distribution-p12-password: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }} apple-developer-id-profile-base64: ${{ secrets.APPLE_DEVELOPER_ID_PROFILE_BASE64 }} apple-notary-key-base64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }} apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }} @@ -526,6 +544,8 @@ jobs: fetch-depth: ${{ inputs.fetch-depth }} apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }} apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }} + apple-distribution-p12-base64: ${{ secrets.APPLE_DISTRIBUTION_P12_BASE64 }} + apple-distribution-p12-password: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }} apple-developer-id-profile-base64: ${{ secrets.APPLE_DEVELOPER_ID_PROFILE_BASE64 }} apple-notary-key-base64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }} apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }} @@ -573,6 +593,8 @@ jobs: fetch-depth: ${{ inputs.fetch-depth }} apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }} apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }} + apple-distribution-p12-base64: ${{ secrets.APPLE_DISTRIBUTION_P12_BASE64 }} + apple-distribution-p12-password: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }} apple-developer-id-profile-base64: ${{ secrets.APPLE_DEVELOPER_ID_PROFILE_BASE64 }} apple-notary-key-base64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }} apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }} diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index 0ecf2c00..682520f7 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -559,8 +559,10 @@ jobs: if: env.HAS_SIGNING == 'true' uses: agoodkind/swift-makefile/.github/actions/import-signing-cert@main with: - p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }} - p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }} + # A release signs with Developer ID, whatever a consumer's CI signs with. + # An App Store certificate cannot sign something a person downloads. + developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }} + developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }} identity-name: ${{ inputs.signing-identity-name }} - name: Install provisioning profile From ebddbd4c553176b629c458ca2d4a550a67e0c30c Mon Sep 17 00:00:00 2001 From: Alexander Goodkind Date: Sat, 8 Aug 2026 15:42:02 -0700 Subject: [PATCH 2/3] Run the release signing action from the branch under test in this repository The release workflow referenced the action only at main, so a change to the action and its caller together had this repository test the previous action against the current workflow. The certificate import then received no certificate and the release build failed. The CI gate already splits local and remote for this reason. --- .github/workflows/_release.yml | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index 682520f7..5dd52f45 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -554,13 +554,29 @@ jobs: SWIFT_MK_HELPER_ROOT: ${{ github.repository == 'agoodkind/swift-makefile' && '.' || '.swift-makefile' }} run: bash "${SWIFT_MK_HELPER_ROOT}/.github/actions/ci-diagnostics/start.sh" "${RUNNER_TEMP}/ci-diagnostics" "${SWIFT_MK_HELPER_ROOT}" + # A release signs with Developer ID, whatever a repository's CI signs with, + # because an App Store certificate cannot sign something a person downloads. + # + # Two steps for one action, the same split the CI gate uses. This repository + # runs the copy in the branch under test, so a change to the action is + # exercised by the release dry run that ships it; every other repository runs + # the released copy. Referencing only @main would have this repository test + # the previous action against the current workflow, which fails whenever the + # two change together. + - name: Install signing certificate (swift-makefile) + id: cert-local + if: ${{ env.HAS_SIGNING == 'true' && github.repository == 'agoodkind/swift-makefile' }} + uses: ./.github/actions/import-signing-cert + with: + developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }} + developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }} + identity-name: ${{ inputs.signing-identity-name }} + - name: Install signing certificate - id: cert - if: env.HAS_SIGNING == 'true' + id: cert-remote + if: ${{ env.HAS_SIGNING == 'true' && github.repository != 'agoodkind/swift-makefile' }} uses: agoodkind/swift-makefile/.github/actions/import-signing-cert@main with: - # A release signs with Developer ID, whatever a consumer's CI signs with. - # An App Store certificate cannot sign something a person downloads. developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }} developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }} identity-name: ${{ inputs.signing-identity-name }} @@ -581,8 +597,8 @@ jobs: shell: bash env: SWIFT_MK_HELPER_ROOT: ${{ github.repository == 'agoodkind/swift-makefile' && '.' || '.swift-makefile' }} - SIGNING_KEYCHAIN: ${{ steps.cert.outputs.keychain }} - SIGNING_IDENTITY_SHA1: ${{ steps.cert.outputs.identity-sha1 }} + SIGNING_KEYCHAIN: ${{ steps.cert-local.outputs.keychain || steps.cert-remote.outputs.keychain }} + SIGNING_IDENTITY_SHA1: ${{ steps.cert-local.outputs.identity-sha1 || steps.cert-remote.outputs.identity-sha1 }} run: | bash "${SWIFT_MK_HELPER_ROOT}/.github/actions/ci-diagnostics/capture-signing.sh" \ "${RUNNER_TEMP}/ci-diagnostics" "${SIGNING_KEYCHAIN}" "${SIGNING_IDENTITY_SHA1}" @@ -595,8 +611,8 @@ jobs: - name: Build release artifacts shell: bash env: - CERT_SHA1: ${{ steps.cert.outputs.identity-sha1 }} - CODE_SIGN_KEYCHAIN: ${{ steps.cert.outputs.keychain }} + CERT_SHA1: ${{ steps.cert-local.outputs.identity-sha1 || steps.cert-remote.outputs.identity-sha1 }} + CODE_SIGN_KEYCHAIN: ${{ steps.cert-local.outputs.keychain || steps.cert-remote.outputs.keychain }} TEAM_ID: ${{ inputs.apple-team-id }} PROVISIONING_PROFILE_SPECIFIER: ${{ steps.profile.outputs.profile-specifier }} RELEASE_TAG: ${{ needs.meta.outputs.tag }} From 89d16807b92669313adf45236fd1b3350b844090 Mon Sep 17 00:00:00 2001 From: Alexander Goodkind Date: Sat, 8 Aug 2026 15:56:36 -0700 Subject: [PATCH 3/3] Create the signing keychain at the path every later command names Under the keychain directory the Security framework appends -db to the path it is given, so creating with the extensionless path produced a file ending in a bare -db while set-keychain-settings, unlock, import, and the identity lookup all named a .keychain-db file that did not exist. Locally that errors; on a session-less runner the step hung for six minutes. Also replaces the password generator, whose pipe made the reader exit first and kill the writer. --- .github/actions/import-signing-cert/action.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/actions/import-signing-cert/action.yml b/.github/actions/import-signing-cert/action.yml index 798d2660..0b976aeb 100644 --- a/.github/actions/import-signing-cert/action.yml +++ b/.github/actions/import-signing-cert/action.yml @@ -92,12 +92,22 @@ runs: run: | set -euo pipefail - keychain_password="$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | dd bs=1 count=40 2>/dev/null)" + # One command, no pipe. Reading /dev/urandom through a pipe into a + # byte-counting reader makes the reader exit first, which kills the writer + # with a broken pipe; under `pipefail` that fails, and on a runner it hung + # instead. openssl reads its own randomness and stops on its own. + keychain_password="$(openssl rand -hex 24)" # Masked before it reaches any later step's environment, so no log can # echo it even if a step runs with the shell tracing on. echo "::add-mask::${keychain_password}" - security create-keychain -p "$keychain_password" "$KEYCHAIN_NOEXT" + # Create with the .keychain suffix, which is what apple-actions passes. + # Under ~/Library/Keychains the Security framework appends -db to whatever + # path it is given, so this produces the .keychain-db file every later + # command names. Passing the extensionless path instead produces a file + # ending in a bare -db, and each later command then names a keychain that + # does not exist. + security create-keychain -p "$keychain_password" "${KEYCHAIN_NOEXT}.keychain" # Keep it unlocked for the job: the default is a six-hour idle relock, # which a long build can cross, and a relocked keychain fails signing # with a prompt nobody can answer.