fix(ci): fix appcast not updating on stable release#233
Merged
Conversation
Three bugs prevented v0.9.0 from appearing in appcast.xml: 1. Race condition: the `release: published` and explicit `workflow_dispatch` triggers both fired within 2 seconds of the release being published, before the GitHub API had indexed the new release. The appcast workflow got the pre-release cached list of exactly 30 items and silently skipped v0.9.0 (no zip asset visible yet → jq returned empty → no warning logged → diff showed no changes → no PR created). 2. Pagination: generate-appcast.sh fetched a single page with GitHub's default of 30 releases. With 50+ releases in the repo, any stable release beyond page 1 would be invisible to the generator. Fixes: - Replace `release: published` + explicit `workflow_dispatch` trigger with `workflow_run` on Release workflow completion. The Release workflow takes 30-60 minutes to build/sign/notarize; by the time it completes the API has long indexed the new release. - Add job-level guard to skip appcast update when the Release workflow failed. - Implement page-looping in fetch_releases() (per_page=100&page=N until empty) so all releases are fetched regardless of count. - Add GH_TOKEN auth header to bypass the API response cache. - Fix stale github.event.release.tag_name reference (removed with the release: published trigger) → github.event.workflow_run.head_branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was referenced Apr 28, 2026
Merged
eyelock
added a commit
that referenced
this pull request
May 11, 2026
* chore: Update appcast for release manual Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(ci): fix appcast not updating on stable release (#233) Three bugs prevented v0.9.0 from appearing in appcast.xml: 1. Race condition: the `release: published` and explicit `workflow_dispatch` triggers both fired within 2 seconds of the release being published, before the GitHub API had indexed the new release. The appcast workflow got the pre-release cached list of exactly 30 items and silently skipped v0.9.0 (no zip asset visible yet → jq returned empty → no warning logged → diff showed no changes → no PR created). 2. Pagination: generate-appcast.sh fetched a single page with GitHub's default of 30 releases. With 50+ releases in the repo, any stable release beyond page 1 would be invisible to the generator. Fixes: - Replace `release: published` + explicit `workflow_dispatch` trigger with `workflow_run` on Release workflow completion. The Release workflow takes 30-60 minutes to build/sign/notarize; by the time it completes the API has long indexed the new release. - Add job-level guard to skip appcast update when the Release workflow failed. - Implement page-looping in fetch_releases() (per_page=100&page=N until empty) so all releases are fetched regardless of count. - Add GH_TOKEN auth header to bypass the API response cache. - Fix stale github.event.release.tag_name reference (removed with the release: published trigger) → github.event.workflow_run.head_branch. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(harnesses): fix uninstall for local harnesses with no YNH install record (#234) Harnesses that exist on disk but were never registered via `ynh install` caused `ynh uninstall` to fail with "harness not installed". The fix: - `uninstallHarness(name:)` now detects `installedFrom == nil` and deletes the harness directory via FileManager directly, bypassing the ynh terminal entirely, then cleans associations and refreshes the list - Removed a redundant `FileManager.removeItem` from `performDeleteLocalHarness` in the sidebar (now owned by `uninstallHarness`) - Uninstall confirmation dialogs now show context-aware messages for all three harness provenance states (untracked / ynh-local / registry+git), with the selection logic extracted into a single `Strings.Harnesses.uninstallBaseMessage(for:)` function used by both HarnessDetailView and HarnessesSidebarTab Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(skills): update hotfix procedure — PR before CI, CHANGELOG, appcast forward-port - Open PR to main before waiting for CI (CI runs on the PR) - Always update CHANGELOG.md on the hotfix branch before tagging - Forward-port PR must include CHANGELOG and appcast changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(ui): re-register URL Apple Event handler after SwiftUI scene setup (#239) SwiftUI registers its own kAEGetURL handler during scene initialisation, which runs after App.init — overriding the registration we placed there. This caused SwiftUI's AppWindowsController.activateWindowForExternalEvent to close the main window on every URL open. Moving the NSAppleEventManager registration to applicationDidFinishLaunching ensures TermQ's handler is set last and wins, so SwiftUI never sees the URL Apple Event and cannot hide the window. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(terminal): replace -50 Finder dialog with correct file/URL handling (#240) Cmd+clicking a path in the terminal produced the macOS "-50" dialog because SwiftTerm's default requestOpenLink implementation calls URL(string:) on bare paths, which produces a schemeless URL that LaunchServices rejects. Root cause: LocalProcessTerminalView satisfies requestOpenLink via a protocol extension default baked into the SwiftTerm binary. Subclass overrides in our module land in a separate vtable slot that the inherited witness table never consults. Per SwiftTerm's own docs, the fix is to replace terminalDelegate with a proxy and forward all values. - Add TermQLinkDelegate: full-proxy TerminalViewDelegate installed in TermQTerminalView.init; intercepts requestOpenLink, forwards everything else to LocalProcessTerminalView's concrete implementations - Add TerminalLinkResolver: pure resolution of a link string into .openURL / .openFile / .revealInFinder / .fallbackString / .noop - Add TermQTerminalLink.open: single entry point for all link clicks; pre-flight checks for registered handler, surfaces friendly alert instead of -50 dialog, opens directories directly in Finder - Add TerminalLinkRoutingTests: static guardrail that scans all requestOpenLink definitions and asserts each routes through TermQTerminalLink.open - Add TerminalLinkResolverTests: 20 unit tests for sanitize + resolve - Wire ControlModePaneDelegate.requestOpenLink through the same entry point - Localize two new alert strings (no-handler, launch-failed) into 40 languages Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for v0.9.2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(concurrency): add @sendable to system-dispatched closures to prevent MainActor isolation crash TerminalLinkResolver and TmuxControlModeSession both had closures passed to system APIs (LaunchServices completion handler and FileHandle.readabilityHandler) that were inheriting @mainactor isolation from their enclosing context. When called on a background queue by the system this triggers EXC_BREAKPOINT via _swift_task_checkIsolatedSwift. Mark both closures @sendable to opt them out of actor isolation inheritance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for v0.9.3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.3 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(harness): tolerate name/id mismatch when resolving selected harness (#252) `Harness.id` is `"namespace/name"` for namespaced installs but `YNHPersistence` keys associations by bare `name`. The Launch <harness> flow on worktree rows passes the persisted name through and sets `selectedHarnessName = name`, but `HarnessRepository.selectedHarness` matched on `id` only — for any namespaced install the lookup missed, the launch sheet's content closure returned nothing, and the sheet rendered as a blank rounded rectangle that never populated and could only be dismissed with Esc. Make `selectedHarness` match by `id` then fall back to `name`. Apply the same rule to the stale-selection eviction inside `refresh()` so the next list refresh doesn't immediately clear a name-keyed selection. Affects v0.9.3 — also forward-ported via hotfix release. Co-authored-by: David Collie <support@eyelock.net> * chore: Update appcast for release v0.9.4 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(harness): adapt to ynh 0.3 JSON envelope and version_installed rename Three breaking changes in ynh 0.3.0's structured-output format combined to leave TermQ unable to load any harness data against the new YNH: 1. `ynh ls --format json` now returns an envelope object `{capabilities, harnesses, ynh_version}` instead of a bare array. 2. `ynh info <name> --format json` likewise wraps in `{capabilities, harness, ynh_version}`. 3. The harness `version` field was renamed to `version_installed` in both `ynh ls` and `ynh info` payloads. Update HarnessRepository to decode through YNHListEnvelope / YNHInfoEnvelope wrappers, and remap the `version` CodingKey on Harness and HarnessInfo to `version_installed`. ynd compose still emits `version` so HarnessComposition is unchanged. User-visible symptom on v0.9.4: the Harnesses sidebar tab was empty, harness detail showed nothing, and Launch from a worktree row presented a blank rounded sheet (or did nothing at all). The v0.9.4 identifier-fallback fix at HarnessRepository:40 was a downstream patch on the same bug class but couldn't help while the list itself was empty. YNH-side schema changes are documented separately in a YNH bug report (envelope shape, version rename, Harness.namespace not populated for registry installs). All three are 0.x churn and explicitly not backward-compatible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.5 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * hotfix(v0.9.6): backport focus, marketplace persistence, and OSC 52 default fixes (#272) * fix(window): stop stealing focus on AppleEvent reopen (#268) applicationShouldHandleReopen fired on every MCP-driven termq:// URL delivery (NSWorkspace.open with activates:false does not suppress the underlying AE Reopen). The handler unconditionally called makeKeyAndOrderFront, so each background MCP op stole focus from whatever app the user was working in. Gate the activation: unhide on Cmd+H, deminiaturize on Cmd+M, bring the window forward only when no windows are visible. When the window is already visible and the app is not hidden, no-op — AppKit handles genuine Dock-click activation independently of this delegate method. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(marketplace): persist removals and harden against silent save failures (#264) Removing a marketplace from Settings → External Sources didn't survive relaunch. Three concurrent issues: - Confirmation dialog read marketplaceToRemove after dismissal (racy); switched to the presenting: form so the action captures by value, matching the pattern already used in HarnessDetailDependencyView. - save() swallowed all errors with try?; now logs via TermQLogger.io and exposes lastPersistenceError on the store. - A re-seed (after a defaults reset or version bump) could re-add a default the user had explicitly removed. Added tombstone tracking (marketplaces.removedDefaultURLs.v1) honoured on seed; the explicit Restore Defaults button bypasses tombstones via force: true. MarketplaceStore.init now accepts optional fileURL and UserDefaults for isolated tests; new MarketplaceStoreTests.swift covers seeding, removal persistence, tombstone behaviour, and dedup. Fixes #260 Co-authored-by: David Collie <support@eyelock.net> * fix(security): align OSC 52 clipboard runtime default with Settings UI The runtime gate in TerminalHostView defaulted to true on unset while SettingsView displayed false — so a never-touched user saw "Off" in Settings → Data & Security but terminal programs could silently copy to the clipboard. Drop the explicit-true sentinel; UserDefaults.bool returns false when the key is absent, matching the Settings UI and every other read site for this key. Surgical version of #270 for the v0.9.6 hotfix line; the develop fix routes the same gate through SettingsStore, which doesn't exist on this branch. Also reflow MarketplaceSidebarTab.swift:168 to satisfy swift-format (carried over from the #264 cherry-pick), and add a 0.9.6 CHANGELOG section covering the three backports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.6 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * hotfix(v0.9.7): tolerate YNH 0.2.x list/info shapes (#296) v0.9.5/0.9.6 hard-coded the YNH 0.3 structured-output shape, but YNH 0.3 was never published to the Homebrew tap. Every user on `brew install ynh` is on 0.2.3, so harness loading failed entirely: empty Harnesses sidebar, blank Launch card on worktree rows that already had a harness associated. Decoding is now tolerant of both shapes: - `YNHListEnvelope` / `YNHInfoEnvelope` accept either the 0.3 envelope (`{harnesses: [...]}` / `{harness: {...}}`) or a bare `[Harness]` / `HarnessInfo` payload. - `Harness` / `HarnessInfo` accept either `version_installed` (0.3) or `version` (0.2.x). Tests cover both shapes for both call sites. The compat layer is intentional and sticks around past 0.10. Removal plan: once YNH 0.3 ships to the tap, gate behavior on `YNHDetector.capabilityMeets("0.3.0")` for one release, then delete the legacy branches. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.7 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: Update appcast for release v0.10.0-beta.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: Update appcast for release v0.10.0-beta.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: update CHANGELOG for v0.10.0 * fix(merge): remove duplicate init/encode from HarnessInfo after merge conflict resolution * chore: sync appcast entries for v0.10.0 back to develop --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
eyelock
added a commit
that referenced
this pull request
May 12, 2026
* chore: Update appcast for release manual Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(ci): fix appcast not updating on stable release (#233) Three bugs prevented v0.9.0 from appearing in appcast.xml: 1. Race condition: the `release: published` and explicit `workflow_dispatch` triggers both fired within 2 seconds of the release being published, before the GitHub API had indexed the new release. The appcast workflow got the pre-release cached list of exactly 30 items and silently skipped v0.9.0 (no zip asset visible yet → jq returned empty → no warning logged → diff showed no changes → no PR created). 2. Pagination: generate-appcast.sh fetched a single page with GitHub's default of 30 releases. With 50+ releases in the repo, any stable release beyond page 1 would be invisible to the generator. Fixes: - Replace `release: published` + explicit `workflow_dispatch` trigger with `workflow_run` on Release workflow completion. The Release workflow takes 30-60 minutes to build/sign/notarize; by the time it completes the API has long indexed the new release. - Add job-level guard to skip appcast update when the Release workflow failed. - Implement page-looping in fetch_releases() (per_page=100&page=N until empty) so all releases are fetched regardless of count. - Add GH_TOKEN auth header to bypass the API response cache. - Fix stale github.event.release.tag_name reference (removed with the release: published trigger) → github.event.workflow_run.head_branch. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(harnesses): fix uninstall for local harnesses with no YNH install record (#234) Harnesses that exist on disk but were never registered via `ynh install` caused `ynh uninstall` to fail with "harness not installed". The fix: - `uninstallHarness(name:)` now detects `installedFrom == nil` and deletes the harness directory via FileManager directly, bypassing the ynh terminal entirely, then cleans associations and refreshes the list - Removed a redundant `FileManager.removeItem` from `performDeleteLocalHarness` in the sidebar (now owned by `uninstallHarness`) - Uninstall confirmation dialogs now show context-aware messages for all three harness provenance states (untracked / ynh-local / registry+git), with the selection logic extracted into a single `Strings.Harnesses.uninstallBaseMessage(for:)` function used by both HarnessDetailView and HarnessesSidebarTab Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(skills): update hotfix procedure — PR before CI, CHANGELOG, appcast forward-port - Open PR to main before waiting for CI (CI runs on the PR) - Always update CHANGELOG.md on the hotfix branch before tagging - Forward-port PR must include CHANGELOG and appcast changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(ui): re-register URL Apple Event handler after SwiftUI scene setup (#239) SwiftUI registers its own kAEGetURL handler during scene initialisation, which runs after App.init — overriding the registration we placed there. This caused SwiftUI's AppWindowsController.activateWindowForExternalEvent to close the main window on every URL open. Moving the NSAppleEventManager registration to applicationDidFinishLaunching ensures TermQ's handler is set last and wins, so SwiftUI never sees the URL Apple Event and cannot hide the window. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(terminal): replace -50 Finder dialog with correct file/URL handling (#240) Cmd+clicking a path in the terminal produced the macOS "-50" dialog because SwiftTerm's default requestOpenLink implementation calls URL(string:) on bare paths, which produces a schemeless URL that LaunchServices rejects. Root cause: LocalProcessTerminalView satisfies requestOpenLink via a protocol extension default baked into the SwiftTerm binary. Subclass overrides in our module land in a separate vtable slot that the inherited witness table never consults. Per SwiftTerm's own docs, the fix is to replace terminalDelegate with a proxy and forward all values. - Add TermQLinkDelegate: full-proxy TerminalViewDelegate installed in TermQTerminalView.init; intercepts requestOpenLink, forwards everything else to LocalProcessTerminalView's concrete implementations - Add TerminalLinkResolver: pure resolution of a link string into .openURL / .openFile / .revealInFinder / .fallbackString / .noop - Add TermQTerminalLink.open: single entry point for all link clicks; pre-flight checks for registered handler, surfaces friendly alert instead of -50 dialog, opens directories directly in Finder - Add TerminalLinkRoutingTests: static guardrail that scans all requestOpenLink definitions and asserts each routes through TermQTerminalLink.open - Add TerminalLinkResolverTests: 20 unit tests for sanitize + resolve - Wire ControlModePaneDelegate.requestOpenLink through the same entry point - Localize two new alert strings (no-handler, launch-failed) into 40 languages Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for v0.9.2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(concurrency): add @sendable to system-dispatched closures to prevent MainActor isolation crash TerminalLinkResolver and TmuxControlModeSession both had closures passed to system APIs (LaunchServices completion handler and FileHandle.readabilityHandler) that were inheriting @mainactor isolation from their enclosing context. When called on a background queue by the system this triggers EXC_BREAKPOINT via _swift_task_checkIsolatedSwift. Mark both closures @sendable to opt them out of actor isolation inheritance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for v0.9.3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.3 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(harness): tolerate name/id mismatch when resolving selected harness (#252) `Harness.id` is `"namespace/name"` for namespaced installs but `YNHPersistence` keys associations by bare `name`. The Launch <harness> flow on worktree rows passes the persisted name through and sets `selectedHarnessName = name`, but `HarnessRepository.selectedHarness` matched on `id` only — for any namespaced install the lookup missed, the launch sheet's content closure returned nothing, and the sheet rendered as a blank rounded rectangle that never populated and could only be dismissed with Esc. Make `selectedHarness` match by `id` then fall back to `name`. Apply the same rule to the stale-selection eviction inside `refresh()` so the next list refresh doesn't immediately clear a name-keyed selection. Affects v0.9.3 — also forward-ported via hotfix release. Co-authored-by: David Collie <support@eyelock.net> * chore: Update appcast for release v0.9.4 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(harness): adapt to ynh 0.3 JSON envelope and version_installed rename Three breaking changes in ynh 0.3.0's structured-output format combined to leave TermQ unable to load any harness data against the new YNH: 1. `ynh ls --format json` now returns an envelope object `{capabilities, harnesses, ynh_version}` instead of a bare array. 2. `ynh info <name> --format json` likewise wraps in `{capabilities, harness, ynh_version}`. 3. The harness `version` field was renamed to `version_installed` in both `ynh ls` and `ynh info` payloads. Update HarnessRepository to decode through YNHListEnvelope / YNHInfoEnvelope wrappers, and remap the `version` CodingKey on Harness and HarnessInfo to `version_installed`. ynd compose still emits `version` so HarnessComposition is unchanged. User-visible symptom on v0.9.4: the Harnesses sidebar tab was empty, harness detail showed nothing, and Launch from a worktree row presented a blank rounded sheet (or did nothing at all). The v0.9.4 identifier-fallback fix at HarnessRepository:40 was a downstream patch on the same bug class but couldn't help while the list itself was empty. YNH-side schema changes are documented separately in a YNH bug report (envelope shape, version rename, Harness.namespace not populated for registry installs). All three are 0.x churn and explicitly not backward-compatible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.5 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * hotfix(v0.9.6): backport focus, marketplace persistence, and OSC 52 default fixes (#272) * fix(window): stop stealing focus on AppleEvent reopen (#268) applicationShouldHandleReopen fired on every MCP-driven termq:// URL delivery (NSWorkspace.open with activates:false does not suppress the underlying AE Reopen). The handler unconditionally called makeKeyAndOrderFront, so each background MCP op stole focus from whatever app the user was working in. Gate the activation: unhide on Cmd+H, deminiaturize on Cmd+M, bring the window forward only when no windows are visible. When the window is already visible and the app is not hidden, no-op — AppKit handles genuine Dock-click activation independently of this delegate method. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(marketplace): persist removals and harden against silent save failures (#264) Removing a marketplace from Settings → External Sources didn't survive relaunch. Three concurrent issues: - Confirmation dialog read marketplaceToRemove after dismissal (racy); switched to the presenting: form so the action captures by value, matching the pattern already used in HarnessDetailDependencyView. - save() swallowed all errors with try?; now logs via TermQLogger.io and exposes lastPersistenceError on the store. - A re-seed (after a defaults reset or version bump) could re-add a default the user had explicitly removed. Added tombstone tracking (marketplaces.removedDefaultURLs.v1) honoured on seed; the explicit Restore Defaults button bypasses tombstones via force: true. MarketplaceStore.init now accepts optional fileURL and UserDefaults for isolated tests; new MarketplaceStoreTests.swift covers seeding, removal persistence, tombstone behaviour, and dedup. Fixes #260 Co-authored-by: David Collie <support@eyelock.net> * fix(security): align OSC 52 clipboard runtime default with Settings UI The runtime gate in TerminalHostView defaulted to true on unset while SettingsView displayed false — so a never-touched user saw "Off" in Settings → Data & Security but terminal programs could silently copy to the clipboard. Drop the explicit-true sentinel; UserDefaults.bool returns false when the key is absent, matching the Settings UI and every other read site for this key. Surgical version of #270 for the v0.9.6 hotfix line; the develop fix routes the same gate through SettingsStore, which doesn't exist on this branch. Also reflow MarketplaceSidebarTab.swift:168 to satisfy swift-format (carried over from the #264 cherry-pick), and add a 0.9.6 CHANGELOG section covering the three backports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.6 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * hotfix(v0.9.7): tolerate YNH 0.2.x list/info shapes (#296) v0.9.5/0.9.6 hard-coded the YNH 0.3 structured-output shape, but YNH 0.3 was never published to the Homebrew tap. Every user on `brew install ynh` is on 0.2.3, so harness loading failed entirely: empty Harnesses sidebar, blank Launch card on worktree rows that already had a harness associated. Decoding is now tolerant of both shapes: - `YNHListEnvelope` / `YNHInfoEnvelope` accept either the 0.3 envelope (`{harnesses: [...]}` / `{harness: {...}}`) or a bare `[Harness]` / `HarnessInfo` payload. - `Harness` / `HarnessInfo` accept either `version_installed` (0.3) or `version` (0.2.x). Tests cover both shapes for both call sites. The compat layer is intentional and sticks around past 0.10. Removal plan: once YNH 0.3 ships to the tap, gate behavior on `YNHDetector.capabilityMeets("0.3.0")` for one release, then delete the legacy branches. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.7 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: Update appcast for release v0.10.0-beta.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: Update appcast for release v0.10.0-beta.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: update CHANGELOG for v0.10.0 * fix(merge): remove duplicate init/encode from HarnessInfo after merge conflict resolution * chore: Update appcast for release v0.10.0 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: update CHANGELOG for v0.10.1 --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
eyelock
added a commit
that referenced
this pull request
May 13, 2026
* chore: Update appcast for release manual Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(ci): fix appcast not updating on stable release (#233) Three bugs prevented v0.9.0 from appearing in appcast.xml: 1. Race condition: the `release: published` and explicit `workflow_dispatch` triggers both fired within 2 seconds of the release being published, before the GitHub API had indexed the new release. The appcast workflow got the pre-release cached list of exactly 30 items and silently skipped v0.9.0 (no zip asset visible yet → jq returned empty → no warning logged → diff showed no changes → no PR created). 2. Pagination: generate-appcast.sh fetched a single page with GitHub's default of 30 releases. With 50+ releases in the repo, any stable release beyond page 1 would be invisible to the generator. Fixes: - Replace `release: published` + explicit `workflow_dispatch` trigger with `workflow_run` on Release workflow completion. The Release workflow takes 30-60 minutes to build/sign/notarize; by the time it completes the API has long indexed the new release. - Add job-level guard to skip appcast update when the Release workflow failed. - Implement page-looping in fetch_releases() (per_page=100&page=N until empty) so all releases are fetched regardless of count. - Add GH_TOKEN auth header to bypass the API response cache. - Fix stale github.event.release.tag_name reference (removed with the release: published trigger) → github.event.workflow_run.head_branch. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(harnesses): fix uninstall for local harnesses with no YNH install record (#234) Harnesses that exist on disk but were never registered via `ynh install` caused `ynh uninstall` to fail with "harness not installed". The fix: - `uninstallHarness(name:)` now detects `installedFrom == nil` and deletes the harness directory via FileManager directly, bypassing the ynh terminal entirely, then cleans associations and refreshes the list - Removed a redundant `FileManager.removeItem` from `performDeleteLocalHarness` in the sidebar (now owned by `uninstallHarness`) - Uninstall confirmation dialogs now show context-aware messages for all three harness provenance states (untracked / ynh-local / registry+git), with the selection logic extracted into a single `Strings.Harnesses.uninstallBaseMessage(for:)` function used by both HarnessDetailView and HarnessesSidebarTab Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(skills): update hotfix procedure — PR before CI, CHANGELOG, appcast forward-port - Open PR to main before waiting for CI (CI runs on the PR) - Always update CHANGELOG.md on the hotfix branch before tagging - Forward-port PR must include CHANGELOG and appcast changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(ui): re-register URL Apple Event handler after SwiftUI scene setup (#239) SwiftUI registers its own kAEGetURL handler during scene initialisation, which runs after App.init — overriding the registration we placed there. This caused SwiftUI's AppWindowsController.activateWindowForExternalEvent to close the main window on every URL open. Moving the NSAppleEventManager registration to applicationDidFinishLaunching ensures TermQ's handler is set last and wins, so SwiftUI never sees the URL Apple Event and cannot hide the window. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(terminal): replace -50 Finder dialog with correct file/URL handling (#240) Cmd+clicking a path in the terminal produced the macOS "-50" dialog because SwiftTerm's default requestOpenLink implementation calls URL(string:) on bare paths, which produces a schemeless URL that LaunchServices rejects. Root cause: LocalProcessTerminalView satisfies requestOpenLink via a protocol extension default baked into the SwiftTerm binary. Subclass overrides in our module land in a separate vtable slot that the inherited witness table never consults. Per SwiftTerm's own docs, the fix is to replace terminalDelegate with a proxy and forward all values. - Add TermQLinkDelegate: full-proxy TerminalViewDelegate installed in TermQTerminalView.init; intercepts requestOpenLink, forwards everything else to LocalProcessTerminalView's concrete implementations - Add TerminalLinkResolver: pure resolution of a link string into .openURL / .openFile / .revealInFinder / .fallbackString / .noop - Add TermQTerminalLink.open: single entry point for all link clicks; pre-flight checks for registered handler, surfaces friendly alert instead of -50 dialog, opens directories directly in Finder - Add TerminalLinkRoutingTests: static guardrail that scans all requestOpenLink definitions and asserts each routes through TermQTerminalLink.open - Add TerminalLinkResolverTests: 20 unit tests for sanitize + resolve - Wire ControlModePaneDelegate.requestOpenLink through the same entry point - Localize two new alert strings (no-handler, launch-failed) into 40 languages Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for v0.9.2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(concurrency): add @sendable to system-dispatched closures to prevent MainActor isolation crash TerminalLinkResolver and TmuxControlModeSession both had closures passed to system APIs (LaunchServices completion handler and FileHandle.readabilityHandler) that were inheriting @mainactor isolation from their enclosing context. When called on a background queue by the system this triggers EXC_BREAKPOINT via _swift_task_checkIsolatedSwift. Mark both closures @sendable to opt them out of actor isolation inheritance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for v0.9.3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: Update appcast for release v0.9.3 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(harness): tolerate name/id mismatch when resolving selected harness (#252) `Harness.id` is `"namespace/name"` for namespaced installs but `YNHPersistence` keys associations by bare `name`. The Launch <harness> flow on worktree rows passes the persisted name through and sets `selectedHarnessName = name`, but `HarnessRepository.selectedHarness` matched on `id` only — for any namespaced install the lookup missed, the launch sheet's content closure returned nothing, and the sheet rendered as a blank rounded rectangle that never populated and could only be dismissed with Esc. Make `selectedHarness` match by `id` then fall back to `name`. Apply the same rule to the stale-selection eviction inside `refresh()` so the next list refresh doesn't immediately clear a name-keyed selection. Affects v0.9.3 — also forward-ported via hotfix release. Co-authored-by: David Collie <support@eyelock.net> * chore: Update appcast for release v0.9.4 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix(harness): adapt to ynh 0.3 JSON envelope and version_installed rename Three breaking changes in ynh 0.3.0's structured-output format combined to leave TermQ unable to load any harness data against the new YNH: 1. `ynh ls --format json` now returns an envelope object `{capabilities, harnesses, ynh_version}` instead of a bare array. 2. `ynh info <name> --format json` likewise wraps in `{capabilities, harness, ynh_version}`. 3. The harness `version` field was renamed to `version_installed` in both `ynh ls` and `ynh info` payloads. Update HarnessRepository to decode through YNHListEnvelope / YNHInfoEnvelope wrappers, and remap the `version` CodingKey on Harness and HarnessInfo to `version_installed`. ynd compose still emits `version` so HarnessComposition is unchanged. User-visible symptom on v0.9.4: the Harnesses sidebar tab was empty, harness detail showed nothing, and Launch from a worktree row presented a blank rounded sheet (or did nothing at all). The v0.9.4 identifier-fallback fix at HarnessRepository:40 was a downstream patch on the same bug class but couldn't help while the list itself was empty. YNH-side schema changes are documented separately in a YNH bug report (envelope shape, version rename, Harness.namespace not populated for registry installs). All three are 0.x churn and explicitly not backward-compatible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.5 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * hotfix(v0.9.6): backport focus, marketplace persistence, and OSC 52 default fixes (#272) * fix(window): stop stealing focus on AppleEvent reopen (#268) applicationShouldHandleReopen fired on every MCP-driven termq:// URL delivery (NSWorkspace.open with activates:false does not suppress the underlying AE Reopen). The handler unconditionally called makeKeyAndOrderFront, so each background MCP op stole focus from whatever app the user was working in. Gate the activation: unhide on Cmd+H, deminiaturize on Cmd+M, bring the window forward only when no windows are visible. When the window is already visible and the app is not hidden, no-op — AppKit handles genuine Dock-click activation independently of this delegate method. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(marketplace): persist removals and harden against silent save failures (#264) Removing a marketplace from Settings → External Sources didn't survive relaunch. Three concurrent issues: - Confirmation dialog read marketplaceToRemove after dismissal (racy); switched to the presenting: form so the action captures by value, matching the pattern already used in HarnessDetailDependencyView. - save() swallowed all errors with try?; now logs via TermQLogger.io and exposes lastPersistenceError on the store. - A re-seed (after a defaults reset or version bump) could re-add a default the user had explicitly removed. Added tombstone tracking (marketplaces.removedDefaultURLs.v1) honoured on seed; the explicit Restore Defaults button bypasses tombstones via force: true. MarketplaceStore.init now accepts optional fileURL and UserDefaults for isolated tests; new MarketplaceStoreTests.swift covers seeding, removal persistence, tombstone behaviour, and dedup. Fixes #260 Co-authored-by: David Collie <support@eyelock.net> * fix(security): align OSC 52 clipboard runtime default with Settings UI The runtime gate in TerminalHostView defaulted to true on unset while SettingsView displayed false — so a never-touched user saw "Off" in Settings → Data & Security but terminal programs could silently copy to the clipboard. Drop the explicit-true sentinel; UserDefaults.bool returns false when the key is absent, matching the Settings UI and every other read site for this key. Surgical version of #270 for the v0.9.6 hotfix line; the develop fix routes the same gate through SettingsStore, which doesn't exist on this branch. Also reflow MarketplaceSidebarTab.swift:168 to satisfy swift-format (carried over from the #264 cherry-pick), and add a 0.9.6 CHANGELOG section covering the three backports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.6 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * hotfix(v0.9.7): tolerate YNH 0.2.x list/info shapes (#296) v0.9.5/0.9.6 hard-coded the YNH 0.3 structured-output shape, but YNH 0.3 was never published to the Homebrew tap. Every user on `brew install ynh` is on 0.2.3, so harness loading failed entirely: empty Harnesses sidebar, blank Launch card on worktree rows that already had a harness associated. Decoding is now tolerant of both shapes: - `YNHListEnvelope` / `YNHInfoEnvelope` accept either the 0.3 envelope (`{harnesses: [...]}` / `{harness: {...}}`) or a bare `[Harness]` / `HarnessInfo` payload. - `Harness` / `HarnessInfo` accept either `version_installed` (0.3) or `version` (0.2.x). Tests cover both shapes for both call sites. The compat layer is intentional and sticks around past 0.10. Removal plan: once YNH 0.3 ships to the tap, gate behavior on `YNHDetector.capabilityMeets("0.3.0")` for one release, then delete the legacy branches. Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: Update appcast for release v0.9.7 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: Update appcast for release v0.10.0-beta.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: Update appcast for release v0.10.0-beta.2 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: update CHANGELOG for v0.10.0 * fix(merge): remove duplicate init/encode from HarnessInfo after merge conflict resolution * chore: Update appcast for release v0.10.0 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: update CHANGELOG for v0.10.1 * chore: Update appcast for release v0.10.1 Auto-generated appcast files for Sparkle auto-updates. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore: update CHANGELOG for v0.11.0 * chore: sync appcast entries for v0.11.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: David Collie <support@eyelock.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
release: publishedevent), before GitHub's API indexed the new release. The generator received the pre-v0.9.0 cached list of exactly 30 items and silently skipped the new release.github.event.release.tag_namewas only available under the now-removedrelease: publishedtrigger.Changes
update-appcast.yml— replacerelease: published+ explicitworkflow_dispatchwithworkflow_runon Release workflow completion. By the time the Release workflow finishes (30–60 min build/sign/notarize), the API has long indexed the new release. Add job-level guard to skip when the Release workflow failed. Fix stalegithub.event.release.tag_name→github.event.workflow_run.head_branch.release.yml— remove the explicitgh workflow run update-appcast.ymlstep (now superseded byworkflow_run).generate-appcast.sh— replace the single-page fetch with a page-loop (per_page=100&page=Nuntil empty page), so all releases are fetched regardless of count. AddGH_TOKENauth header to bypass API response cache.Immediate fix
PR #232 was triggered manually and is open with auto-merge — v0.9.0 will land in the appcast once CI passes.
Test plan
🤖 Generated with Claude Code