Skip to content

chore(release): v0.5.4 - #87

Merged
lanfermann-release-bot[bot] merged 3 commits into
mainfrom
release/v0.5.4
Jul 5, 2026
Merged

chore(release): v0.5.4#87
lanfermann-release-bot[bot] merged 3 commits into
mainfrom
release/v0.5.4

Conversation

@lanfermann-release-bot

Copy link
Copy Markdown
Contributor

Promotion PR for v0.5.4.

Merging this releases it: release.yml tags v0.5.4, builds + signs + notarizes the DMG, uploads it to the GitHub release, updates the Homebrew cask, and syncs stage back from main.

jaylann and others added 3 commits July 4, 2026 20:02
…, coverage (#82)

## Summary

Implements the fixes from a full-codebase review (5 parallel review
agents + direct verification). Organized into the tiers from the review
plan; each tier is independently reviewable in the two commits.

**245 tests pass (+23 new), `just check` clean, build + launch smoke
test green.**

## Tier 1 — behavioral bugs (`eaad5f5`)
- **Rate limiting:** `403/429` + rate-limit headers →
`ClientError.rateLimited(until:)`; the poll loop backs off to the reset
time (skipping the request-heavy CI/repo-feed hydration while limited)
instead of hammering into GitHub's *secondary* limit. Message surfaced
to the UI.
- **Legacy-token dead-end:** a `401` on legacy-token migration now drops
the dead token so the UI falls back to sign-in instead of a permanent
first-load skeleton.
- **Device-flow `slow_down`:** replace the poll interval instead of
accumulating it.
- **Notification 401s:** `markRead`/`markAllRead` route to the reconnect
prompt, matching the quick-action path.
- **`reviews()` pagination:** walk the `Link` header to the last page so
a >100-review PR's latest verdict gates Approve/Merge.

## Tier 2 — URL/scheme hardening (`fbe146b`)
- Reject non-`https` base URLs in `GitHubClient.makeRequest` (no bearer
token over cleartext); validate the Enterprise host field at submit with
an inline hint.
- New `WebLink` scheme allowlist gates every URL-open site
(`NSWorkspace` in `NotificationService`, `openURL` in menu/notification
rows) to `http(s)` only — a hostile self-hosted host can't return a
`file:`/`javascript:` deep link.

## Tier 3 — robustness
- Close the concurrent `force`-refresh race (coalesce onto a peer's
fresh run).
- Tolerant ISO8601 decoding (fractional seconds) so one field can't fail
a whole page decode.
- Nil `mergeReadinessTask` on completion/sign-out; reject `.`/`..`
traversal in `normalizedSlug`; reset stale search state when the filter
bar unmounts; persist the OAuth client ID only on success and clear the
pasted PAT on mode-switch/close.
- `justfile`: tee the full build log + surface error lines instead of
`tail -30`.

## Tier 4 — accessibility
- Expose selected/toggle state to VoiceOver on the tab bar, segmented
control, and filter chips; hide the decorative `UnseenDot` when seen.

## Tier 5 — coverage + CI
- New tests: reconnect (in-place token swap / reentrancy / failure),
`addAccount` invalid + duplicate, maintainer-without-push merge,
CI-passed banner, https rejection, `WebLink`, `normalizedSlug`
traversal.
- Cache resolved SwiftPM/Tuist deps in the build & test CI jobs.

## Deferred (intentionally, want your call)
- **Full Dynamic Type support** — anchoring every font token to a text
style would materially change the dense, fixed-width popover layout;
it's a design decision for you rather than a silent refactor.
- **Protocol-level `GitHubAPIError`** — the store still matches
`GitHubClient.ClientError`. YAGNI until a second `GitHubAPI` backend
exists; low value, non-trivial churn.
- **Decoding GitHub error message bodies** into `ClientError.http` —
would change its `Equatable` shape and ripple through many test matches
for marginal benefit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
… republish (#84) (#85)

## What

Fixes #84 — the post-approval **merge-readiness poll**'s gate write
being clobbered by a concurrent **CI hydration wave**'s batch republish,
which briefly hides (or wrongly shows) the Merge button after an
approval.

## Root cause

`prGates` (the per-PR Approve/Merge action gate) is written by two
`@MainActor` flows that interleave across `await`:
- the hydration wave (`drainChecks`/`fold`/`publishChecks`), which
refetches many PRs and republishes gates in batches;
- `refreshPRState`, a single-PR gate refetch driven by the post-approve
poll.

A wave that read a PR's gate *before* the poll's single-key write would
republish the whole map and clobber it. The `checksGeneration` guard
only catches cross-generation clobbers; this is a same-generation
interleave. #82's `.checksOnly` path widened the window.

## Approach — issue-time write-recency

The naive fixes fail: "poll always wins" shows a Merge button that 405s
when the base advanced; a **commit-time** clock still loses because the
wave's full fetch reads its gate *early* (from the PR detail) but folds
*late* (after its slow checks/reviews legs), so a wave that observed
older state commits last and wrongly wins.

Fix: a monotonic **issue-time** clock (`gateWriteClock` + `prGateSeq`).
Both writers take a tick when they *issue* their fetch — the wave stamps
a per-full-fetch issue seq up front; the poll ticks before its detail
request and defers to any concurrently-issued newer write.
`publishChecks` merges gates onto the live map per key, applying a wave
gate only when its issue tick beats the live one. So the observation
that read the **newer server state wins in both clobber directions**,
regardless of commit order. Checks-only carry-overs and failed fetches
don't compete (they keep the live gate rather than blanking it).

`prGateSeq` is pruned alongside `prGates` at every mutation/reset site.
A documented, bounded, self-healing residual remains (a whole wave
shares one issue instant, so a mid-wave poll beats a late-queued key) —
noted in code; per-key stamping would need a main-actor hop per fetch
and isn't worth it for a transient gate flip.

## Tests

Three deterministic `GatedGitHubAPI` regression tests pinning the
interleavings by issue order:
- `testMergePollGateWriteSurvivesChecksOnlyRepublish` — checks-only
carry-over can't clobber the poll.
- `testLaterIssuedMergePollGateBeatsEarlierIssuedWaveFullFetch` — the
primary #84 trigger; **verified it fails under commit-time stamping**.
- `testLaterIssuedWaveFetchBeatsEarlierMergePollGate` — a fresher wave
fetch beats a staler poll write (guards "poll always wins").

`just check` clean, full suite (269 tests) green. Reviewed over three
rounds (multi-agent + single-agent); final review passed with no
blocking findings.

Closes #84
@lanfermann-release-bot
lanfermann-release-bot Bot requested a review from jaylann as a code owner July 5, 2026 15:31
@lanfermann-release-bot lanfermann-release-bot Bot added this to the v0.5.4 milestone Jul 5, 2026
@lanfermann-release-bot
lanfermann-release-bot Bot merged commit 2988f16 into main Jul 5, 2026
7 checks passed
@lanfermann-release-bot
lanfermann-release-bot Bot deleted the release/v0.5.4 branch July 5, 2026 15:33
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