Skip to content

Revert Sparkle manual update dialog flow from #1908#2090

Merged
austinywang merged 2 commits intomainfrom
issue-2028-update-dialog-popover
Mar 25, 2026
Merged

Revert Sparkle manual update dialog flow from #1908#2090
austinywang merged 2 commits intomainfrom
issue-2028-update-dialog-popover

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented Mar 25, 2026

Summary

Testing

  • ./scripts/reload.sh --tag issue-2028-update-dialog-popover

Notes

  • full UI and unit suites were not run locally per repo policy
  • leaves unrelated local CLAUDE.md edits out of the PR

Summary by CodeRabbit

  • Refactor

    • Streamlined update-checking mechanism to a single unified flow.
    • Simplified update readiness logic by removing presentation-handling state.
  • Tests

    • Updated update-related UI tests to validate new sidebar update notification presentation.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 25, 2026 3:30am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

The PR removes the Sparkle dialog presentation infrastructure and presentation-specific routing from the update flow, consolidating all update checking into a unified code path. The UpdateUserInitiatedCheckPresentation concept, associated state management, and helper methods are eliminated. Tests are updated to verify sidebar pill-based notifications instead of modal dialog behavior.

Changes

Cohort / File(s) Summary
Update flow consolidation
Sources/Update/UpdateController.swift, Sources/Update/UpdateDriver.swift
Removed latestItemProbe, presentation-specific routing, and Sparkle dialog wiring. Consolidated update checking to a single path via checkForUpdates()checkForUpdatesWhenReady(). Removed UpdateUserInitiatedCheckPresentation enum routing and presentation state tracking (pendingUserInitiatedCheckPresentation, activeUserInitiatedCheckPresentation).
Delegate simplification
Sources/Update/UpdateDelegate.swift
Removed conditional logic forwarding user choices to SPUStandardUserDriver. Delegate now ignores user-initiated state and unconditionally updates the cmux viewModel.
Test updates
cmuxUITests/SidebarHelpMenuUITests.swift
Replaced dialog-based test (testHelpMenuCheckForUpdatesShowsSparkleDialogOnFirstAttempt) with pill-based test (testHelpMenuCheckForUpdatesTriggersSidebarUpdatePill). Removed locale configuration helper and Sparkle UI assertions; added sidebar UpdatePill verification.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 No more dialogs blocking the way,
A unified path for update day!
The pill now shines in the sidebar's grace,
Simpler code in a cleaner space. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR partially addresses issue #2028. It reverts the dialog flow, but the objective states both presentation styles should be supported—dialog for menu and popover for sidebar. This PR removes all dialog handling entirely. Clarify whether the goal is to revert to pre-#1908 behavior completely or to implement dual presentation modes (dialog and popover by entry point) as specified in issue #2028.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes are focused on reverting the Sparkle dialog flow and simplifying the update controller. All modifications align with the stated objective of reverting PR #1908 and restoring inline update-pill behavior.
Title check ✅ Passed The title accurately and specifically describes the main changes: restoring inline sidebar update checks and embedding appcast changelog, which aligns with the core PR objectives.
Description check ✅ Passed The description covers all required template sections: summary (what changed and why), testing (shell commands and manual verification), and includes the checklist. However, no demo video URL is provided for the UI changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2028-update-dialog-popover

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@austinywang austinywang changed the title Restore inline sidebar update checks and embed appcast changelog Revert Sparkle manual update dialog flow from #1908 Mar 25, 2026
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 10 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/sparkle_generate_appcast.sh">

<violation number="1" location="scripts/sparkle_generate_appcast.sh:96">
P2: Do not swallow changelog extraction errors with `|| true`; it hides real failures and silently drops embedded release notes.</violation>
</file>

<file name="Sources/Update/UpdatePill.swift">

<violation number="1" location="Sources/Update/UpdatePill.swift:32">
P2: Sidebar update pill click now uses the dialog route instead of the inline/custom update flow, creating inconsistent in-app update behavior.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread scripts/sparkle_generate_appcast.sh Outdated
fi

if [[ -z "$APPCAST_CHANGELOG_TEXT" && -f "$CHANGELOG_PATH" ]]; then
APPCAST_CHANGELOG_TEXT="$(python3 scripts/appcast_changelog.py --tag "$TAG" --changelog "$CHANGELOG_PATH" || true)"
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 25, 2026

