Skip to content

feat(installed): Save the environment as a Brewfile - #248

Open
ronanrodrigo wants to merge 2 commits into
Homebrew:mainfrom
ronanrodrigo:feat/save-brewfile
Open

ronanrodrigo wants to merge 2 commits into
Homebrew:mainfrom
ronanrodrigo:feat/save-brewfile

Conversation

@ronanrodrigo

@ronanrodrigo ronanrodrigo commented Sep 24, 2026 •

Copy link
Copy Markdown

What and why

BrewUI reads the environment out of Homebrew but cannot write it back: to snapshot, back up or share
what is installed, a user has to leave the app and run brew bundle dump in a terminal. This adds a
Save Brewfile… action to the Installed header, where the installed set already lives.

  • Installed header. A Save Brewfile… button beside "Your packages", disabled until the
    inventory has loaded — there is nothing to dump before that. It asks for a destination in a save
    panel defaulting to ~/Brewfile, then runs brew bundle dump --file=… --force through the shared
    command center, so the run streams in the console like every other brew invocation. The header
    then reports where the file landed, or the failure, carrying the same message the console holds.
  • Placement. The dump is a snapshot of what is installed, so it belongs with that list rather
    than the Configuration tab — which is what review asked for here, and what feat: export installed packages as a Brewfile #210 did.
  • --force. The save panel already asked about replacing an existing file, so the dump need not
    ask again. Cancelling the panel is not a failure and leaves the screen untouched.
  • No new machinery. The dump is a new BrewOperationKind.bundleDump reusing the existing mutating
    pipeline (BrewMutatingCommandFactory → SerialBrewCommandCenter → console), so it behaves like
    uninstall and upgrade: one operation at a time, cancellable, logged.

Prior art

#210 (closed) added the same feature to the Installed header. This branch was written independently
and first put the action on Configuration; review moved it to the Installed header. Nothing from #210
was copied — the implementation follows the current command pipeline.

Validation

macOS 27.0 (26A428) · Apple M4 (arm64) · Xcode 27.0 (27A266) · Swift 6.4.

