Add build error detection with automatic iteration via @copilot #10
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: "Deploy Boring Notch" | |
| on: | |
| issue_comment: | |
| types: [created] | |
| concurrency: | |
| group: build-boringnotch-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| projname: boringNotch | |
| beta-channel-name: "beta" | |
| EXPORT_METHOD: "development" | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| preparation: | |
| name: Preparation job | |
| if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/release') }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| outputs: | |
| is_beta: ${{ steps.check-beta.outputs.is_beta }} | |
| version: ${{ steps.extract-version.outputs.version }} | |
| build_number: ${{ steps.extract-version.outputs.build_number }} | |
| title: ${{ steps.generate-release-notes.outputs.title }} | |
| release_notes: ${{ steps.generate-release-notes.outputs.release_notes }} | |
| steps: | |
| - uses: xt0rted/pull-request-comment-branch@v1 | |
| id: comment-branch | |
| - name: Add reaction to comment | |
| uses: peter-evans/create-or-update-comment@v4 | |
| with: | |
| comment-id: ${{ github.event.comment.id }} | |
| reactions: eyes | |
| - uses: actions/github-script@v6 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const commenter = context.payload.comment.user.login; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| // Check the commenter's repository permission level (need write or admin) | |
| try { | |
| const perm = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: commenter }); | |
| const level = perm.data.permission; // admin, write, read, none | |
| if (level !== 'admin') { | |
| core.setFailed("Commenter is not an admin on the repository"); | |
| } | |
| } catch (err) { | |
| core.setFailed("Failed to determine collaborator permission level: " + err.message); | |
| } | |
| // Check if the PR is ready to be merged | |
| const pr = await github.rest.pulls.get({ | |
| owner: owner, | |
| repo: repo, | |
| pull_number: context.issue.number, | |
| }); | |
| if (pr.data.draft || !pr.data.mergeable) { | |
| core.setFailed("PR is not ready to be merged"); | |
| } | |
| - uses: actions/checkout@v3 | |
| if: success() | |
| with: | |
| ref: ${{ steps.comment-branch.outputs.head_ref }} | |
| - name: Ensure scripts directory exists | |
| run: | | |
| if [ ! -f ".github/scripts/extract_version.py" ]; then | |
| echo "Script not found in PR branch, fetching from base branch" | |
| git fetch origin ${{ steps.comment-branch.outputs.base_ref }} | |
| git checkout origin/${{ steps.comment-branch.outputs.base_ref }} -- .github/scripts/ | |
| fi | |
| - name: Extract version from comment or Xcode project | |
| id: extract-version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Ensure semver is installed deterministically | |
| python3 -m pip install --upgrade --no-cache-dir semver | |
| export COMMENT="${{ github.event.comment.body }}" | |
| export projname="${{ env.projname }}" | |
| # Run script: if it exits non-zero, this step will fail and stop the job | |
| OUTPUT=$(python3 .github/scripts/extract_version.py -c "$COMMENT") | |
| # Parse outputs (script prints version=... and is_beta=...) | |
| VERSION=$(echo "$OUTPUT" | grep -m1 '^version=' | sed 's/^version=//') | |
| IS_BETA=$(echo "$OUTPUT" | grep -m1 '^is_beta=' | sed 's/^is_beta=//') | |
| BUILD_NUMBER="${GITHUB_RUN_NUMBER}" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "is_beta=$IS_BETA" >> $GITHUB_OUTPUT | |
| echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT | |
| echo "Using Actions run number as build number: $BUILD_NUMBER for version: $VERSION" | |
| - name: Generate release notes and title | |
| id: generate-release-notes | |
| uses: actions/github-script@v6 | |
| with: | |
| result-encoding: string | |
| script: | | |
| // Fetch the PR and use its title/body for release notes | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const prNumber = context.issue.number; | |
| const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }); | |
| const title = pr.data.title || ( (context.payload.comment && context.payload.comment.body && context.payload.comment.body.includes('beta')) ? 'Beta Release' : 'Release'); | |
| const body = pr.data.body || "- No release notes provided"; | |
| // Set step outputs for downstream jobs | |
| core.setOutput('title', title); | |
| core.setOutput('release_notes', body); | |
| // Also write the release notes to a file for tools that expect a file | |
| const fs = require('fs'); | |
| fs.writeFileSync('release_notes.md', body); | |
| - name: Check if version already released | |
| run: | | |
| NEW_VERSION="v${{ steps.extract-version.outputs.version }}" | |
| git fetch --tags | |
| if git rev-parse "$NEW_VERSION" >/dev/null 2>&1; then | |
| echo "Version $NEW_VERSION already exists as a tag" >> $GITHUB_STEP_SUMMARY | |
| exit 1 | |
| else | |
| echo "Version $NEW_VERSION not found in tags, continuing..." | |
| fi | |
| - name: Sync branch (for stable releases) | |
| if: ${{ steps.check-beta.outputs.is_beta == 'false' }} | |
| uses: devmasx/merge-branch@master | |
| with: | |
| type: now | |
| from_branch: ${{ steps.comment-branch.outputs.base_ref }} | |
| target_branch: ${{ steps.comment-branch.outputs.head_ref }} | |
| github_token: ${{ github.token }} | |
| message: "Sync branch before release" | |
| build: | |
| name: Build and sign app | |
| permissions: | |
| contents: write | |
| runs-on: macos-latest | |
| needs: preparation | |
| env: | |
| DEVELOPMENT_TEAM: ${{ vars.DEVELOPMENT_TEAM_ID }} | |
| CODE_SIGN_IDENTITY: "Apple Development" | |
| steps: | |
| - uses: xt0rted/pull-request-comment-branch@v1 | |
| id: comment-branch | |
| - uses: actions/checkout@v3 | |
| if: success() | |
| with: | |
| ref: ${{ steps.comment-branch.outputs.head_ref }} | |
| persist-credentials: true | |
| - name: Resolve Swift packages | |
| run: xcodebuild -resolvePackageDependencies -project ${{ env.projname }}.xcodeproj | |
| - name: Install Apple certificate | |
| env: | |
| BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} | |
| P12_PASSWORD: ${{ secrets.P12_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| CERT_PATH=$RUNNER_TEMP/build_certificate.p12 | |
| KC=$RUNNER_TEMP/app-signing.keychain-db | |
| echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KC" | |
| security set-keychain-settings -lut 21600 "$KC" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KC" | |
| security import "$CERT_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KC" | |
| security list-keychain -d user -s "$KC" | |
| - name: Switch Xcode version | |
| run: | | |
| sudo xcode-select -s "/Applications/Xcode_16.4.app" | |
| /usr/bin/xcodebuild -version | |
| - name: Set version and build number in project | |
| run: | | |
| sed -i '' "s/MARKETING_VERSION = [^;]*/MARKETING_VERSION = ${{ needs.preparation.outputs.version }}/g" ${{ env.projname }}.xcodeproj/project.pbxproj | |
| sed -i '' "s/CURRENT_PROJECT_VERSION = [^;]*/CURRENT_PROJECT_VERSION = ${{ needs.preparation.outputs.build_number }}/g" ${{ env.projname }}.xcodeproj/project.pbxproj | |
| - name: Commit version changes | |
| run: | | |
| git add ${{ env.projname }}.xcodeproj/project.pbxproj | |
| git commit -m "Set version to v${{ needs.preparation.outputs.version }} (build ${{ needs.preparation.outputs.build_number }})" || echo "No changes to commit" | |
| git push origin HEAD:${{ steps.comment-branch.outputs.head_ref }} || true | |
| - name: Build and archive | |
| run: | | |
| xcodebuild clean archive \ | |
| -project ${{ env.projname }}.xcodeproj \ | |
| -scheme ${{ env.projname }} \ | |
| -archivePath ${{ env.projname }} \ | |
| DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \ | |
| CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \ | |
| -allowProvisioningUpdates | |
| - name: Export app | |
| env: | |
| DEVELOPMENT_TEAM: ${{ vars.DEVELOPMENT_TEAM_ID }} | |
| run: | | |
| TEMP_PLIST="$RUNNER_TEMP/export_options.plist" | |
| cat > "$TEMP_PLIST" <<EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>method</key> | |
| <string>${EXPORT_METHOD}</string> | |
| <key>signingStyle</key> | |
| <string>automatic</string> | |
| <key>teamID</key> | |
| <string>$DEVELOPMENT_TEAM</string> | |
| </dict> | |
| </plist> | |
| EOF | |
| xcodebuild -exportArchive -archivePath "${{ env.projname }}.xcarchive" -exportPath Release -exportOptionsPlist "$TEMP_PLIST" | |
| - name: Check for generate_appcast in repository | |
| run: | | |
| TOOL_PATH="Configuration/sparkle/generate_appcast" | |
| if [ ! -x "$TOOL_PATH" ]; then | |
| echo "Configuration/sparkle/generate_appcast missing or not executable" >&2 | |
| exit 1 | |
| fi | |
| echo "Found generate_appcast at $TOOL_PATH" | |
| - name: Create DMG | |
| run: | | |
| cd Release | |
| hdiutil create -volname "boringNotch ${{ needs.preparation.outputs.version }}" \ | |
| -srcfolder "${{ env.projname }}.app" \ | |
| -ov -format UDZO \ | |
| "${{ env.projname }}.dmg" | |
| - name: Upload DMG artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.projname }}.dmg | |
| path: Release/${{ env.projname }}.dmg | |
| - name: Upload .app artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.projname }}.app | |
| path: Release/${{ env.projname }}.app | |
| publish: | |
| name: Publish Release | |
| runs-on: macos-latest | |
| needs: [preparation, build] | |
| steps: | |
| - uses: xt0rted/pull-request-comment-branch@v1 | |
| id: comment-branch | |
| - uses: actions/checkout@v3 | |
| if: success() | |
| with: | |
| ref: ${{ steps.comment-branch.outputs.head_ref }} | |
| - name: Download DMG artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ${{ env.projname }}.dmg | |
| path: Release | |
| - name: Create HTML release notes | |
| run: | | |
| mkdir -p Release | |
| cat > Release/boringNotch.html <<EOF | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>Boring Notch ${{ needs.preparation.outputs.version }}</title> | |
| </head> | |
| <body> | |
| <h2>BoringNotch ${{ needs.preparation.outputs.version }}</h2> | |
| <div> | |
| ${{ needs.preparation.outputs.release_notes }} | |
| </div> | |
| </body> | |
| </html> | |
| EOF | |
| - name: Check for generate_appcast in repository | |
| run: | | |
| TOOL_PATH="Configuration/sparkle/generate_appcast" | |
| if [ ! -x "$TOOL_PATH" ]; then | |
| echo "Configuration/sparkle/generate_appcast missing or not executable" >&2 | |
| exit 1 | |
| fi | |
| echo "Found generate_appcast at $TOOL_PATH" | |
| - name: Generate signed appcast | |
| run: | | |
| set -euo pipefail | |
| GITHUB_REPO="https://github.com/TheBoredTeam/boring.notch/releases" | |
| DOWNLOAD_PREFIX="https://github.com/TheBoredTeam/boring.notch/releases/download/v${{ needs.preparation.outputs.version }}/" | |
| CHANNEL_ARG="" | |
| if [[ "${{ needs.preparation.outputs.is_beta }}" == "true" ]]; then | |
| CHANNEL_ARG="--channel ${{ env.beta-channel-name }}" | |
| fi | |
| printf '%s' "${{ secrets.PRIVATE_SPARKLE_KEY }}" | ./Configuration/sparkle/generate_appcast \ | |
| --ed-key-file - \ | |
| --link "$GITHUB_REPO" \ | |
| --download-url-prefix "$DOWNLOAD_PREFIX" \ | |
| $CHANNEL_ARG \ | |
| -o updater/appcast.xml \ | |
| Release/ | |
| - name: Commit appcast (and pbxproj for stable) | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| git add updater/appcast.xml | |
| if [[ "${{ needs.preparation.outputs.is_beta }}" == "false" ]]; then | |
| git add ${{ env.projname }}.xcodeproj/project.pbxproj || true | |
| git commit -m "Update version to v${{ needs.preparation.outputs.version }}" || echo "No changes to commit" | |
| else | |
| # git checkout main | |
| # git add updater/appcast.xml | |
| # git commit -m "Update appcast with beta release for v${{ needs.preparation.outputs.version }}" || echo "No changes to commit" | |
| # git push origin main | |
| fi | |
| - name: Create GitHub release | |
| uses: softprops/action-gh-release@v1 | |
| if: false | |
| with: | |
| name: v${{ needs.preparation.outputs.version }} - ${{ needs.preparation.outputs.title }} | |
| tag_name: v${{ needs.preparation.outputs.version }} | |
| fail_on_unmatched_files: true | |
| body: ${{ needs.preparation.outputs.release_notes }} | |
| files: Release/boringNotch.dmg | |
| prerelease: ${{ needs.preparation.outputs.is_beta }} | |
| draft: false | |
| upgrade-brew: | |
| name: Upgrade Homebrew formula | |
| runs-on: macos-latest | |
| needs: [preparation, publish] | |
| if: ${{ needs.preparation.outputs.is_beta == 'false' }} | |
| steps: | |
| - name: Generate Homebrew cask | |
| run: | | |
| DMG_URL="https://github.com/TheBoredTeam/boring.notch/releases/download/v${{ needs.preparation.outputs.version }}/boringNotch.dmg" | |
| NEW_SHA256=$(curl -sL "$DMG_URL" | shasum -a 256 | cut -d' ' -f1) | |
| cat > boring-notch.rb <<EOF | |
| cask "boring-notch" do | |
| version "${{ needs.preparation.outputs.version }}" | |
| sha256 "$NEW_SHA256" | |
| url "$DMG_URL" | |
| name "Boring Notch" | |
| desc "Not so boring notch That Rocks 🎸🎶 " | |
| homepage "https://github.com/TheBoredTeam/boring.notch" | |
| livecheck do | |
| url :url | |
| strategy :github_latest | |
| end | |
| auto_updates true | |
| depends_on macos: ">= :sonoma" | |
| app "boringNotch.app" | |
| zap trash: [ | |
| "~/Library/Application Scripts/theboringteam.boringnotch/", | |
| "~/Library/Containers/theboringteam.boringnotch/", | |
| ] | |
| end | |
| EOF | |
| - name: Upload Homebrew cask artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: homebrew-cask-${{ needs.preparation.outputs.version }} | |
| path: boring-notch.rb | |
| upgrade-brew-beta: | |
| name: Upgrade Homebrew beta formula | |
| runs-on: macos-latest | |
| needs: [preparation, publish] | |
| if: ${{ needs.preparation.outputs.is_beta == 'true' }} | |
| steps: | |
| - name: Generate Homebrew beta cask | |
| run: | | |
| DMG_URL="https://github.com/TheBoredTeam/boring.notch/releases/download/v${{ needs.preparation.outputs.version }}/boringNotch.dmg" | |
| NEW_SHA256=$(curl -sL "$DMG_URL" | shasum -a 256 | cut -d' ' -f1) | |
| cat > [email protected] <<EOF | |
| cask "boring-notch@rc" do | |
| version "${{ needs.preparation.outputs.version }}" | |
| sha256 "$NEW_SHA256" | |
| url "$DMG_URL" | |
| name "Boring Notch RC" | |
| desc "Not so boring notch That Rocks 🎸🎶 (Release Candidate)" | |
| homepage "https://github.com/TheBoredTeam/boring.notch" | |
| livecheck do | |
| url :url | |
| strategy :github_latest | |
| end | |
| auto_updates true | |
| depends_on macos: ">= :sonoma" | |
| app "boringNotch.app" | |
| zap trash: [ | |
| "~/Library/Application Scripts/theboringteam.boringnotch/", | |
| "~/Library/Containers/theboringteam.boringnotch/", | |
| ] | |
| end | |
| EOF | |
| - name: Upload Homebrew beta cask artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: homebrew-cask-${{ needs.preparation.outputs.version }} | |
| path: [email protected] | |
| ending: | |
| name: Ending job | |
| if: ${{ always() && github.event.issue.pull_request && (contains(github.event.comment.body, '/build') || contains(github.event.comment.body, '/release')) }} | |
| runs-on: ubuntu-latest | |
| needs: [preparation, build, publish, upgrade-brew] | |
| steps: | |
| - uses: xt0rted/pull-request-comment-branch@v1 | |
| id: comment-branch | |
| - uses: actions/checkout@v3 | |
| if: ${{ contains(join(needs.*.result, ','), 'success') }} | |
| with: | |
| ref: ${{ steps.comment-branch.outputs.head_ref }} | |
| - name: Merge PR (for stable releases) | |
| uses: devmasx/merge-branch@master | |
| if: ${{ needs.preparation.outputs.is_beta == 'false' && contains(join(needs.*.result, ','), 'success') && false }} | |
| with: | |
| type: now | |
| from_branch: ${{ steps.comment-branch.outputs.head_ref }} | |
| target_branch: ${{ steps.comment-branch.outputs.base_ref }} | |
| github_token: ${{ github.token }} | |
| message: "Release version v${{ needs.preparation.outputs.version }}" | |
| - name: Add success reactions | |
| if: ${{ !contains(join(needs.*.result, ','), 'failure') }} | |
| uses: peter-evans/create-or-update-comment@v4 | |
| with: | |
| comment-id: ${{ github.event.comment.id }} | |
| reactions: rocket | |
| - name: Add negative reaction | |
| if: ${{ contains(join(needs.*.result, ','), 'failure') }} | |
| uses: peter-evans/create-or-update-comment@v4 | |
| with: | |
| comment-id: ${{ github.event.comment.id }} | |
| reactions: confused | |
| - name: Create summary | |
| run: | | |
| BUILD_TYPE="stable" | |
| if [[ "${{ needs.preparation.outputs.is_beta }}" == "true" ]]; then | |
| BUILD_TYPE="beta" | |
| fi | |
| ALL_RESULTS="${{ join(needs.*.result, ',') }}" | |
| if [[ "${ALL_RESULTS}" != *"failure"* ]]; then | |
| echo "✅ Successfully released boringNotch v${{ needs.preparation.outputs.version }} ($BUILD_TYPE build ${{ needs.preparation.outputs.build_number }})" >> $GITHUB_STEP_SUMMARY | |
| echo "🍺 Homebrew cask updated" >> $GITHUB_STEP_SUMMARY | |
| echo "📱 Sparkle appcast updated" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "❌ Release failed for boringNotch v${{ needs.preparation.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| fi |