Skip to content

Switch auto-updating to Sparkle, with a staging rehearsal and hand-written release notes - #214

Merged
alexkroman merged 11 commits into
mainfrom
claude/brave-lamport-x3daft
Sep 30, 2026
Merged

alexkroman merged 11 commits into
mainfrom
claude/brave-lamport-x3daft

Conversation

@alexkroman

Copy link
Copy Markdown
Collaborator

Summary

Moves Blurt's auto-updating to Sparkle and adds what's needed to ship it safely.

  • Sparkle updater (0266f8a, ebc873b, bf453fa): UpdaterModel over SPUStandardUpdaterController. Only the shipping bundle ID starts the updater. The Settings → Updates section follows the HIG review.
  • Real key pair (0358a67): SPARKLE_PUBLIC_ED_KEY is set, and the Xcode project is regenerated. The private key is the SPARKLE_ED_PRIVATE_KEY secret on release-build, with a backup outside the keychain.
  • Staging rehearsal (021d449): SUFeedURL is now the BLURT_SPARKLE_FEED_URL build setting.
    • release-build.sh --staging builds an app that checks the sparkle-staging pre-release, which is never marked latest.
    • The build reads SUFeedURL back out of the built app and fails on a mismatch.
    • The workflow's new stage job (and release-stage.sh) uploads there.
    • release-publish.sh refuses to publish a staging build.
  • Changelog in the update window (021d449, b7c4df9, 507fc76): the appcast carries a Markdown <description>. A release needs a hand-written release-notes/X.Y.Z.md:
    • The bump creates it with a TODO that check fails on.
    • check-release-notes.sh lints it: at most 6 bullets of 100 characters, no filler words, emoji, PR numbers, or code names.
    • --new X.Y.Z creates the file for a version that didn't come through a bump.
    • Only staging falls back to commit subjects.

Documented in RELEASE.md → Release notes and Rehearsing an update.

Rehearsal (done)

This ran end to end on CI from release-dry-run-sparkle:

  • rc1 (build 56) was installed from its DMG.
  • rc2 (build 57, with hand-written notes) was staged.
  • rc1 updated itself to build 57. Sparkle logged EdDSA signature is correct for update.
  • The installed app is Developer ID, spctl reports accepted / Notarized Developer ID, and it has no quarantine flag.
  • /releases/latest stayed on v0.1.57 throughout.

The rehearsal caught one bug, fixed in 507fc76: notes under the gitignored docs/ couldn't be committed.

Test plan

  • scripts/check.sh passes locally (full run before 507fc76; --portable after)
  • scripts/release.test.sh passes under bash 5 and macOS bash 3.2
  • Staging rehearsal: rc1 → rc2 update verified
  • Confirm relaunch and that pasting works after the update (Accessibility permission kept)
  • The first real bump (v0.1.58) creates release-notes/0.1.58.md; write the notes in that PR

Notes

  • Copies on v0.1.57 and earlier have no updater, so v0.1.58 is a one-time manual download for them.
  • The release-build environment currently has no branch restriction. RELEASE.md recommends main only, and the rehearsal steps explain the temporary release-dry-run* exception.

🤖 Generated with Claude Code

claude and others added 7 commits September 29, 2026 23:17
Replace the download-only GitHub-API update check with Sparkle 2.10.0
(app target only; the engine stays dependency-free).

- App: UpdaterModel wraps SPUStandardUpdaterController; the app menu,
  menu-bar item and a new Settings → Advanced → Updates section (check
  button, auto-check and auto-install toggles) all drive it. Only the
  shipping bundle id starts the updater, so Blurt Dev never replaces
  itself with the release.
- project.yml: Sparkle package pinned exactly, SUFeedURL pointing at
  releases/latest/download/appcast.xml, SUPublicEDKey from the new
  SPARKLE_PUBLIC_ED_KEY setting (placeholder until the key pair exists).
- Engine: remove UpdateChecker, GitHubRelease, UpdateAlertContent,
  AutomaticUpdateCheck, LastUpdateCheckStore, SemanticVersion,
  HostIdentity.releaseURL and the lastUpdateCheck defaults key.
- Release: sign Sparkle's nested helpers, EdDSA-sign the DMG with
  sign_update, verify the signature against the built app's key, write
  appcast.xml, and attach + verify it on publish. New
  SPARKLE_ED_PRIVATE_KEY secret on release-build.
- Guards/docs: check.sh allowlists the Sparkle URL as the one remote app
  package; retire the "download-only updates" invariant rule and rewrite
  the settled-decision row, guardrails skill, AGENTS.md, RELEASE.md,
  SECURITY.md and READMEs.

