Skip to content

[Bug] Shelf not working on Tahoe 26.3 #1277

[Bug] Shelf not working on Tahoe 26.3

[Bug] Shelf not working on Tahoe 26.3 #1277

Workflow file for this run

name: "Deploy Boring Notch"
on:
issue_comment:
types: [created]
concurrency:
group: release-${{ github.event.issue.number || github.run_id }}
cancel-in-progress: true
env:
PROJECT_NAME: boringNotch
BETA_CHANNEL_NAME: beta
RELEASE_COMMAND: /release
CODE_SIGN_IDENTITY: "Apple Development"
XCODE_VERSION: "16.4"
permissions:
contents: read
pull-requests: write
jobs:
# helper job to test for the release command in the comment; env is safe to use here
check_release:
name: Check for release command
if: ${{ github.event.issue.pull_request }}
runs-on: ubuntu-latest
outputs:
is_release: ${{ steps.check.outputs.is_release }}
is_eligible_pr: ${{ steps.check.outputs.is_eligible_pr }}
steps:
- id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
COMMENT_BODY: ${{ github.event.comment.body }}
RELEASE_COMMAND: ${{ env.RELEASE_COMMAND }}
run: |
set -euo pipefail
if [[ "$COMMENT_BODY" == *"$RELEASE_COMMAND"* ]]; then
IS_RELEASE=true
else
IS_RELEASE=false
fi
IS_ELIGIBLE_PR=false
PR_NUMBER="${{ github.event.issue.number }}"
PR_DATA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" \
--jq '{head_ref: .head.ref, base_ref: .base.ref, head_repo: .head.repo.full_name, base_repo: .base.repo.full_name}')
HEAD_REF=$(echo "$PR_DATA" | jq -r '.head_ref')
BASE_REF=$(echo "$PR_DATA" | jq -r '.base_ref')
HEAD_REPO=$(echo "$PR_DATA" | jq -r '.head_repo')
BASE_REPO=$(echo "$PR_DATA" | jq -r '.base_repo')
if [[ "$HEAD_REPO" == "$REPO" && "$BASE_REPO" == "$REPO" && "$HEAD_REF" == "dev" && "$BASE_REF" == "main" ]]; then
IS_ELIGIBLE_PR=true
fi
echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
echo "is_eligible_pr=$IS_ELIGIBLE_PR" >> "$GITHUB_OUTPUT"
preparation:
name: Preparation
needs: check_release
if: ${{ needs.check_release.outputs.is_release == 'true' && needs.check_release.outputs.is_eligible_pr == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
is_beta: ${{ steps.extract_version.outputs.is_beta }}
version: ${{ steps.extract_version.outputs.version }}
build_number: ${{ steps.extract_version.outputs.build_number }}
xcode_version: ${{ steps.build_config.outputs.xcode_version }}
code_sign_identity: ${{ steps.build_config.outputs.code_sign_identity }}
title: ${{ steps.release_notes.outputs.title }}
release_notes: ${{ steps.release_notes.outputs.release_notes }}
release_notes_github: ${{ steps.release_notes.outputs.release_notes_github }}
head_ref: ${{ steps.pr_info.outputs.head_ref }}
base_ref: ${{ steps.pr_info.outputs.base_ref }}
steps:
- name: Validate permissions and PR state
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
REPO="${{ github.repository }}"
COMMENTER="${{ github.event.comment.user.login }}"
PR_NUMBER="${{ github.event.issue.number }}"
# Acknowledge the command
gh api "repos/${REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \
-f content=eyes --silent
# Require admin permission
PERM=$(gh api "repos/${REPO}/collaborators/${COMMENTER}/permission" --jq '.permission')
if [[ "$PERM" != "admin" ]]; then
echo "::error::${COMMENTER} is not an admin (permission: ${PERM})"
exit 1
fi
# Require mergeable, non-draft PR
IS_DRAFT=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.draft')
MERGEABLE=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.mergeable')
if [[ "$IS_DRAFT" == "true" || "$MERGEABLE" != "true" ]]; then
echo "::error::PR #${PR_NUMBER} is not ready to merge (draft=${IS_DRAFT}, mergeable=${MERGEABLE})"
exit 1
fi
- name: Get PR branch info
id: pr_info
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
PR_DATA=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}" \
--jq '{head_ref: .head.ref, base_ref: .base.ref}')
echo "head_ref=$(echo "$PR_DATA" | jq -r '.head_ref')" >> "$GITHUB_OUTPUT"
echo "base_ref=$(echo "$PR_DATA" | jq -r '.base_ref')" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.pr_info.outputs.head_ref }}
- name: Ensure scripts directory exists
env:
BASE_REF: ${{ steps.pr_info.outputs.base_ref }}
run: |
if [ ! -f ".github/scripts/extract_version.py" ]; then
echo "Script not found in PR branch, fetching from base branch"
git fetch origin "$BASE_REF"
git checkout "origin/$BASE_REF" -- .github/scripts/
fi
- name: Extract version from comment
id: extract_version
env:
COMMENT: ${{ github.event.comment.body }}
run: |
set -euo pipefail
export projname="${{ env.PROJECT_NAME }}"
OUTPUT=$(python3 .github/scripts/extract_version.py -c "$COMMENT")
VERSION=$(awk -F= '/^version=/{print $2; exit}' <<<"$OUTPUT")
IS_BETA=$(awk -F= '/^is_beta=/{print $2; exit}' <<<"$OUTPUT")
BUILD_NUMBER="${GITHUB_RUN_NUMBER}"
{
echo "version=$VERSION"
echo "is_beta=$IS_BETA"
echo "build_number=$BUILD_NUMBER"
} >> "$GITHUB_OUTPUT"
echo "Version: $VERSION | Beta: $IS_BETA | Build: $BUILD_NUMBER"
- name: Expose build configuration
id: build_config
run: |
{
echo "xcode_version=${{ env.XCODE_VERSION }}"
echo "code_sign_identity=${{ env.CODE_SIGN_IDENTITY }}"
} >> "$GITHUB_OUTPUT"
- name: Generate release notes from PR
id: release_notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
PR_NUMBER="${{ github.event.issue.number }}"
REPO="${{ github.repository }}"
TITLE=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.title // "Release"')
BODY=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.body // "- No release notes provided"')
# Strip H1 headings for GitHub release notes
BODY_GITHUB=$(printf '%s' "$BODY" | sed '/^# /d' | perl -pe 's#<h1[^>]*>.*?</h1>##gi')
{
echo "title=${TITLE}"
echo "release_notes<<RELEASE_NOTES_DELIM"
echo "$BODY"
echo "RELEASE_NOTES_DELIM"
echo "release_notes_github<<RELEASE_NOTES_GH_DELIM"
echo "$BODY_GITHUB"
echo "RELEASE_NOTES_GH_DELIM"
} >> "$GITHUB_OUTPUT"
- name: Check version not already released
env:
VERSION: ${{ steps.extract_version.outputs.version }}
run: |
git fetch --tags
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
echo "::error::Version v${VERSION} already exists as a tag"
exit 1
fi
- name: Sync branch (stable releases only)
if: steps.extract_version.outputs.is_beta == 'false'
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
BASE_REF: ${{ steps.pr_info.outputs.base_ref }}
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config --global credential.https://github.com.helper \
"!f() { echo \"username=x-access-token\"; printf \"password=%s\\n\" \"\${GITHUB_TOKEN}\"; }; f"
git fetch origin "$BASE_REF"
git checkout "$HEAD_REF"
git merge --no-ff "origin/$BASE_REF" -m "Sync branch before release"
git push origin "$HEAD_REF"
build:
name: Build and sign
needs: preparation
permissions:
contents: write
uses: ./.github/workflows/build_reusable.yml
with:
head_ref: ${{ needs.preparation.outputs.head_ref }}
version: ${{ needs.preparation.outputs.version }}
build_number: ${{ needs.preparation.outputs.build_number }}
xcode_version: ${{ needs.preparation.outputs.xcode_version }}
code_sign_identity: ${{ needs.preparation.outputs.code_sign_identity }}
secrets:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
publish:
name: Publish release
needs: [preparation, build]
runs-on: macos-latest
permissions:
contents: write
env:
HEAD_REF: ${{ needs.preparation.outputs.head_ref }}
VERSION: ${{ needs.preparation.outputs.version }}
IS_BETA: ${{ needs.preparation.outputs.is_beta }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ env.HEAD_REF }}
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ env.PROJECT_NAME }}.dmg
path: Release
- name: Create embedded release notes
env:
RELEASE_NOTES: ${{ needs.preparation.outputs.release_notes }}
run: printf '%s' "$RELEASE_NOTES" > Release/boringNotch.html
- name: Build Sparkle generate_appcast from source
run: |
set -euo pipefail
SPARKLE_METADATA_PATH="boringNotch.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
SPARKLE_SRC="$RUNNER_TEMP/Sparkle"
SPARKLE_DERIVED="$RUNNER_TEMP/sparkle-derived"
SPARKLE_REPO_URL="https://github.com/sparkle-project/Sparkle"
IFS=$'\t' read -r SPARKLE_VERSION SPARKLE_REVISION <<< "$(python3 - "$SPARKLE_METADATA_PATH" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
data = json.loads(path.read_text())
pins = data.get("pins", [])
sparkle = next((pin for pin in pins if pin.get("identity") == "sparkle"), None)
if sparkle is None:
print(f"::error::No 'sparkle' pin found in {path}", file=sys.stderr)
sys.exit(1)
state = sparkle.get("state", {})
version = state.get("version")
revision = state.get("revision")
if not version:
print(f"::error::Sparkle pin in {path} is not version-pinned", file=sys.stderr)
sys.exit(1)
if not revision:
print(f"::error::Sparkle pin in {path} has no revision", file=sys.stderr)
sys.exit(1)
print(f"{version}\t{revision}")
PY
)"
echo "Using Sparkle ${SPARKLE_VERSION} (${SPARKLE_REVISION}) from ${SPARKLE_METADATA_PATH}"
SPARKLE_TAG=""
for candidate in "$SPARKLE_VERSION" "v$SPARKLE_VERSION"; do
if git ls-remote --exit-code --tags "$SPARKLE_REPO_URL" "refs/tags/$candidate" >/dev/null; then
SPARKLE_TAG="$candidate"
break
else
ls_remote_exit=$?
if [[ "$ls_remote_exit" -ne 2 ]]; then
echo "::error::Failed to query Sparkle tags from $SPARKLE_REPO_URL while checking $candidate"
exit 1
fi
fi
done
if [[ -z "$SPARKLE_TAG" ]]; then
echo "::error::Could not find Sparkle tag $SPARKLE_VERSION or v$SPARKLE_VERSION"
exit 1
fi
git clone \
--depth 1 \
--branch "$SPARKLE_TAG" \
--single-branch \
"$SPARKLE_REPO_URL" \
"$SPARKLE_SRC"
ACTUAL_REVISION="$(git -C "$SPARKLE_SRC" rev-parse HEAD)"
if [[ "$ACTUAL_REVISION" != "$SPARKLE_REVISION" ]]; then
echo "::error::Sparkle tag ${SPARKLE_TAG} resolves to ${ACTUAL_REVISION}, expected ${SPARKLE_REVISION}"
exit 1
fi
xcodebuild -project "$SPARKLE_SRC/Sparkle.xcodeproj" \
-scheme generate_appcast \
-configuration Release \
-derivedDataPath "$SPARKLE_DERIVED" \
CODE_SIGNING_ALLOWED=NO \
build
APPCAST_BIN="$SPARKLE_DERIVED/Build/Products/Release/generate_appcast"
[[ -x "$APPCAST_BIN" ]] || { echo "::error::generate_appcast was not built"; exit 1; }
install -m 0755 "$APPCAST_BIN" "$RUNNER_TEMP/generate_appcast"
- name: Generate signed appcast
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.PRIVATE_SPARKLE_KEY }}
run: |
set -euo pipefail
test -x "$RUNNER_TEMP/generate_appcast" || {
echo "::error::$RUNNER_TEMP/generate_appcast missing or not executable"; exit 1;
}
CHANNEL_ARGS=()
if [[ "${IS_BETA}" == "true" ]]; then
CHANNEL_ARGS=(--channel "${{ env.BETA_CHANNEL_NAME }}")
fi
printf '%s' "$SPARKLE_PRIVATE_KEY" | "$RUNNER_TEMP/generate_appcast" \
--ed-key-file - \
--link "https://github.com/TheBoredTeam/boring.notch/releases" \
--download-url-prefix "https://github.com/TheBoredTeam/boring.notch/releases/download/v${VERSION}/" \
--embed-release-notes \
"${CHANNEL_ARGS[@]}" \
-o updater/appcast.xml \
Release/
- name: Commit appcast
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if [[ "${IS_BETA}" == "false" ]]; then
git add updater/appcast.xml
git commit -m "Update version to v${VERSION} and appcast" || true
git push origin "HEAD:${HEAD_REF}" || true
else
# Save generated appcast, switch to main, apply and push
cp updater/appcast.xml "$RUNNER_TEMP/appcast.xml"
git fetch origin main
git checkout main
cp "$RUNNER_TEMP/appcast.xml" updater/appcast.xml
git add updater/appcast.xml
git commit -m "Update appcast with beta release for v${VERSION}" || true
git push origin main
fi
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TITLE: ${{ needs.preparation.outputs.title }}
NOTES: ${{ needs.preparation.outputs.release_notes_github }}
run: |
RELEASE_FLAGS=()
if [[ "${IS_BETA}" == "true" ]]; then
RELEASE_FLAGS=(--prerelease)
fi
gh release create "v${VERSION}" Release/boringNotch.dmg \
--title "v${VERSION} - ${TITLE}" \
--notes "$NOTES" \
"${RELEASE_FLAGS[@]}"
upgrade-brew:
name: Update Homebrew cask
needs: [preparation, publish]
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.preparation.outputs.version }}
IS_BETA: ${{ needs.preparation.outputs.is_beta }}
steps:
- name: Generate cask files
run: |
set -euo pipefail
DMG_URL="https://github.com/TheBoredTeam/boring.notch/releases/download/v${VERSION}/boringNotch.dmg"
# Retry SHA calculation (release may need a moment to propagate)
for attempt in 1 2 3; do
if SHA256=$(curl -sL --fail "$DMG_URL" | shasum -a 256 | cut -d' ' -f1); then break; fi
echo "Attempt $attempt failed, retrying in 10s..."; sleep 10
done
[[ -n "${SHA256:-}" ]] || { echo "::error::Failed to download DMG for SHA256"; exit 1; }
write_cask() {
local CASK_NAME="$1" DISPLAY_NAME="$2" DESC="$3"
cat <<CASK
cask "${CASK_NAME}" do
version "${VERSION}"
sha256 "${SHA256}"
url "${DMG_URL}"
name "${DISPLAY_NAME}"
desc "${DESC}"
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"
postflight do
app_path = appdir/"boringNotch.app"
next unless app_path.exist?
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", app_path]
end
uninstall quit: "theboringteam.boringnotch"
zap trash: [
"~/Library/Application Scripts/theboringteam.boringnotch/",
"~/Library/Containers/theboringteam.boringnotch/",
]
end
CASK
}
write_cask "boring-notch@rc" "Boring Notch RC" \
"Not so boring notch That Rocks (Release Candidate)" > boring-notch@rc.rb
if [[ "${IS_BETA}" == "false" ]]; then
write_cask "boring-notch" "Boring Notch" \
"Not so boring notch That Rocks" > boring-notch.rb
fi
- name: Upload cask artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: homebrew-cask-${{ env.VERSION }}
path: boring-notch*.rb
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: TheBoredTeam/homebrew-boring-notch
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: homebrew-tap
- name: Update casks in tap
run: |
set -euo pipefail
cp boring-notch@rc.rb homebrew-tap/Casks/boring-notch@rc.rb
COMMIT_MSG="Update boring-notch@rc to v${VERSION}"
if [[ "${IS_BETA}" == "false" ]]; then
cp boring-notch.rb homebrew-tap/Casks/boring-notch.rb
COMMIT_MSG="Update boring-notch and boring-notch@rc to v${VERSION}"
fi
cd homebrew-tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Casks/
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "$COMMIT_MSG"
git push
fi
ending:
name: Finalize
if: ${{ always() && needs.preparation.result != 'skipped' && needs.check_release.outputs.is_release == 'true' }}
needs: [check_release, preparation, build, publish, upgrade-brew]
runs-on: ubuntu-latest
permissions:
contents: write
env:
ALL_RESULTS: ${{ join(needs.*.result, ',') }}
RELEASE_SUCCEEDED: ${{ !contains(join(needs.*.result, ','), 'failure') && !contains(join(needs.*.result, ','), 'cancelled') }}
steps:
- name: React to trigger comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
REPO="${{ github.repository }}"
COMMENT_ID="${{ github.event.comment.id }}"
if [[ "$ALL_RESULTS" != *"failure"* && "$ALL_RESULTS" != *"cancelled"* ]]; then
REACTION="rocket"
else
REACTION="confused"
fi
gh api "repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \
-f content="$REACTION" --silent
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: needs.preparation.outputs.is_beta == 'false' && env.RELEASE_SUCCEEDED == 'true'
with:
ref: ${{ needs.preparation.outputs.head_ref }}
fetch-depth: 0
- name: Merge PR (stable releases only)
if: needs.preparation.outputs.is_beta == 'false' && env.RELEASE_SUCCEEDED == 'true'
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
HEAD_REF: ${{ needs.preparation.outputs.head_ref }}
BASE_REF: ${{ needs.preparation.outputs.base_ref }}
VERSION: ${{ needs.preparation.outputs.version }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git config --global credential.https://github.com.helper \
"!f() { echo \"username=x-access-token\"; printf \"password=%s\\n\" \"\${GITHUB_TOKEN}\"; }; f"
git fetch origin "$BASE_REF"
git checkout "$BASE_REF"
git merge --no-ff "origin/$HEAD_REF" -m "Release version v${VERSION}"
git push origin "$BASE_REF"
- name: Summary
env:
IS_BETA: ${{ needs.preparation.outputs.is_beta }}
VERSION: ${{ needs.preparation.outputs.version }}
BUILD_NUMBER: ${{ needs.preparation.outputs.build_number }}
shell: bash
run: |
if [[ "${IS_BETA}" == "true" ]]; then
BUILD_TYPE="beta"
else
BUILD_TYPE="stable"
fi
{
if [[ "${RELEASE_SUCCEEDED}" == "true" ]]; then
echo "โœ… Released boringNotch v${VERSION} (${BUILD_TYPE} build ${BUILD_NUMBER})"
echo "๐Ÿบ Homebrew cask updated"
echo "๐Ÿ“ฑ Sparkle appcast updated"
else
echo "โŒ Release failed${VERSION:+ for boringNotch v${VERSION}}"
fi
} >> "$GITHUB_STEP_SUMMARY"