Tuesday morning I pushed AutoBrew v2.5.0. By Wednesday evening, v2.5.2 was live. Three releases in under 48 hours — not because anything went catastrophically wrong, but because that is what disciplined swift development actually looks like up close: you ship, you catch something, you fix it the same day, and then you take a pass at the things that have been quietly rotting in the corner. This post covers what changed across that short run, why each release existed, and the one spot where I have to be honest about the codebase state I left myself.
AutoBrew v2.5: Backoff, Disk Pressure, a Squashed Sidebar, and a Cleanup Pass
Retry backoff for failed upgrades
The headline addition in v2.5.0 is a retry scheduler for upgrades that fail. Before this, if brew upgrade crashed on a cask — network hiccup, CDN timeout, a formula that briefly had a bad checksum — AutoBrew logged the failure and moved on. It would try again on the next scheduled run, which might be hours away, or it might not retry at all depending on how the user had configured the update window.
Now there is explicit backoff: 1 hour, then 4 hours, then 12 hours. The logic lives in the upgrade scheduler, keyed by cask identifier. On each retry the counter increments; after three strikes the cask is flagged as persistently failing and the user gets a notification that says “this one needs your attention” rather than silently dropping it. It is a small thing but it matters: silent failures are the worst kind in background automation. Users should never have to wonder why an app is still on version 1.4 when AutoBrew has been running all week.
Disk-pressure-aware snapshots
The snapshot engine introduced in v2.4 now checks available disk space before capturing a pre-upgrade snapshot. The threshold defaults to 10 GiB of free space on the home-directory volume. If the volume is tighter than that, the snapshot is skipped for that specific cask and a notification explains why — but the upgrade itself still runs. The rollback button in the Update History sidebar is simply unavailable for that run.
I went back and forth on whether to block the upgrade entirely when disk is low. I decided against it. Blocking feels paternalistic: the user might have accepted that their disk is full and just want the update to go through. Skipping only the snapshot and surfacing a clear reason is the more honest behaviour. The user stays in control; AutoBrew tells the truth about what it did.
The large widget family also picked up a “Run Now” link in this release — a small quality-of-life addition that I had been putting off for two releases. It was straightforward once the widget extension already had URL-scheme wiring from the v2.4 CLI work.
The Collections layout bug I shipped and fixed the same day
v2.5.1 exists because of a layout regression I spotted a few hours after the v2.5.0 release. The Collections sidebar entry was rendering a second, nested NavigationSplitView inside the BrewStore detail pane. On a full-size window it was barely noticeable — two nested split views, slightly off — but on a narrow window the inner list collapsed to a few pixels wide and the “No collection selected” placeholder got clipped to the point of being unreadable.
The fix was replacing that inner NavigationSplitView with a flat HStack split, which is what every other BrewStore section uses. Consistent, readable, done. What I am less happy about is that this got through at all. I review UI at a few standard window sizes before releasing, but I had not tested Collections at narrow widths specifically because it was not a surface I had touched in this release. The regression was a holdover from earlier layout work. Lesson noted: when a view shares structural ancestry with something you just changed, test it explicitly even if you did not touch it directly.
Turnaround from spotting the bug to v2.5.1 being live was about nine hours. That felt right.
Dead code cleanup in v2.5.2
The third release in this run was a cleanup pass, not a feature. Three internal symbols had accumulated without callers and I removed them: AppLogger (a wrapper around Logger(subsystem:category:) that every consumer had already stopped using in favour of direct instantiation), the DoctorReport.Severity.other enum case (never assigned, never matched in any switch), and two stored properties on RestoreWizardStore — sourceURL and restoreList — that were written during bundle load but never read anywhere.
None of these caused bugs. That is exactly the problem. Dead code does not crash; it just sits there accumulating cognitive weight, making the next developer — which is me, six months from now — wonder “is this used somewhere I’m not seeing?”. Removing it is housekeeping, and housekeeping is easier right after a release when the context is fresh.
I also cleaned up .gitignore to stop tracking the .build/ directory and the AutoBrew.xcodeproj/ file, both of which had been drifting in and out of commits. Small thing, but the diff noise was annoying me every time I reviewed commit history.
Side notes: CI signing and the personal site
CI signing, again. Earlier in the week, before v2.5.0 landed, I was still sorting out the signing configuration from the v2.4 work covered last week. The short version: I switched the CI archive step to automatic signing, then switched back to Manual + Profile when automatic signing silently dropped an entitlement, then consolidated the pipeline to route through my own reusable ci-open-source@v1 workflow and dropped a redundant keychain-password secret that had been superseded by the match-password parameter since v1.3.0. The signing configuration is finally stable and I am not touching it again unless Apple gives me a reason to.
Personal site. On Tuesday I pushed a batch of SEO and infrastructure work to marcelrgberger.com: Organisation + LocalBusiness JSON-LD structured data, og:image:alt on every Open Graph image, an llms.txt file to signal my content licensing preferences to AI crawlers (yes, this is a thing now and worth doing early), and an AI-bot allowlist in robots.txt. I also fixed a contrast regression on the legal pages — the prose colours were hardcoded hex values that looked fine in light mode and nearly unreadable in dark mode. Binding them to design tokens took ten minutes; I have no idea how long that had been broken.
The GA4 Consent Mode v2 setup also went in — default-denied pings, cookieless fallback, and a three-step signal on accept so the page does not need a reload to start tracking. GDPR-compliant and actually measuring something now.
What this looks like from the outside
If you are considering having a custom app built and you are wondering what “maintained software” actually means in practice: this week is a reasonable illustration. A feature release, a same-day layout fix, a cleanup pass, CI hygiene, and an honest post about the one thing that should have been caught earlier. No single week is representative, but this one is not unusual either.
If you are building something similar — a macOS utility, a background automation tool, anything that needs to stay reliable without demanding attention — take a look at what I build and how I work. The same patterns apply.
AutoBrew v2.5.2 is available at the AutoBrew page on the App Store and the source is at github.com/marcelrgberger/auto-brew.