Pin release workflow to Xcode 26.3 #2
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 | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| contents: write | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| # Required repository configuration before pushing a v* tag: | |
| # Variable: | |
| # SPARKLE_PUBLIC_ED_KEY: output from Sparkle's generate_keys tool. | |
| # Secret: | |
| # SPARKLE_PRIVATE_ED_KEY: contents of the file exported by generate_keys -x. | |
| # The public and private Sparkle keys must be generated as one pair. | |
| # Releases are intentionally Apple-unsigned and are authenticated by Sparkle EdDSA. | |
| jobs: | |
| release: | |
| runs-on: macos-15 | |
| # macOS 15 defaults to Xcode 16.4 (Swift 6.1), while Defaults 9.0.8 requires | |
| # Swift tools 6.2. Pin the same Xcode generation used for local release builds. | |
| env: | |
| DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer | |
| SPARKLE_PUBLIC_ED_KEY: ${{ vars.SPARKLE_PUBLIC_ED_KEY }} | |
| SPARKLE_PRIVATE_ED_KEY: ${{ secrets.SPARKLE_PRIVATE_ED_KEY }} | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Verify Xcode toolchain | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| xcodebuild -version | |
| swift_version="$(xcrun swift --version)" | |
| printf '%s\n' "$swift_version" | |
| [[ "$swift_version" == *"Swift version 6.2"* ]] || { | |
| echo "Release requires Swift 6.2, but selected toolchain reports: $swift_version" >&2 | |
| exit 1 | |
| } | |
| - name: Validate release configuration | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| missing=() | |
| for name in SPARKLE_PUBLIC_ED_KEY SPARKLE_PRIVATE_ED_KEY; do | |
| if [[ -z "${!name:-}" ]]; then | |
| missing+=("$name") | |
| fi | |
| done | |
| if (( ${#missing[@]} )); then | |
| printf 'Missing release configuration: %s\n' "${missing[*]}" >&2 | |
| exit 1 | |
| fi | |
| if [[ "$SPARKLE_PUBLIC_ED_KEY" == "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" ]]; then | |
| echo "SPARKLE_PUBLIC_ED_KEY must not use the local-development placeholder." >&2 | |
| exit 1 | |
| fi | |
| decoded_length="$(printf '%s' "$SPARKLE_PUBLIC_ED_KEY" | /usr/bin/base64 -D | wc -c | tr -d ' ')" | |
| [[ "$decoded_length" == "32" ]] || { | |
| echo "SPARKLE_PUBLIC_ED_KEY must decode to 32 bytes, found $decoded_length." >&2 | |
| exit 1 | |
| } | |
| - name: Verify tag matches app version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="$(sed -n 's/.*MARKETING_VERSION = \([^;]*\);/\1/p' ElapseRecorder.xcodeproj/project.pbxproj | sort -u)" | |
| build="$(sed -n 's/.*CURRENT_PROJECT_VERSION = \([^;]*\);/\1/p' ElapseRecorder.xcodeproj/project.pbxproj | sort -u)" | |
| tag_version="${GITHUB_REF_NAME#v}" | |
| [[ "$version" == "$tag_version" ]] || { | |
| echo "Tag $GITHUB_REF_NAME does not match MARKETING_VERSION $version." >&2 | |
| exit 1 | |
| } | |
| [[ "$build" =~ ^[0-9]+$ ]] || { | |
| echo "CURRENT_PROJECT_VERSION must be one integer, found: $build" >&2 | |
| exit 1 | |
| } | |
| echo "APP_VERSION=$version" >> "$GITHUB_ENV" | |
| echo "APP_BUILD=$build" >> "$GITHUB_ENV" | |
| - name: Build Apple-unsigned application | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p build | |
| env GIT_CONFIG_COUNT=1 \ | |
| GIT_CONFIG_KEY_0=safe.bareRepository \ | |
| GIT_CONFIG_VALUE_0=all \ | |
| xcodebuild build \ | |
| -project ElapseRecorder.xcodeproj \ | |
| -scheme ElapseRecorder \ | |
| -configuration Release \ | |
| -destination 'generic/platform=macOS' \ | |
| -derivedDataPath "$PWD/build/DerivedData" \ | |
| -clonedSourcePackagesDirPath "$PWD/build/SourcePackages" \ | |
| -skipMacroValidation \ | |
| -skipPackagePluginValidation \ | |
| -onlyUsePackageVersionsFromResolvedFile \ | |
| CODE_SIGNING_ALLOWED=NO \ | |
| CODE_SIGNING_REQUIRED=NO \ | |
| CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \ | |
| ENABLE_HARDENED_RUNTIME=NO \ | |
| SPARKLE_PUBLIC_ED_KEY="$SPARKLE_PUBLIC_ED_KEY" | |
| app="$PWD/build/DerivedData/Build/Products/Release/Elapse Recorder.app" | |
| [[ -d "$app" ]] || { | |
| echo "Unsigned app was not produced at $app" >&2 | |
| exit 1 | |
| } | |
| # Mach-O binaries may receive an automatic linker ad-hoc signature even when Xcode | |
| # code signing is disabled. Remove it so Sparkle uses EdDSA as the sole trust anchor. | |
| codesign --remove-signature "$app" | |
| if codesign -dv "$app" >/dev/null 2>&1; then | |
| echo "Expected an Apple-unsigned host app, but a code signature is present." >&2 | |
| exit 1 | |
| fi | |
| embedded_key="$(/usr/libexec/PlistBuddy -c 'Print SUPublicEDKey' "$app/Contents/Info.plist")" | |
| [[ "$embedded_key" == "$SPARKLE_PUBLIC_ED_KEY" ]] || { | |
| echo "Built app does not contain the configured Sparkle public key." >&2 | |
| exit 1 | |
| } | |
| embedded_version="$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$app/Contents/Info.plist")" | |
| embedded_build="$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$app/Contents/Info.plist")" | |
| [[ "$embedded_version" == "$APP_VERSION" && "$embedded_build" == "$APP_BUILD" ]] || { | |
| echo "Built app version $embedded_version ($embedded_build) does not match $APP_VERSION ($APP_BUILD)." >&2 | |
| exit 1 | |
| } | |
| echo "UNSIGNED_APP=$app" >> "$GITHUB_ENV" | |
| - name: Package Sparkle update | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p build/updates | |
| archive="build/updates/ElapseRecorder-${APP_VERSION}.zip" | |
| ditto -c -k --sequesterRsrc --keepParent "$UNSIGNED_APP" "$archive" | |
| echo "UPDATE_ARCHIVE=$archive" >> "$GITHUB_ENV" | |
| - name: Create draft GitHub release | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then | |
| gh release upload "$GITHUB_REF_NAME" "$UPDATE_ARCHIVE" --clobber | |
| else | |
| gh release create "$GITHUB_REF_NAME" "$UPDATE_ARCHIVE" \ | |
| --draft \ | |
| --generate-notes \ | |
| --title "ElapseRecorder ${APP_VERSION}" | |
| fi | |
| gh release view "$GITHUB_REF_NAME" --json body --jq .body \ | |
| > "build/updates/ElapseRecorder-${APP_VERSION}.md" | |
| - name: Generate signed Sparkle appcast | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tool="$PWD/build/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_appcast" | |
| [[ -x "$tool" ]] || { | |
| echo "Sparkle generate_appcast tool was not resolved at $tool" >&2 | |
| exit 1 | |
| } | |
| printf '%s' "$SPARKLE_PRIVATE_ED_KEY" | "$tool" \ | |
| --ed-key-file - \ | |
| --download-url-prefix "https://github.com/deltaiota/ElapseRecorder/releases/download/${GITHUB_REF_NAME}/" \ | |
| --embed-release-notes \ | |
| --link "https://github.com/deltaiota/ElapseRecorder" \ | |
| --maximum-versions 3 \ | |
| --maximum-deltas 0 \ | |
| build/updates | |
| test -s build/updates/appcast.xml | |
| xmllint --noout build/updates/appcast.xml | |
| grep -q 'sparkle:edSignature=' build/updates/appcast.xml | |
| - name: Publish release and appcast | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| gh release upload "$GITHUB_REF_NAME" build/updates/appcast.xml --clobber | |
| gh release edit "$GITHUB_REF_NAME" --draft=false |