Release #9
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Tagging v* builds, signs, notarizes and publishes the app. The same five steps | |
| # that are documented for local releases in STRUCTURE.md, so the two stay in sync. | |
| # A v* tag builds and publishes. A manual run does everything except publish, | |
| # so the signing and notarization path can be rehearsed without shipping. | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| jobs: | |
| release: | |
| runs-on: macos-15 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Select the newest Xcode | |
| run: | | |
| set -euo pipefail | |
| NEWEST=$(ls -d /Applications/Xcode*.app | sort -V | tail -1) | |
| sudo xcode-select -s "$NEWEST" | |
| xcodebuild -version | |
| - name: Import Developer ID certificate | |
| env: | |
| CERT_P12_BASE64: ${{ secrets.DEVELOPER_ID_CERT_P12_BASE64 }} | |
| CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| # A throwaway keychain, unlocked only for this job. The password is | |
| # generated here, so it never needs to exist as a repository secret. | |
| KEYCHAIN_PASSWORD="$(uuidgen)" | |
| KEYCHAIN=$RUNNER_TEMP/build.keychain-db | |
| # Strip any whitespace the secret picked up on its way in, then make | |
| # sure we actually decoded a PKCS12 before blaming the password. | |
| printf '%s' "$CERT_P12_BASE64" | tr -d '[:space:]' | base64 --decode > "$RUNNER_TEMP/cert.p12" | |
| if [ ! -s "$RUNNER_TEMP/cert.p12" ]; then | |
| echo "::error::DEVELOPER_ID_CERT_P12_BASE64 decoded to an empty file" | |
| exit 1 | |
| fi | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" | |
| security set-keychain-settings -lut 3600 "$KEYCHAIN" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" | |
| # Not validated with openssl first: Keychain Access exports .p12 files | |
| # using RC2-40-CBC, which OpenSSL 3 dropped from its default provider. | |
| # `security` handles it, so let it be the judge. | |
| if ! security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$CERT_PASSWORD" \ | |
| -T /usr/bin/codesign -T /usr/bin/security; then | |
| echo "::error::Could not import the certificate. A 'MAC verification failed' above means DEVELOPER_ID_CERT_PASSWORD does not match the password used when exporting the .p12." | |
| exit 1 | |
| fi | |
| # Lets codesign use the key without an interactive prompt. | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: \ | |
| -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" > /dev/null | |
| security list-keychain -d user -s "$KEYCHAIN" login.keychain-db | |
| rm -f "$RUNNER_TEMP/cert.p12" | |
| security find-identity -v -p codesigning "$KEYCHAIN" | |
| - name: Archive | |
| run: | | |
| set -euo pipefail | |
| # The project signs with Apple Development so local builds and Xcode's | |
| # Run button work. CI only holds the Developer ID certificate, so | |
| # override the identity here instead of changing the project. | |
| xcodebuild -scheme Cleankey -configuration Release \ | |
| -destination 'platform=macOS' \ | |
| -archivePath "$RUNNER_TEMP/Cleankey.xcarchive" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| CODE_SIGN_IDENTITY="Developer ID Application" \ | |
| PROVISIONING_PROFILE_SPECIFIER="" \ | |
| OTHER_CODE_SIGN_FLAGS="--keychain $RUNNER_TEMP/build.keychain-db" \ | |
| archive | |
| - name: Export with Developer ID | |
| run: | | |
| set -euo pipefail | |
| xcodebuild -exportArchive \ | |
| -archivePath "$RUNNER_TEMP/Cleankey.xcarchive" \ | |
| -exportOptionsPlist ExportOptions.plist \ | |
| -exportPath "$RUNNER_TEMP/export" | |
| - name: Verify signature and entitlements | |
| run: | | |
| set -euo pipefail | |
| APP="$RUNNER_TEMP/export/Cleankey.app" | |
| codesign -dv --verbose=4 "$APP" 2>&1 | grep "Authority=Developer ID Application" | |
| # A debugger-attachable build must never ship. | |
| if codesign -d --entitlements - --xml "$APP" 2>/dev/null | grep -q "get-task-allow"; then | |
| echo "::error::get-task-allow present in exported app" | |
| exit 1 | |
| fi | |
| - name: Notarize and staple | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| APP="$RUNNER_TEMP/export/Cleankey.app" | |
| ditto -c -k --keepParent "$APP" "$RUNNER_TEMP/submit.zip" | |
| xcrun notarytool submit "$RUNNER_TEMP/submit.zip" \ | |
| --apple-id "$APPLE_ID" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --password "$APPLE_APP_PASSWORD" \ | |
| --wait | |
| xcrun stapler staple "$APP" | |
| xcrun stapler validate "$APP" | |
| spctl -a -vv "$APP" | |
| - name: Package | |
| id: package | |
| run: | | |
| set -euo pipefail | |
| APP="$RUNNER_TEMP/export/Cleankey.app" | |
| # On a manual run there is no tag, so take the version from the built | |
| # app and mark it, rather than naming the zip after the branch. | |
| if [[ "$GITHUB_REF" == refs/tags/* ]]; then | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| else | |
| BUILT=$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$APP/Contents/Info.plist") | |
| VERSION="$BUILT-dev" | |
| fi | |
| # Zip after stapling, so the ticket travels inside the app. | |
| ditto -c -k --keepParent "$APP" "$RUNNER_TEMP/Cleankey-$VERSION.zip" | |
| echo "zip=$RUNNER_TEMP/Cleankey-$VERSION.zip" >> "$GITHUB_OUTPUT" | |
| echo "name=Cleankey-$VERSION.zip" >> "$GITHUB_OUTPUT" | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ steps.package.outputs.name }} | |
| path: ${{ steps.package.outputs.zip }} | |
| if-no-files-found: error | |
| - name: Publish release | |
| # Tags only. A manual run stops here, having proved the whole signing and | |
| # notarization path without creating a release named after the branch. | |
| if: startsWith(github.ref, 'refs/tags/') | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" > /dev/null; then | |
| gh release upload "$GITHUB_REF_NAME" "${{ steps.package.outputs.zip }}" \ | |
| --repo "$GITHUB_REPOSITORY" --clobber | |
| EXISTING_NOTES="$(gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --json body --jq .body)" | |
| gh release edit "$GITHUB_REF_NAME" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --notes "$(git log -1 --format='- %s')"$'\n'"$EXISTING_NOTES" | |
| else | |
| gh release create "$GITHUB_REF_NAME" "${{ steps.package.outputs.zip }}" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --title "Cleankey ${GITHUB_REF_NAME#v}" \ | |
| --generate-notes \ | |
| --latest | |
| fi | |
| - name: Clean up keychain | |
| if: always() | |
| run: security delete-keychain "$RUNNER_TEMP/build.keychain-db" || true |