Check Result
swift build --build-tests Build complete
xcrun swift test --package-path . 2 failures, pre-existing and unrelated: DiscoverListRowViewModelTests and DiscoverPackageDetailViewModelTests expect 12,345 / 3,500 and get 12.345 / 3.500 on this machine (AppleLocale = en_BR). Both reproduce on a clean origin/main worktree, so they are not from this change. Every BrewFeatureInstalled, BrewCore and BrewAccessibilityID suite passes.
mint run swiftformat --lint Clean
mint run swiftlint lint --strict 0 violations in 412 files
BrewUILint over Homebrew HomebrewUpgradeHelper Sources exit 0
scripts/localize verify String catalogs verified
scripts/test-ui 48 tests, 0 failures. Run as xcodebuild test -scheme Brew-UI with a manual ad-hoc identity: on this machine scripts/test-ui aborts before the first test (troubleshooting item 8 in AGENTS.md — CODE_SIGNING_ALLOWED=NO invalidates the runner's signature).
scripts/annotate-flaky-tests No test needed more than one run
Manual checks Not run: VoiceOver and keyboard navigation. Both screenshots below come from the deterministic fixture suite.

Two notes on the tooling, neither caused by this change:

  • The catalog keys were added by hand, because scripts/localize sync cannot see package
    *.stringsdata on this machine (its find looks for Target.build, while the app build emits
    Target-b.build). The three new keys were inserted in the tool's own format and the result checked
    with scripts/localize verify.

  • scripts/test-ui passing here took the xcodebuild invocation above rather than the script.

  • I followed the conventions and workflow, checked for duplicate PRs and kept this change focused.

  • I added regression coverage for bug fixes or explained why automated coverage is impractical, and reported the relevant validation above.

Screenshots

Before After
Installed header before Installed header after

The "before" image comes from building this branch with the header's button and result note removed,
which leaves the header exactly as main renders it. The "after" image is in the machine's dark
appearance; the same header in light appearance is
here.

AI assistance

  • AI was used to generate or assist with generating this PR.

Written by Hermes Agent (Nous Research) running deepseek/deepseek-v4.1-flash through
OpenRouter, under Ronan's direction: the implementation, tests, catalog entries, screenshots and
every command in the table above. The human reviewed the diff and supplied the framing. No manual
accessibility or keyboard pass was performed, as noted in Validation.

Copilot AI lite review requested due to automatic review settings September 24, 2026 12:48
@github-actions github-actions Bot closed this Sep 24, 2026
@github-actions github-actions Bot reopened this Sep 24, 2026
@SMillerDev

Copy link
Copy Markdown
Member

A brewfile is a representation of what's installed. I don't think it makes sense to have buttons for it in the Configuration page.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved findings remain around command display escaping, preview dependency injection, and verifying the action is enabled in UI tests.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds a Configuration action to export the Homebrew environment as a Brewfile via brew bundle dump.

Changes:

  • Adds bundle-dump command plumbing and operation tracking.
  • Adds save-panel UI, result reporting, localization, and accessibility identifiers.
  • Adds unit and UI coverage.
File Change
Tests/​BrewFeatureConfigTests/​ConfigViewModelTests.swift Tests Brewfile save behavior and failures.
Tests/​BrewCoreTests/​BrewOperationIDTests.swift Tests operation classification.
Tests/​BrewCoreTests/​BrewCommandsTests.swift Tests bundle-dump command construction.
Tests/​BrewAccessibilityIDTests/​AXIDTests.swift Tests the new accessibility identifier.
Sources/​BrewRepositoryInterfaces/​Fakes/​Stubs.swift Updates command fakes.
Sources/​BrewRepositoryInterfaces/​CommandJob.swift Handles bundle-dump jobs.
Sources/​BrewFeatureConfig/​Views/​ConfigView.swift Adds the save action and result UI.
Sources/​BrewFeatureConfig/​Views/​ConfigColumnsRoot.swift Provides command dependencies.
Sources/​BrewFeatureConfig/​Views/​BrewfileSavePanel.swift Provides destination selection.
Sources/​BrewFeatureConfig/​ViewModels/​ConfigViewModel+Sections.swift Adds save availability state.
Sources/​BrewFeatureConfig/​ViewModels/​ConfigViewModel.swift Runs exports and tracks outcomes.
Sources/​BrewFeatureConfig/​Resources/​Localizable.xcstrings Adds export-related strings.
Sources/​BrewCore/​Operations/​BrewOperationModels.swift Adds the bundle-dump operation kind.
Sources/​BrewCore/​Operations/​BrewMutatingCommandFactory.swift Extends command construction.
Sources/​BrewCore/​Operations/​BrewCommands.swift Builds bundle-dump arguments.
Sources/​BrewCore/​Operations/​BrewCommand.swift Renders display commands.
Sources/​BrewCLI/​LiveBrewMutatingCommandFactory.swift Implements live command creation.
Sources/​BrewAppEnvironment/​UnimplementedRepositories.swift Updates the fallback factory.
Sources/​BrewAccessibilityID/​AXID.swift Defines the save-button identifier.
Package.swift Adds test dependencies.
BrewUITests/​Tests/​ConfigUITests.swift Covers the Configuration action.
BrewUITests/​Screens/​ConfigScreen.swift Adds UI assertion support.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +28 to +30
var displayCommand: String {
"brew " + arguments.joined(separator: " ")
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressing this in the next commit: shell-quote each displayed argument and add coverage for a path containing spaces.

@ronanrodrigo

ronanrodrigo commented Sep 24, 2026 •

Copy link
Copy Markdown
Author

@SMillerDev A brewfile is a representation of what's installed. I don't think it makes sense to have buttons for it in the Configuration page.

I use it to keep a snapshot of my environment, making it easier to use the same tools across different Macs or set up a new one, especially during the initial setup based on a well-defined/known configuration.

@SMillerDev

Copy link
Copy Markdown
Member

Sure, but it's a snapshot of what's installed. Not of the configuration of your Homebrew install.

@github-actions github-actions Bot closed this Sep 24, 2026
@github-actions github-actions Bot reopened this Sep 24, 2026
The Installed header gains a Save Brewfile… action that writes the installed formulae, casks and taps to a Brewfile, so the environment can be snapshotted, backed up or shared without leaving the app.

The dump runs brew bundle dump --file=… --force through the existing mutating command pipeline as a new .bundleDump operation, so it streams in the console like every other brew invocation, and the header reports the destination or the failure once it finishes.
@ronanrodrigo ronanrodrigo changed the title feat(config): Save the environment as a Brewfile feat(installed): Save the environment as a Brewfile Sep 24, 2026
@ronanrodrigo

Copy link
Copy Markdown
Author

Fair point — moved. The action is now in the Installed header next to the package count, so it dumps the list you are looking at, and Configuration is back to what it was. Commit, body and screenshots (before/after of that header) updated.

@graeme graeme left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@ronanrodrigo : This isn't really a fundamental enough feature to have it front and centre of the main tab. How about sticking it in the File menu? That's where these sort of "export to file" features usually belong on macOS.

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.

4 participants