Choose a reason for hiding this comment

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

P2: Do not swallow changelog extraction errors with || true; it hides real failures and silently drops embedded release notes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/sparkle_generate_appcast.sh, line 96:

<comment>Do not swallow changelog extraction errors with `|| true`; it hides real failures and silently drops embedded release notes.</comment>

<file context>
@@ -90,6 +92,10 @@ if [[ ! -f "$generated_appcast_path" ]]; then
 fi
 
+if [[ -z "$APPCAST_CHANGELOG_TEXT" && -f "$CHANGELOG_PATH" ]]; then
+  APPCAST_CHANGELOG_TEXT="$(python3 scripts/appcast_changelog.py --tag "$TAG" --changelog "$CHANGELOG_PATH" || true)"
+fi
+
</file context>
Fix with Cubic

Comment thread Sources/Update/UpdatePill.swift Outdated
if model.showsDetectedBackgroundUpdate {
showPopover = false
AppDelegate.shared?.checkForUpdates(nil)
AppDelegate.shared?.checkForUpdatesInDialog(nil)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 25, 2026

Choose a reason for hiding this comment

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

P2: Sidebar update pill click now uses the dialog route instead of the inline/custom update flow, creating inconsistent in-app update behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Update/UpdatePill.swift, line 32:

<comment>Sidebar update pill click now uses the dialog route instead of the inline/custom update flow, creating inconsistent in-app update behavior.</comment>

<file context>
@@ -29,7 +29,7 @@ struct UpdatePill: View {
             if model.showsDetectedBackgroundUpdate {
                 showPopover = false
-                AppDelegate.shared?.checkForUpdates(nil)
+                AppDelegate.shared?.checkForUpdatesInDialog(nil)
                 return
             }
</file context>
Suggested change
AppDelegate.shared?.checkForUpdatesInDialog(nil)
AppDelegate.shared?.checkForUpdates(nil)
Fix with Cubic

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 25, 2026

Greptile Summary

This PR restores the two-path update UX: the sidebar/in-app "Check for Updates" action now uses the inline UpdatePill flow (.custom presentation), while the macOS app-menu, menu-bar-extra, and background-update pill clicks route through Sparkle's standard dialog (.dialog presentation). It also adds a new appcast_changelog.py script that extracts and formats plain-text release notes from CHANGELOG.md and injects them into the generated appcast <description> element, so Sparkle can show changelog text inline in its update dialog.

Key changes:

  • UpdateController now has two distinct public methods — checkForUpdates() (.custom) and checkForUpdatesInDialog() (.dialog) — replacing the prior single method that always used .dialog
  • AppDelegate wires the new methods to the correct call sites and adds a #if DEBUG env-var hook (CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG) for UI-test coverage of the dialog path
  • UpdateTestURLProtocol is extended to serve <description> and <sparkle:fullReleaseNotesLink> in the test feed, enabling testDialogTriggeredCheckForUpdatesShowsSparkleDialogWithChangelog
  • Two existing UI tests are updated to assert the inline pill (not Sparkle dialog buttons) for sidebar-triggered checks; a new test verifies the dialog path and that changelog text surfaces in the dialog
  • sparkle_generate_appcast.sh integrates appcast_changelog.py to embed release notes at build/release time, with CI coverage via tests/test_appcast_changelog_extraction.sh

Confidence Score: 5/5

  • Safe to merge — the routing split is clean, tests cover both paths, and the only findings are two non-blocking style suggestions in the build scripts.
  • The Swift-side changes are minimal and well-isolated: the controller split is correct, #if DEBUG guards are in place, and the UpdateDelegate.feedURLString(for:) correctly handles UpdateTestURLProtocol registration for the dialog UI-test path without needing CMUX_UI_TEST_MODE. The Python changelog extractor and shell injection logic are sound; the two flagged issues (re.subn backreference risk and missing *-bullet handling) are low-probability edge cases in a controlled build environment and do not affect the primary user path.
  • scripts/sparkle_generate_appcast.sh (re.subn replacement string) and scripts/appcast_changelog.py (* bullet handling) have minor style suggestions, but neither blocks merging.

Important Files Changed

Filename Overview
Sources/Update/UpdateController.swift Splits the single checkForUpdates() (previously .dialog) into two methods: checkForUpdates() → inline .custom presentation, and checkForUpdatesInDialog() → Sparkle .dialog. Clean, well-commented separation.
Sources/AppDelegate.swift Adds checkForUpdatesInDialog() wrapping the new controller method; routes menu-bar-extra and app-menu "Check for Updates" to dialog; adds #if DEBUG hook for CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG with appropriate weak self capture. All test-mode code is correctly guarded inside the existing #if DEBUG block.
cmuxUITests/SidebarHelpMenuUITests.swift Renames and updates two existing tests to expect the inline pill (not Sparkle dialog buttons); adds testDialogTriggeredCheckForUpdatesShowsSparkleDialogWithChangelog that uses CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG and verifies the changelog text appears in the Sparkle dialog. UpdateTestURLProtocol registration is correctly handled via feedURLString(for:) in UpdateDelegate without requiring CMUX_UI_TEST_MODE.
scripts/appcast_changelog.py New script extracts plain-text release notes from CHANGELOG.md for a given version tag. Keep-a-Changelog format parsing is correct; CDATA escaping and markdown stripping are sound. Minor: only - bullet markers are handled; * bullets would be treated as plain paragraphs.
scripts/sparkle_generate_appcast.sh Integrates changelog extraction and XML injection into the appcast pipeline. CDATA escaping of ]]> is correct. The re.subn replacement string concatenates changelog content directly, which risks mis-interpretation of backslash sequences as regex backreferences if changelog text ever contains them.

Sequence Diagram

sequenceDiagram
    participant User
    participant SidebarHelpMenu
    participant AppMenu as App Menu / MenuBarExtra
    participant BackgroundPill as UpdatePill (background update)
    participant AppDelegate
    participant UpdateController
    participant Sparkle

    Note over User,Sparkle: Inline pill flow (sidebar "Check for Updates")
    User->>SidebarHelpMenu: Click "Check for Updates"
    SidebarHelpMenu->>AppDelegate: checkForUpdates(nil)
    AppDelegate->>UpdateController: checkForUpdates()
    UpdateController->>Sparkle: requestCheckForUpdates(.custom)
    Sparkle-->>AppDelegate: didFindUpdate
    AppDelegate-->>User: Shows inline UpdatePill in sidebar

    Note over User,Sparkle: Sparkle dialog flow (app menu / menu bar extra)
    User->>AppMenu: Click "Check for Updates…"
    AppMenu->>AppDelegate: checkForUpdatesInDialog(nil)
    AppDelegate->>UpdateController: checkForUpdatesInDialog()
    UpdateController->>Sparkle: requestCheckForUpdates(.dialog)
    Sparkle-->>User: Shows Sparkle dialog (with embedded changelog)

    Note over User,Sparkle: Background-detected update pill click
    Sparkle->>AppDelegate: didFindUpdate (background probe)
    AppDelegate-->>User: Shows UpdatePill (showsDetectedBackgroundUpdate=true)
    User->>BackgroundPill: Click pill
    BackgroundPill->>AppDelegate: checkForUpdatesInDialog(nil)
    AppDelegate->>UpdateController: checkForUpdatesInDialog()
    UpdateController->>Sparkle: requestCheckForUpdates(.dialog)
    Sparkle-->>User: Shows Sparkle dialog (with embedded changelog)
Loading

Comments Outside Diff (2)

  1. scripts/sparkle_generate_appcast.sh, line 161 (link)

    P2 Changelog text directly in re.subn replacement string

    The description variable is concatenated directly into the regex replacement string:

    r"\1\n            " + description

    Python's re.subn interprets backslash sequences in the replacement string (e.g., \1\99 as group backreferences, \n as newline). If the changelog text (extracted via appcast_changelog.py) ever contains a backslash followed by a digit — e.g. a Windows path in a bug-fix note, a reference like \1, or escaped characters — Python will either silently produce wrong XML or raise re.error: invalid group reference.

    Use a lambda to prevent the replacement string from being processed for backreferences:

  2. scripts/appcast_changelog.py, line 70-73 (link)

    P2 *-prefixed bullet points not handled

    Only - bullets are recognized. Markdown also allows * as a bullet marker (and some CHANGELOG entries use it). A * item line would fall through to the paragraph handler, losing the bullet symbol entirely.

Reviews (1): Last reviewed commit: "Revert Sparkle manual update dialog flow" | Re-trigger Greptile

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
cmuxUITests/SidebarHelpMenuUITests.swift (2)

92-113: This should exercise the real menu route, not only the debug hook.

The env flag proves checkForUpdatesInDialog can render the Sparkle dialog, but it doesn't verify that the actual menu item's action still points there. If the menu wiring regresses back to the inline path, this test stays green.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/SidebarHelpMenuUITests.swift` around lines 92 - 113, The test
currently uses the debug env flag CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG which
only exercises the debug hook; update
testDialogTriggeredCheckForUpdatesShowsSparkleDialogWithChangelog to trigger the
real menu action instead: remove or stop relying on
CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG, keep the feed-related env vars
(CMUX_UI_TEST_FEED_URL, CMUX_UI_TEST_FEED_MODE, CMUX_UI_TEST_UPDATE_VERSION,
CMUX_UI_TEST_RELEASE_NOTES_SUMMARY), launch the app, then open the app menu and
tap the actual "Check for Updates…" menu item (use
XCUIApplication().menuBars.menuItems["Check for Updates…"] or the app's
accessibility identifier) before asserting the Sparkle dialog appears with
waitForWindowCount and checking installButton/"Remind Me Later"/"Skip This
Version"/"99.0.0"/summary existence.

59-90: Please assert the inline popover contents too.

Both sidebar-path tests stop after the pill appears. That still passes if the pill renders but the anchored popover never opens, or if it opens without the release-notes summary / install action that issue #2028 calls for. I'd extend these to click the pill and assert the inline popover content, not just the absence of Sparkle's modal buttons.

Also applies to: 115-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/SidebarHelpMenuUITests.swift` around lines 59 - 90, The test
stops after the update pill appears but must also verify the anchored inline
popover and its contents; update
testHelpMenuCheckForUpdatesShowsInlineUpdatePillOnFirstAttempt (and the other
sidebar-path test) to click the pill (app.buttons["Update Available: 99.0.0"]),
wait for the anchored popover to appear and be hittable, and then assert the
presence of the release-notes summary element and the inline Install action (and
optionally Remind Me Later / Skip This Version controls) inside that popover to
ensure the inline UI opened correctly rather than only the pill rendering.
scripts/appcast_changelog.py (1)

17-18: Please lock the v/bare-tag contract with a regression test.

sparkle_generate_appcast.sh passes $TAG through as-is, so these two functions are what keep v0.62.2 and 0.62.2 equivalent. A small CLI test for both spellings would make that release-path behavior much harder to regress.

Also applies to: 32-44

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/appcast_changelog.py` around lines 17 - 18, Add a regression test
that ensures tags with and without a leading "v" produce identical results:
write a unit test for normalize_tag(tag: str) that asserts
normalize_tag("v0.62.2") == normalize_tag("0.62.2"), and add a small
CLI/integration test that invokes the appcast generator script twice (once with
TAG="v0.62.2" and once with TAG="0.62.2") and asserts the generated
appcast/changelog outputs are byte-for-byte equal; place tests alongside
existing test suite and target the appcast generation entrypoint that consumes
TAG so the v/bare-tag contract (functions including normalize_tag and the
appcast generation entrypoint) cannot regress.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/sparkle_generate_appcast.sh`:
- Around line 95-97: The command substitution that populates
APPCAST_CHANGELOG_TEXT is swallowing real errors via "|| true"; remove the "||
true" so failures in scripts/appcast_changelog.py propagate and cause the script
to fail instead of silently publishing without notes. Update the block that runs
scripts/appcast_changelog.py (the line setting APPCAST_CHANGELOG_TEXT with TAG
and CHANGELOG_PATH) to omit "|| true", and optionally add an explicit
empty-string check for APPCAST_CHANGELOG_TEXT after the call if you need
different handling for legitimately-missing changelog entries versus execution
errors.

In `@Sources/AppDelegate.swift`:
- Around line 5872-5875: The ContentView and UpdateDriver callers still call
checkForUpdates(nil) and bypass the dialog helper; update those call sites to
use the dialog entry path: clear any override by setting
updateViewModel.overrideState = nil (or ensure the view model state is cleared)
and invoke AppDelegate's checkForUpdatesInDialog (or call
updateController.checkForUpdatesInDialog()) instead of checkForUpdates(nil) so
user-triggered and retry-from-error flows consistently present Sparkle's dialog;
locate and replace the calls to checkForUpdates(nil) in the methods referenced
(Sources/ContentView.swift and Sources/Update/UpdateDriver.swift) to call the
dialog helper or ensure overrideState is nil before invoking
checkForUpdatesInDialog.
- Around line 2356-2360: The test hook fires checkForUpdatesInDialog(nil) too
early; change the DispatchQueue.main.asyncAfter block to wait until the app has
a main/visible window before calling checkForUpdatesInDialog(nil). Concretely,
after detecting env["CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG"] == "1" (and
after UpdateLogStore.shared.append), replace the single delayed call with logic
that either observes NSWindow/NSApplication window notifications (e.g.,
NSWindow.didBecomeMainNotification or NSApplication.shared.addObserver for
window activation) or polls on the main queue for
NSApplication.shared.mainWindow/visible window, and only invoke
self?.checkForUpdatesInDialog(nil) once a main/visible window exists; keep using
the same DispatchQueue/main context and preserve the 0.25s fallback timing if
desired.

In `@Sources/Update/UpdatePill.swift`:
- Around line 30-33: The current branch handling
model.showsDetectedBackgroundUpdate calls
AppDelegate.shared?.checkForUpdatesInDialog(nil), forcing Sparkle's modal dialog
instead of the intended inline/sidebar pill flow; replace that call with the
inline update presentation path (e.g., call the app-level method that triggers
the sidebar/inline update UI such as a hypothetical
AppDelegate.shared?.checkForUpdatesInline(...) or presentUpdatePillInline(...)
), or if no such helper exists, add one that triggers the existing
inline/sidebar update UI and call it here instead of
checkForUpdatesInDialog(nil), keeping showPopover = false and returning as
before; update references to model.showsDetectedBackgroundUpdate, showPopover,
and the AppDelegate call accordingly.

In `@Sources/Update/UpdateTestURLProtocol.swift`:
- Around line 78-80: The releaseNotesSummary string may contain the CDATA
terminator "]]>", which will break XML when interpolated; before using
releaseNotesSummary in XML (and similarly for the values used at the locations
referenced around lines 93-94), replace every occurrence of "]]>" with
"]]]]><![CDATA[>" (or otherwise split the terminator) so the CDATA block stays
valid—update the code that prepares releaseNotesSummary (and the other
release-notes variable(s) used in the XML interpolation) to perform this
replacement prior to trimming/embedding.

---

Nitpick comments:
In `@cmuxUITests/SidebarHelpMenuUITests.swift`:
- Around line 92-113: The test currently uses the debug env flag
CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG which only exercises the debug hook;
update testDialogTriggeredCheckForUpdatesShowsSparkleDialogWithChangelog to
trigger the real menu action instead: remove or stop relying on
CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG, keep the feed-related env vars
(CMUX_UI_TEST_FEED_URL, CMUX_UI_TEST_FEED_MODE, CMUX_UI_TEST_UPDATE_VERSION,
CMUX_UI_TEST_RELEASE_NOTES_SUMMARY), launch the app, then open the app menu and
tap the actual "Check for Updates…" menu item (use
XCUIApplication().menuBars.menuItems["Check for Updates…"] or the app's
accessibility identifier) before asserting the Sparkle dialog appears with
waitForWindowCount and checking installButton/"Remind Me Later"/"Skip This
Version"/"99.0.0"/summary existence.
- Around line 59-90: The test stops after the update pill appears but must also
verify the anchored inline popover and its contents; update
testHelpMenuCheckForUpdatesShowsInlineUpdatePillOnFirstAttempt (and the other
sidebar-path test) to click the pill (app.buttons["Update Available: 99.0.0"]),
wait for the anchored popover to appear and be hittable, and then assert the
presence of the release-notes summary element and the inline Install action (and
optionally Remind Me Later / Skip This Version controls) inside that popover to
ensure the inline UI opened correctly rather than only the pill rendering.

