Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build_reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ on:
required: false
type: string
default: "Apple Development"
commit_version_changes:
required: false
type: boolean
default: true

jobs:
build:
Expand Down Expand Up @@ -84,6 +88,7 @@ jobs:
sed -i '' "s/CURRENT_PROJECT_VERSION = [^;]*/CURRENT_PROJECT_VERSION = ${BUILD_NUMBER}/g" "$PBXPROJ"

- name: Commit version changes
if: ${{ inputs.commit_version_changes }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
Expand Down
169 changes: 169 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
name: Nightly Branch Builds

on:
push:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think nightly builds should be on every push, this should be a job that runs once a day

branches:
- main
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop the main nightly (at least for now), as main only gets code changes that are released into stable

- dev
paths:
- "boringNotch/**"
- "BoringNotchXPCHelper/**"
- "boringNotch.xcodeproj/**"
- "Configuration/**"
- "mediaremote-adapter/**"

concurrency:
group: nightly-${{ github.ref_name }}
cancel-in-progress: true

permissions:
contents: write

env:
PROJECT_NAME: boringNotch

jobs:
prepare:
name: Prepare nightly metadata
runs-on: ubuntu-latest
outputs:
branch_name: ${{ steps.meta.outputs.branch_name }}
commit_sha: ${{ steps.meta.outputs.commit_sha }}
short_sha: ${{ steps.meta.outputs.short_sha }}
tag: ${{ steps.meta.outputs.tag }}
appcast_file: ${{ steps.meta.outputs.appcast_file }}
build_number: ${{ steps.meta.outputs.build_number }}
asset_name: ${{ steps.meta.outputs.asset_name }}
steps:
- name: Compute metadata
id: meta
run: |
set -euo pipefail
BRANCH_NAME="${GITHUB_REF_NAME}"
COMMIT_SHA="${GITHUB_SHA}"
SHORT_SHA="${COMMIT_SHA::7}"
TAG="nightly-${BRANCH_NAME}-${SHORT_SHA}"
APPCAST_FILE="appcast-${BRANCH_NAME}.xml"
BUILD_NUMBER="$(date -u +%Y%m%d%H%M%S)$(printf '%06d%02d' "${GITHUB_RUN_NUMBER:-0}" "${GITHUB_RUN_ATTEMPT:-0}")"
ASSET_NAME="boringNotch-${BRANCH_NAME}-${SHORT_SHA}.dmg"
{
echo "branch_name=$BRANCH_NAME"
echo "commit_sha=$COMMIT_SHA"
echo "short_sha=$SHORT_SHA"
echo "tag=$TAG"
echo "appcast_file=$APPCAST_FILE"
echo "build_number=$BUILD_NUMBER"
echo "asset_name=$ASSET_NAME"
} >> "$GITHUB_OUTPUT"

build:
name: Build nightly DMG
needs: [prepare]
uses: ./.github/workflows/build_reusable.yml
with:
head_ref: ${{ needs.prepare.outputs.branch_name }}
version: ""
build_number: ${{ needs.prepare.outputs.build_number }}
xcode_version: "16.4"
code_sign_identity: "Apple Development"
commit_version_changes: false
secrets:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}

publish:
name: Publish nightly channel
needs: [prepare, build]
runs-on: macos-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SPARKLE_PRIVATE_KEY: ${{ secrets.PRIVATE_SPARKLE_KEY }}
BRANCH_NAME: ${{ needs.prepare.outputs.branch_name }}
COMMIT_SHA: ${{ needs.prepare.outputs.commit_sha }}
SHORT_SHA: ${{ needs.prepare.outputs.short_sha }}
TAG: ${{ needs.prepare.outputs.tag }}
APPCAST_FILE: ${{ needs.prepare.outputs.appcast_file }}
ASSET_NAME: ${{ needs.prepare.outputs.asset_name }}
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
steps:
- name: Checkout branch
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
ref: ${{ env.BRANCH_NAME }}

- name: Download DMG artifact
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: ${{ env.PROJECT_NAME }}.dmg
path: Release

- name: Rename DMG to immutable name
run: |
set -euo pipefail
mv "Release/${PROJECT_NAME}.dmg" "Release/${ASSET_NAME}"

