Skip to content

Add Sparkle update channels (stable/beta/nightly) with nightly pipeline#1078

Open
notluquis wants to merge 4 commits intoTheBoredTeam:devfrom
notluquis:feature/sparkle-update-channels
Open

Add Sparkle update channels (stable/beta/nightly) with nightly pipeline#1078
notluquis wants to merge 4 commits intoTheBoredTeam:devfrom
notluquis:feature/sparkle-update-channels

Conversation

@notluquis
Copy link

Summary

This PR adds first-class Sparkle update channel support and aligns CI/CD publishing with channel-based updates.

App changes

  • Add update channel selector in Settings → About → Software updates
  • Channels available:
    • Stable
    • Beta
    • Main (Nightly)
    • Dev (Nightly)
  • Configure Sparkle updater delegate to use:
    • Dynamic feed URL (feedURLString(for:))
    • Allowed channels (allowedChannels(for:))

Workflow changes

  • Add new workflow: .github/workflows/nightly.yml
    • Runs on code pushes to main and dev (not pull_request)
    • Builds signed DMG via reusable build workflow
    • Publishes immutable per-commit nightly releases
    • Generates signed branch appcasts:
      • updater/appcast-main.xml
      • updater/appcast-dev.xml
    • Commits appcast pointer updates back to the corresponding branch
  • Update reusable build workflow with commit_version_changes input to support nightly without committing version bumps
  • Update release workflow build number generation to UTC timestamp for monotonic cross-channel update ordering

Docs

  • Add update channel documentation to README.md
  • Add maintainer notes for release/nightly channel behavior to CONTRIBUTING.md

Why

  • Enable users/testers to explicitly choose between stable, beta, and nightly tracks
  • Keep Sparkle channel behavior consistent with release metadata
  • Avoid version-order issues when switching channels
  • Keep nightly artifacts traceable and immutable by commit

Notes

  • Nightly builds are generated from branch pushes to main/dev with app-code path filters.
  • Beta continues to come from the release workflow using Sparkle beta channel metadata.

Copilot AI review requested due to automatic review settings March 2, 2026 21:10
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class Sparkle update channel support (stable/beta/nightly per branch) and updates CI/CD so nightly builds are published and consumed via channel-specific appcasts.

Changes:

  • Introduces an UpdateChannel preference, UI picker, and Sparkle delegate that selects feed URL + allowed channels dynamically.
  • Adds a new nightly.yml workflow to build and publish per-commit nightly releases and branch-specific appcasts (appcast-main.xml, appcast-dev.xml).
  • Updates release/build workflows to support nightlies (optional version-commit behavior) and uses UTC timestamp build numbers for ordering.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
boringNotch/models/Constants.swift Adds UpdateChannel enum and persists selected channel via Defaults.
boringNotch/components/Settings/SoftwareUpdater.swift Adds Sparkle delegate + Settings picker for channel selection.
boringNotch/boringNotchApp.swift Wires Sparkle controller to the new updater delegate.
README.md Documents update channels and nightly behavior for users.
CONTRIBUTING.md Adds maintainer notes about release/nightly channel behavior.
.github/workflows/release.yml Switches build numbers to UTC timestamps; adjusts reusable build inputs.
.github/workflows/nightly.yml New nightly pipeline: build, release, generate/sign appcast, commit pointer updates.
.github/workflows/build_reusable.yml Adds commit_version_changes input to optionally skip committing PBXProj updates.

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)"
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.

BUILD_NUMBER is generated with second-level precision. If two runs start within the same second (eg, re-run/cancel+rerun), this can produce identical build numbers, which can break Sparkle update ordering/deduping. Consider appending ${GITHUB_RUN_NUMBER}/${GITHUB_RUN_ATTEMPT} (or using nanoseconds) to guarantee uniqueness while keeping monotonic ordering.