project.pbxproj still needs `xcodegen generate` on a Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQUiqVMpAMeReVN9Ej2yBu
Match the button title to the menu item ("Check for Updates…"), use
less update-ambiguous symbols, name updates in the auto-install toggle,
and word the debug footer for the build in hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQUiqVMpAMeReVN9Ej2yBu
Sparkle's update alert can switch automatic installs on; observe both
preferences so an open Settings pane reflects it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FQUiqVMpAMeReVN9Ej2yBu
Replaces the SPARKLE_PUBLIC_ED_KEY placeholder with the real EdDSA public
key so release-build.sh no longer refuses to run, and commits the xcodegen
output for the Sparkle switch (package reference + SU* Info.plist keys).
The private half is the SPARKLE_ED_PRIVATE_KEY secret on release-build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- SUFeedURL is now the BLURT_SPARKLE_FEED_URL build setting, so
  `release-build.sh --staging` can build an app that polls the
  sparkle-staging prerelease (never marked latest) instead of /latest.
  The build reads SUFeedURL back out of the built app, release-stage.sh
  (and the workflow's `stage` job) uploads there, and release-publish.sh
  refuses a staging build.
- The appcast carries a Markdown <description> for the update window.
  A release requires docs/release-notes/X.Y.Z.md: release-bump.sh
  scaffolds it with the commits as a comment plus a TODO, and
  check-release-notes.sh (in check.sh and the build) fails on the TODO,
  filler phrases, emoji, PR numbers, identifiers, and over-long lists.
  Only a staging build falls back to filtered commit subjects.
- RELEASE.md documents the notes rules and the update rehearsal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Stop banning "enhanced": it is part of the "Enhanced transcripts"
  setting name, so accurate notes failed lint and blocked the build.
- Report lint line numbers against the file: comments are blanked but
  their newlines kept, so the bump scaffold's long comment no longer
  shifts every reported line.
- Add `check-release-notes.sh --new X.Y.Z` to scaffold notes for a
  version that didn't come through release-bump.sh (a re-run or
  republish), and point the build's missing-notes error at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/ is local-only working notes (.gitignore), so neither the bump nor a
maintainer could commit docs/release-notes/X.Y.Z.md. Move them to a
top-level release-notes/ and pin that it stays committable in
release.test.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread App/Blurt/project.yml
Comment thread App/Blurt/Blurt.xcodeproj/project.pbxproj
alexkroman-assembly and others added 4 commits September 29, 2026 19:23
The bump now scaffolds release-notes/X.Y.Z.md with a TODO that check
fails on, so following the old steps stalled at the PR. Add the
notes-drafting step (draft, show the user, commit on their go-ahead),
the SPARKLE_ED_PRIVATE_KEY precondition, what the build and publish now
do for the appcast, the staging rehearsal for updater changes, and the
one-time manual update for v0.1.58.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RELEASE.md → Release notes → Writing them covers what the lint can't:
the reader, translating commits into what a user notices (with
commit-to-note examples), what to leave out, ordering, wording, and
v0.1.57's rehearsal notes as the model. The release skill now drafts
from that guide, checks the PRs behind vague subjects, and shows the
user what it left out and why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cleanup (/simplify):
- One helper each for the notes scaffold (release-bump.sh and
  check-release-notes.sh --new), commit subjects since a release, and
  verifying downloads against SHA256SUMS (publish and stage).
- sparkle_release_tag is the one channel -> tag mapping; the enclosure
  and "Version History" URLs derive from it, so the release page URL is
  no longer an untested inline if in the build.
- Publish and stage compare the build channel exactly, so a build-info
  without one is refused instead of read as a release.
- lint_release_notes: nocasematch instead of a tr per line, one perl
  pass for emoji instead of one per line.
- RELEASE.md points at RELEASE_NOTES_BANNED instead of a copy that had
  drifted (it still banned "enhanced"), and no longer calls updates a
  manual DMG download in the cert-rotation section.

Fixes (/code-review):
- "Reset Blurt" clears the app's whole defaults domain after the engine
  keys, so Sparkle's SU* settings (auto-install) don't survive it.
- release-bump.sh checks for an earlier release tag before editing
  project.yml, so that failure can't leave a dirty tree.
- The scaffold neutralizes every run of dashes, not just pairs, so a
  "--->" in a commit subject can't close the context comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#186 renamed STT/ConversationContext.swift to STTPrompt.swift, but
mutate.sh's default targets still named the old file, so the script
refused to start. Nothing noticed because the full run is deliberately
kept out of check.sh.

- Swap the dead target for STTPrompt.swift (still pure logic, no imports).
- check.sh now runs `mutate.sh --list`, which validates every target and
  enumerates mutants without building or testing, so it runs in --portable.
- --list no longer backs up and restores targets: the restore's cp bumped
  every target's mtime, which would force a recompile in check.sh's Swift
  build. restore_all uses an `if` so a missing backup doesn't fail the
  EXIT trap under errexit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alexkroman
alexkroman added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit 4f679ac Sep 30, 2026
10 checks passed
@alexkroman
alexkroman deleted the claude/brave-lamport-x3daft branch September 30, 2026 03:06
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