In `@scripts/appcast_changelog.py`:
- Around line 17-18: Add a regression test that ensures tags with and without a
leading "v" produce identical results: write a unit test for normalize_tag(tag:
str) that asserts normalize_tag("v0.62.2") == normalize_tag("0.62.2"), and add a
small CLI/integration test that invokes the appcast generator script twice (once
with TAG="v0.62.2" and once with TAG="0.62.2") and asserts the generated
appcast/changelog outputs are byte-for-byte equal; place tests alongside
existing test suite and target the appcast generation entrypoint that consumes
TAG so the v/bare-tag contract (functions including normalize_tag and the
appcast generation entrypoint) cannot regress.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f642b2a5-213d-4abc-acb7-df311271e711

📥 Commits

Reviewing files that changed from the base of the PR and between 7ffa447 and b9481d4.

📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • Sources/AppDelegate.swift
  • Sources/Update/UpdateController.swift
  • Sources/Update/UpdatePill.swift
  • Sources/Update/UpdateTestURLProtocol.swift
  • Sources/cmuxApp.swift
  • cmuxUITests/SidebarHelpMenuUITests.swift
  • scripts/appcast_changelog.py
  • scripts/sparkle_generate_appcast.sh
  • tests/test_appcast_changelog_extraction.sh

Comment thread scripts/sparkle_generate_appcast.sh Outdated
Comment on lines +95 to +97
if [[ -z "$APPCAST_CHANGELOG_TEXT" && -f "$CHANGELOG_PATH" ]]; then
APPCAST_CHANGELOG_TEXT="$(python3 scripts/appcast_changelog.py --tag "$TAG" --changelog "$CHANGELOG_PATH" || true)"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't swallow extractor failures with || true.

scripts/appcast_changelog.py already exits 0 when the changelog is missing or the tag has no section, so || true only hides real execution/read failures. In that case this script silently publishes an appcast without embedded notes and later reports it as "no matching changelog entry".

