Add Sparkle update channels (stable/beta/nightly) with nightly pipeline#1078
Add Sparkle update channels (stable/beta/nightly) with nightly pipeline#1078notluquis wants to merge 4 commits intoTheBoredTeam:devfrom
Conversation
…out for software updates
There was a problem hiding this comment.
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
UpdateChannelpreference, UI picker, and Sparkle delegate that selects feed URL + allowed channels dynamically. - Adds a new
nightly.ymlworkflow 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. |
.github/workflows/release.yml
Outdated
| 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)" |
There was a problem hiding this comment.
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.
| 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}")" |
| 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 }} |
There was a problem hiding this comment.
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.
.github/workflows/nightly.yml
Outdated
| 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)" |
There was a problem hiding this comment.
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.
| 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}" |
.github/workflows/nightly.yml
Outdated
| cat > Release/boringNotch.html <<HTML | ||
| <h2>Nightly ${BRANCH_NAME}</h2> | ||
| <p>Commit: <code>${COMMIT_SHA}</code></p> | ||
| <p>Message: ${COMMIT_MESSAGE}</p> |
There was a problem hiding this comment.
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).
| 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/&/\&/g' -e 's/</\</g' -e 's/>/\>/g') | |
| cat > Release/boringNotch.html <<HTML | |
| <h2>Nightly ${BRANCH_NAME}</h2> | |
| <p>Commit: <code>${COMMIT_SHA}</code></p> | |
| <p>Message: ${ESCAPED_COMMIT_MESSAGE}</p> |
| DOWNLOAD_PREFIX="https://github.com/TheBoredTeam/boring.notch/releases/download/${TAG}/" | ||
| printf '%s' "$SPARKLE_PRIVATE_KEY" | ./Configuration/sparkle/generate_appcast \ |
There was a problem hiding this comment.
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).
| 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" \ |
.github/workflows/nightly.yml
Outdated
| git commit -m "Update ${APPCAST_FILE} to ${SHORT_SHA}" || true | ||
| git push origin "HEAD:${BRANCH_NAME}" || true |
There was a problem hiding this comment.
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.
| 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 suggestions already addressed. |
Alexander5015
left a comment
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Don't think we need this here, its not really related to contributing guidelines
| on: | ||
| push: | ||
| branches: | ||
| - main |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.", |
There was a problem hiding this comment.
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
Summary
This PR adds first-class Sparkle update channel support and aligns CI/CD publishing with channel-based updates.
App changes
StableBetaMain (Nightly)Dev (Nightly)feedURLString(for:))allowedChannels(for:))Workflow changes
.github/workflows/nightly.ymlmainanddev(notpull_request)updater/appcast-main.xmlupdater/appcast-dev.xmlcommit_version_changesinput to support nightly without committing version bumpsDocs
README.mdCONTRIBUTING.mdWhy
Notes
main/devwith app-code path filters.betachannel metadata.