Suggested change
BUILD_NUMBER="$(date -u +%Y%m%d%H%M%S)"
BUILD_NUMBER="$(date -u +%Y%m%d%H%M%S)$(printf '%06d%02d' "${GITHUB_RUN_NUMBER:-0}" "${GITHUB_RUN_ATTEMPT:-0}")"

Copilot uses AI. Check for mistakes.
Comment on lines 175 to +184
build:
name: Build and sign
needs: preparation
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 }}
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.

This job calls the reusable build workflow, which contains a git push step. Because this workflow’s top-level permissions set contents: read and this job doesn’t override it, the called workflow will also only have read access, causing the push to fail (and the failure is currently easy to miss). Set permissions: contents: write on this build job (or disable commit_version_changes here) so the release process is deterministic.

Copilot uses AI. Check for mistakes.
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)"
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.

BUILD_NUMBER is derived from a UTC timestamp with only second precision. On fast successive pushes (or a canceled run restarting quickly), two different commits can end up with the same build number, which can confuse Sparkle’s update ordering. Consider including ${GITHUB_RUN_NUMBER}/${GITHUB_RUN_ATTEMPT} (or using higher precision time) to ensure uniqueness.

Suggested change
BUILD_NUMBER="$(date -u +%Y%m%d%H%M%S)"
BUILD_NUMBER="$(date -u +%Y%m%d%H%M%S)${GITHUB_RUN_NUMBER}${GITHUB_RUN_ATTEMPT}"

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +113
cat > Release/boringNotch.html <<HTML
<h2>Nightly ${BRANCH_NAME}</h2>
<p>Commit: <code>${COMMIT_SHA}</code></p>
<p>Message: ${COMMIT_MESSAGE}</p>
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.

The generated HTML release notes interpolate ${COMMIT_MESSAGE} directly. Commit messages can contain characters/newlines that break the HTML, and (more importantly) can inject arbitrary HTML into the Sparkle release notes view. Escape/sanitize the commit message before embedding it, or render it as plain text (eg, inside <pre> with escaping).

Suggested change
cat > Release/boringNotch.html <<HTML
<h2>Nightly ${BRANCH_NAME}</h2>
<p>Commit: <code>${COMMIT_SHA}</code></p>
<p>Message: ${COMMIT_MESSAGE}</p>
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>

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +121
DOWNLOAD_PREFIX="https://github.com/TheBoredTeam/boring.notch/releases/download/${TAG}/"
printf '%s' "$SPARKLE_PRIVATE_KEY" | ./Configuration/sparkle/generate_appcast \
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.
Comment on lines +160 to +161
git commit -m "Update ${APPCAST_FILE} to ${SHORT_SHA}" || true
git push origin "HEAD:${BRANCH_NAME}" || true
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.

The git commit/git push are followed by || true, which will mark the workflow successful even if the appcast update fails to be committed/pushed (eg, due to branch protection or a race). This can leave the nightly channel silently stale. Consider failing the job on push failure, or at least emitting an explicit error and exiting non-zero when the push doesn’t succeed.

Suggested change
git commit -m "Update ${APPCAST_FILE} to ${SHORT_SHA}" || true
git push origin "HEAD:${BRANCH_NAME}" || true
if git diff --cached --quiet; then
echo "No changes detected in ${APPCAST_FILE}; skipping commit and push."
else
git commit -m "Update ${APPCAST_FILE} to ${SHORT_SHA}"
if ! git push origin "HEAD:${BRANCH_NAME}"; then
echo "Error: Failed to push appcast update to ${BRANCH_NAME}. Nightly channel may be stale." >&2
exit 1
fi
fi

Copilot uses AI. Check for mistakes.
@notluquis
Copy link
Author

Copilot suggestions already addressed.

Copy link
Member

@Alexander5015 Alexander5015 left a comment

Choose a reason for hiding this comment

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

I haven't had a chance to fully review the implementation yet, but I left some comments. I was also wondering what the testing process for this looked like?


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

on:
push:
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

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

},
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants