Skip to content

Add build error detection with automatic iteration via @copilot #10

Add build error detection with automatic iteration via @copilot

Add build error detection with automatic iteration via @copilot #10

Workflow file for this run

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