Suggested fix
-if [[ -z "$APPCAST_CHANGELOG_TEXT" && -f "$CHANGELOG_PATH" ]]; then
-  APPCAST_CHANGELOG_TEXT="$(python3 scripts/appcast_changelog.py --tag "$TAG" --changelog "$CHANGELOG_PATH" || true)"
-fi
+if [[ -z "$APPCAST_CHANGELOG_TEXT" && -f "$CHANGELOG_PATH" ]]; then
+  if ! APPCAST_CHANGELOG_TEXT="$(python3 scripts/appcast_changelog.py --tag "$TAG" --changelog "$CHANGELOG_PATH")"; then
+    echo "Failed to extract changelog entry for $TAG from $CHANGELOG_PATH" >&2
+    exit 1
+  fi
+fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [[ -z "$APPCAST_CHANGELOG_TEXT" && -f "$CHANGELOG_PATH" ]]; then
APPCAST_CHANGELOG_TEXT="$(python3 scripts/appcast_changelog.py --tag "$TAG" --changelog "$CHANGELOG_PATH" || true)"
fi
if [[ -z "$APPCAST_CHANGELOG_TEXT" && -f "$CHANGELOG_PATH" ]]; then
if ! APPCAST_CHANGELOG_TEXT="$(python3 scripts/appcast_changelog.py --tag "$TAG" --changelog "$CHANGELOG_PATH")"; then
echo "Failed to extract changelog entry for $TAG from $CHANGELOG_PATH" >&2
exit 1
fi
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sparkle_generate_appcast.sh` around lines 95 - 97, The command
substitution that populates APPCAST_CHANGELOG_TEXT is swallowing real errors via
"|| true"; remove the "|| true" so failures in scripts/appcast_changelog.py
propagate and cause the script to fail instead of silently publishing without
notes. Update the block that runs scripts/appcast_changelog.py (the line setting
APPCAST_CHANGELOG_TEXT with TAG and CHANGELOG_PATH) to omit "|| true", and
optionally add an explicit empty-string check for APPCAST_CHANGELOG_TEXT after
the call if you need different handling for legitimately-missing changelog
entries versus execution errors.

Comment thread Sources/AppDelegate.swift Outdated
Comment on lines +2356 to +2360
if env["CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG"] == "1" {
UpdateLogStore.shared.append("ui test trigger update dialog detected")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.checkForUpdatesInDialog(nil)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Wait for a main window before firing the dialog test hook.

Line 2358 queues the dialog at the same 0.25s deadline as the later XCTest fallback that force-creates and activates a window, but this block is enqueued first. On the slow-WindowGroup path called out below, checkForUpdatesInDialog(nil) can run before any window exists, which makes the new Sparkle-dialog UI test hook flaky.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 2356 - 2360, The test hook fires
checkForUpdatesInDialog(nil) too early; change the DispatchQueue.main.asyncAfter
block to wait until the app has a main/visible window before calling
checkForUpdatesInDialog(nil). Concretely, after detecting
env["CMUX_UI_TEST_TRIGGER_UPDATE_CHECK_DIALOG"] == "1" (and after
UpdateLogStore.shared.append), replace the single delayed call with logic that
either observes NSWindow/NSApplication window notifications (e.g.,
NSWindow.didBecomeMainNotification or NSApplication.shared.addObserver for
window activation) or polls on the main queue for
NSApplication.shared.mainWindow/visible window, and only invoke
self?.checkForUpdatesInDialog(nil) once a main/visible window exists; keep using
the same DispatchQueue/main context and preserve the 0.25s fallback timing if
desired.

Comment thread Sources/AppDelegate.swift Outdated
Comment on lines +5872 to +5875
@objc func checkForUpdatesInDialog(_ sender: Any?) {
updateViewModel.overrideState = nil
updateController.checkForUpdatesInDialog()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Other manual-check callers still bypass this dialog helper.

The split is only partial right now: the provided snippets in Sources/ContentView.swift:10445-10453 and Sources/Update/UpdateDriver.swift:130-145 still call checkForUpdates(nil). Those user-triggered and retry-from-error paths will keep showing the inline pill instead of Sparkle's dialog, so the entry-point behavior from issue #2028 is still inconsistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 5872 - 5875, The ContentView and
UpdateDriver callers still call checkForUpdates(nil) and bypass the dialog
helper; update those call sites to use the dialog entry path: clear any override
by setting updateViewModel.overrideState = nil (or ensure the view model state
is cleared) and invoke AppDelegate's checkForUpdatesInDialog (or call
updateController.checkForUpdatesInDialog()) instead of checkForUpdates(nil) so
user-triggered and retry-from-error flows consistently present Sparkle's dialog;
locate and replace the calls to checkForUpdates(nil) in the methods referenced
(Sources/ContentView.swift and Sources/Update/UpdateDriver.swift) to call the
dialog helper or ensure overrideState is nil before invoking
checkForUpdatesInDialog.

Comment thread Sources/Update/UpdatePill.swift
Comment on lines +78 to +80
let releaseNotesSummary = (env["CMUX_UI_TEST_RELEASE_NOTES_SUMMARY"] ??
"Important fixes, improved reliability, and small workflow polish.")
.trimmingCharacters(in: .whitespacesAndNewlines)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Sanitize CDATA terminators in release-notes text before XML interpolation.

If the summary contains ]]>, Line 93 produces malformed XML and the appcast becomes unparsable.

🔧 Proposed fix
-        let releaseNotesSummary = (env["CMUX_UI_TEST_RELEASE_NOTES_SUMMARY"] ??
+        let releaseNotesSummary = (env["CMUX_UI_TEST_RELEASE_NOTES_SUMMARY"] ??
             "Important fixes, improved reliability, and small workflow polish.")
             .trimmingCharacters(in: .whitespacesAndNewlines)
+        let safeReleaseNotesSummary = releaseNotesSummary
+            .replacingOccurrences(of: "]]>", with: "]]]]><![CDATA[>")

@@
-              <description sparkle:format="plain-text"><![CDATA[\(releaseNotesSummary)]]></description>
+              <description sparkle:format="plain-text"><![CDATA[\(safeReleaseNotesSummary)]]></description>

Also applies to: 93-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Update/UpdateTestURLProtocol.swift` around lines 78 - 80, The
releaseNotesSummary string may contain the CDATA terminator "]]>", which will
break XML when interpolated; before using releaseNotesSummary in XML (and
similarly for the values used at the locations referenced around lines 93-94),
replace every occurrence of "]]>" with "]]]]><![CDATA[>" (or otherwise split the
terminator) so the CDATA block stays valid—update the code that prepares
releaseNotesSummary (and the other release-notes variable(s) used in the XML
interpolation) to perform this replacement prior to trimming/embedding.

@austinywang austinywang merged commit 5660206 into main Mar 25, 2026
15 of 16 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…low-ai#2090)

* Restore inline sidebar update checks and embed appcast changelog

* Revert Sparkle manual update dialog flow
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.

1 participant