- name: Create embedded release notes
run: |
ESCAPED_COMMIT_MESSAGE="$(printf '%s' "$COMMIT_MESSAGE" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g')"
cat > Release/boringNotch.html <<HTML
<h2>Nightly ${BRANCH_NAME}</h2>
<p>Commit: <code>${COMMIT_SHA}</code></p>
<p>Message: ${ESCAPED_COMMIT_MESSAGE}</p>
<p>Source: <a href="https://github.com/TheBoredTeam/boring.notch/commit/${COMMIT_SHA}">View commit</a></p>
HTML

- name: Generate signed nightly appcast
run: |
set -euo pipefail
test -x Configuration/sparkle/generate_appcast || {
echo "::error::Configuration/sparkle/generate_appcast missing or not executable"; exit 1;
}
DOWNLOAD_PREFIX="https://github.com/TheBoredTeam/boring.notch/releases/download/${TAG}/"
printf '%s' "$SPARKLE_PRIVATE_KEY" | ./Configuration/sparkle/generate_appcast \
Comment on lines +124 to +125
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike the release workflow, this job doesn’t verify that Configuration/sparkle/generate_appcast exists and is executable before invoking it. Adding an explicit check with a clear error will make failures easier to diagnose (and avoids a more cryptic “file not found/permission denied” error).

Suggested change
DOWNLOAD_PREFIX="https://github.com/TheBoredTeam/boring.notch/releases/download/${TAG}/"
printf '%s' "$SPARKLE_PRIVATE_KEY" | ./Configuration/sparkle/generate_appcast \
GEN_APPCAST="./Configuration/sparkle/generate_appcast"
if [ ! -x "$GEN_APPCAST" ]; then
echo "Error: Expected appcast generator '$GEN_APPCAST' to exist and be executable." >&2
exit 1
fi
DOWNLOAD_PREFIX="https://github.com/TheBoredTeam/boring.notch/releases/download/${TAG}/"
printf '%s' "$SPARKLE_PRIVATE_KEY" | "$GEN_APPCAST" \

Copilot uses AI. Check for mistakes.
--ed-key-file - \
--link "https://github.com/TheBoredTeam/boring.notch/commits/${BRANCH_NAME}" \
--download-url-prefix "${DOWNLOAD_PREFIX}" \
--embed-release-notes \
--channel "${BRANCH_NAME}" \
-o "updater/${APPCAST_FILE}" \
Release/

- name: Create immutable nightly release
run: |
set -euo pipefail
TITLE="Nightly ${BRANCH_NAME}"
NOTES="Automated nightly build for ${BRANCH_NAME}. Commit: ${SHORT_SHA}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists; keeping immutable release."
else
gh release create "$TAG" \
--title "$TITLE" \
--notes "$NOTES" \
--prerelease \
--target "$COMMIT_SHA"
fi

- name: Upload immutable nightly DMG
run: |
set -euo pipefail
if gh release view "$TAG" --json assets --jq '.assets[].name' | grep -Fxq "$ASSET_NAME"; then
echo "Asset $ASSET_NAME already exists in $TAG; skipping upload."
else
gh release upload "$TAG" "Release/${ASSET_NAME}"
fi

- name: Commit branch appcast pointer
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add "updater/${APPCAST_FILE}"
if git diff --cached --quiet; then
echo "No changes detected in ${APPCAST_FILE}; skipping commit and push."
exit 0
fi
git commit -m "Update ${APPCAST_FILE} to ${SHORT_SHA}"
git push origin "HEAD:${BRANCH_NAME}"
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:
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}"
BUILD_NUMBER="$(date -u +%Y%m%d%H%M%S)$(printf '%06d%02d' "${GITHUB_RUN_NUMBER:-0}" "${GITHUB_RUN_ATTEMPT:-0}")"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "is_beta=$IS_BETA" >> "$GITHUB_OUTPUT"
echo "build_number=$BUILD_NUMBER" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -175,19 +175,19 @@ jobs:
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 }}
xcode_version: ${{ env.XCODE_VERSION }}
code_sign_identity: ${{ env.CODE_SIGN_IDENTITY }}
secrets:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}

publish:
name: Publish release
Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ Please submit all translations to [Crowdin](https://crowdin.com/project/boring-n

4. **Be patient**: Reviews take time. Maintainers will get to your PR as soon as they can.

### Release Channels (Maintainers)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we need this here, its not really related to contributing guidelines


- **Stable/Beta** are produced by the release workflow.
- **Nightly (Main/Dev)** builds are produced on code pushes to `main` and `dev` branches.
- Nightly builds are not generated from the `pull_request` event itself.

<!-- ## Code Style Guidelines

- Follow the existing code style and conventions used in the project
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ brew install --cask TheBoredTeam/boring-notch/boring-notch --no-quarantine
- Use the controls to manage your music like a rockstar.
- Click the star in your menu bar to customize your notch to your heart's content.

## Update Channels

Boring Notch supports multiple update channels in **Settings → About → Software updates**:

- **Stable**: Official public releases.
- **Beta**: Prerelease builds from the release pipeline.
- **Main (Nightly)**: Automated nightly builds from the `main` branch.
- **Dev (Nightly)**: Automated nightly builds from the `dev` branch.

> [!NOTE]
> Nightly channels are generated from code pushes to their branch (not from pull request events).

## 📋 Roadmap
- [x] Playback live activity 🎧
- [x] Calendar integration 📆
Expand Down Expand Up @@ -188,4 +200,3 @@ For a full list of licenses and attributions, please see the [Third-Party Licens
- **SwiftUI**: For making us look like coding wizards.
- **You**: For being awesome and checking out **boring.notch**!


4 changes: 3 additions & 1 deletion boringNotch/boringNotchApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ struct DynamicNotchApp: App {
@Default(.menubarIcon) var showMenuBarIcon
@Environment(\.openWindow) var openWindow

let updaterDelegate: UpdateChannelUpdaterDelegate
let updaterController: SPUStandardUpdaterController

init() {
updaterDelegate = UpdateChannelUpdaterDelegate()
updaterController = SPUStandardUpdaterController(
startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
startingUpdater: true, updaterDelegate: updaterDelegate, userDriverDelegate: nil)

// Initialize the settings window controller with the updater controller
SettingsWindowController.shared.setUpdaterController(updaterController)
Expand Down
39 changes: 33 additions & 6 deletions boringNotch/components/Settings/SoftwareUpdater.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@
//

import SwiftUI
import Defaults
import Sparkle

final class UpdateChannelUpdaterDelegate: NSObject, SPUUpdaterDelegate {
@objc func feedURLString(for updater: SPUUpdater) -> String? {
Defaults[.updateChannel].feedURLString
}

@objc func allowedChannels(for updater: SPUUpdater) -> Set<String> {
Defaults[.updateChannel].allowedSparkleChannels
}
}

final class CheckForUpdatesViewModel: ObservableObject {
@Published var canCheckForUpdates = false

Expand Down Expand Up @@ -37,6 +48,7 @@ struct CheckForUpdatesView: View {
struct UpdaterSettingsView: View {
private let updater: SPUUpdater

@Default(.updateChannel) private var updateChannel
@State private var automaticallyChecksForUpdates: Bool
@State private var automaticallyDownloadsUpdates: Bool

Expand All @@ -47,21 +59,36 @@ struct UpdaterSettingsView: View {
}

var body: some View {
Section {
Section(
header: HStack {
Text("Software updates")
},
footer: Text(
NSLocalizedString(
"Stable and Beta come from official releases. Main and Dev use nightly builds from those branches.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about making nightly builds available here. Especially without any testing, these nightly builds will often be completely broken, so I feel like a seprating this might make more sense, but I'm not sure yet

comment: "Software updates channel footer"
)
)
) {
Picker(
NSLocalizedString("Update channel", comment: "Software updates channel picker label"),
selection: $updateChannel
) {
ForEach(UpdateChannel.allCases) { channel in
Text(channel.title).tag(channel)
}
}

Toggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates)
.onChange(of: automaticallyChecksForUpdates) { _, newValue in
updater.automaticallyChecksForUpdates = newValue
}

Toggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates)
.disabled(!automaticallyChecksForUpdates)
.onChange(of: automaticallyDownloadsUpdates) { _, newValue in
updater.automaticallyDownloadsUpdates = newValue
}
} header: {
HStack {
Text("Software updates")
}
}
}
}
Loading