chore: make e2e driver robust to runner environment #193
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 Please | |
| on: | |
| push: | |
| branches: [master] | |
| workflow_dispatch: | |
| inputs: | |
| tag_name: | |
| description: 'Tag to build and release (e.g. v0.3.0)' | |
| required: true | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| release-please: | |
| if: ${{ github.event_name == 'push' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_created: ${{ steps.release.outputs.release_created }} | |
| tag_name: ${{ steps.release.outputs.tag_name }} | |
| steps: | |
| - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4 + release-please 17.3.0 | |
| id: release | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # Mark release as draft so /releases/latest keeps serving the previous | |
| # version's appcast until the build uploads assets and publishes. | |
| # The tag already exists at this point, so the build job can check it out. | |
| - name: Mark release as draft | |
| if: steps.release.outputs.release_created == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: gh release edit "${{ steps.release.outputs.tag_name }}" --draft --repo ${{ github.repository }} | |
| build: | |
| name: Build & Release | |
| if: >- | |
| ${{ always() && ( | |
| (github.event_name == 'workflow_dispatch') || | |
| (needs.release-please.outputs.release_created == 'true') | |
| ) }} | |
| needs: [release-please] | |
| runs-on: macos-15 | |
| env: | |
| TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || needs.release-please.outputs.tag_name }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.TAG }} | |
| - name: Select Xcode 26 | |
| run: sudo xcode-select -s /Applications/Xcode_26.2.app | |
| - name: Resolve SPM dependencies | |
| run: xcodebuild -resolvePackageDependencies -project Deckard.xcodeproj | |
| - name: Extract version | |
| id: version | |
| run: echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" | |
| - name: Build Deckard | |
| run: | | |
| xcodebuild -project Deckard.xcodeproj \ | |
| -scheme Deckard \ | |
| -configuration Release \ | |
| -derivedDataPath build \ | |
| -destination 'platform=macOS' \ | |
| CODE_SIGN_IDENTITY="-" \ | |
| CODE_SIGN_STYLE=Manual \ | |
| DEVELOPMENT_TEAM="" \ | |
| build | |
| - name: Import Developer ID certificate | |
| if: env.CERTIFICATE_P12 != '' | |
| env: | |
| CERTIFICATE_P12: ${{ secrets.DEVELOPER_ID_CERTIFICATE_P12 }} | |
| CERTIFICATE_PASSWORD: ${{ secrets.DEVELOPER_ID_CERTIFICATE_PASSWORD }} | |
| run: | | |
| KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db | |
| security create-keychain -p "" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "" "$KEYCHAIN_PATH" | |
| echo "$CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12 | |
| security import $RUNNER_TEMP/certificate.p12 -P "$CERTIFICATE_PASSWORD" \ | |
| -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "$KEYCHAIN_PATH" | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" | |
| - name: Codesign app | |
| env: | |
| DEVELOPER_ID_IDENTITY: ${{ secrets.DEVELOPER_ID_IDENTITY }} | |
| run: | | |
| APP="build/Build/Products/Release/Deckard.app" | |
| IDENTITY="${DEVELOPER_ID_IDENTITY:-"-"}" | |
| # Sign embedded frameworks and helpers individually first | |
| find "$APP/Contents/Frameworks" -type f -perm +111 -o -name "*.dylib" -o -name "*.framework" 2>/dev/null | while read -r item; do | |
| codesign -s "$IDENTITY" --force --options runtime --timestamp "$item" || true | |
| done | |
| # Sign any bundled binaries | |
| find "$APP/Contents/Resources/bin" -type f -perm +111 2>/dev/null | while read -r item; do | |
| codesign -s "$IDENTITY" --force --options runtime --timestamp "$item" || true | |
| done | |
| # Sign the main app bundle | |
| codesign -s "$IDENTITY" --force --options runtime --timestamp \ | |
| --entitlements Resources/Deckard.entitlements \ | |
| "$APP" | |
| # Verify | |
| codesign --verify --deep --strict "$APP" | |
| - name: Create DMG | |
| run: | | |
| brew install create-dmg | |
| create-dmg \ | |
| --volname "Deckard" \ | |
| --window-pos 200 120 \ | |
| --window-size 600 400 \ | |
| --icon-size 100 \ | |
| --icon "Deckard.app" 150 190 \ | |
| --app-drop-link 450 190 \ | |
| "Deckard-${{ steps.version.outputs.version }}.dmg" \ | |
| build/Build/Products/Release/Deckard.app | |
| - name: Codesign DMG | |
| env: | |
| DEVELOPER_ID_IDENTITY: ${{ secrets.DEVELOPER_ID_IDENTITY }} | |
| run: | | |
| IDENTITY="${DEVELOPER_ID_IDENTITY:-"-"}" | |
| codesign -s "$IDENTITY" --force --timestamp \ | |
| "Deckard-${{ steps.version.outputs.version }}.dmg" | |
| - name: Notarize DMG | |
| if: env.APPLE_ID != '' | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} | |
| run: | | |
| xcrun notarytool submit "Deckard-${{ steps.version.outputs.version }}.dmg" \ | |
| --apple-id "$APPLE_ID" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --password "$APPLE_APP_PASSWORD" \ | |
| --wait | |
| xcrun stapler staple "Deckard-${{ steps.version.outputs.version }}.dmg" | |
| - name: Generate Sparkle appcast | |
| env: | |
| SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| DMG="Deckard-${VERSION}.dmg" | |
| DMG_SIZE=$(stat -f%z "$DMG") | |
| DMG_URL="https://github.com/gi11es/deckard/releases/download/v${VERSION}/${DMG}" | |
| # Get Sparkle's sign_update tool from the resolved SPM package | |
| SPARKLE_PATH=$(find build/SourcePackages/artifacts -name "Sparkle.framework" -path "*/macos*" 2>/dev/null | head -1) | |
| SIGN_UPDATE="" | |
| if [ -n "$SPARKLE_PATH" ]; then | |
| SIGN_UPDATE="$(dirname "$SPARKLE_PATH")/sign_update" | |
| fi | |
| # Fall back to checking other common locations | |
| if [ ! -x "$SIGN_UPDATE" ]; then | |
| SIGN_UPDATE=$(find build -name "sign_update" -type f 2>/dev/null | head -1) | |
| fi | |
| # Generate EdDSA signature if we have the key and tool | |
| SIGNATURE="" | |
| if [ -n "$SPARKLE_PRIVATE_KEY" ] && [ -x "$SIGN_UPDATE" ]; then | |
| SIGNATURE=$("$SIGN_UPDATE" "$DMG" --ed-key-file <(echo "$SPARKLE_PRIVATE_KEY") 2>/dev/null || true) | |
| fi | |
| # Build enclosure attributes | |
| # sign_update outputs complete XML attributes: sparkle:edSignature="..." length="..." | |
| if [ -n "$SIGNATURE" ]; then | |
| SIG_ATTR="$SIGNATURE" | |
| else | |
| SIG_ATTR="length=\"${DMG_SIZE}\"" | |
| fi | |
| # Extract this release's section from CHANGELOG.md and render to HTML | |
| # so Sparkle's "What's New" pane shows it on update. | |
| RELEASE_NOTES_MD=$(awk -v ver="$VERSION" ' | |
| $0 ~ "^## \\[" ver "\\]" { in_section=1 } | |
| in_section && /^## \[/ && $0 !~ "^## \\[" ver "\\]" { exit } | |
| in_section { print } | |
| ' CHANGELOG.md) | |
| RELEASE_NOTES_HTML="" | |
| if [ -n "$RELEASE_NOTES_MD" ]; then | |
| # Degrade to empty notes rather than failing the release if the | |
| # markdown API is unreachable. | |
| RELEASE_NOTES_HTML=$(jq -Rs \ | |
| --arg ctx "${{ github.repository }}" \ | |
| '{mode:"gfm", context:$ctx, text:.}' <<< "$RELEASE_NOTES_MD" \ | |
| | gh api -X POST /markdown --input - || true) | |
| # Neutralize any literal "]]>" so it can't prematurely close our CDATA. | |
| RELEASE_NOTES_HTML="${RELEASE_NOTES_HTML//]]>/]]]]><![CDATA[>}" | |
| fi | |
| cat > appcast.xml << APPCAST_EOF | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/"> | |
| <channel> | |
| <title>Deckard</title> | |
| <link>https://github.com/gi11es/deckard/releases</link> | |
| <language>en</language> | |
| <item> | |
| <title>Version ${VERSION}</title> | |
| <link>https://github.com/gi11es/deckard/releases/tag/v${VERSION}</link> | |
| <sparkle:version>${VERSION}</sparkle:version> | |
| <sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString> | |
| <sparkle:minimumSystemVersion>14.0</sparkle:minimumSystemVersion> | |
| <description><![CDATA[${RELEASE_NOTES_HTML}]]></description> | |
| <enclosure url="${DMG_URL}" type="application/octet-stream" ${SIG_ATTR} /> | |
| </item> | |
| </channel> | |
| </rss> | |
| APPCAST_EOF | |
| # Remove leading whitespace from heredoc indentation | |
| sed -i '' 's/^ //' appcast.xml | |
| - name: Upload DMG and appcast to release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Upload a stable-named DMG so /releases/latest/download/Deckard.dmg always works | |
| cp "Deckard-${{ steps.version.outputs.version }}.dmg" Deckard.dmg | |
| gh release upload "${{ env.TAG }}" \ | |
| "Deckard-${{ steps.version.outputs.version }}.dmg" \ | |
| Deckard.dmg \ | |
| appcast.xml \ | |
| --clobber | |
| - name: Publish release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: gh release edit "${{ env.TAG }}" --draft=false | |
| - name: Update Homebrew cask | |
| env: | |
| DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| DMG="Deckard-${VERSION}.dmg" | |
| SHA=$(shasum -a 256 "$DMG" | awk '{print $1}') | |
| # Set up SSH with deploy key | |
| mkdir -p ~/.ssh | |
| echo "$DEPLOY_KEY" > ~/.ssh/homebrew_tap_key | |
| chmod 600 ~/.ssh/homebrew_tap_key | |
| export GIT_SSH_COMMAND="ssh -i ~/.ssh/homebrew_tap_key -o StrictHostKeyChecking=no" | |
| git clone [email protected]:gi11es/homebrew-tap.git homebrew-tap | |
| cd homebrew-tap | |
| sed -i '' "s/version \".*\"/version \"${VERSION}\"/" Casks/deckard.rb | |
| sed -i '' "s/sha256 \".*\"/sha256 \"${SHA}\"/" Casks/deckard.rb | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Casks/deckard.rb | |
| git commit -m "Update deckard to ${VERSION}" | |
| git push |