Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/skills/brew-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: brew-lifecycle
version: "1.5"
last_updated: "2026-09-12"
last_updated: "2026-09-23"
id: brew-lifecycle
one_line_purpose: Manage OS-managed Homebrew packages and RPM/brew placement.
entry_point: docs/skills/brew-lifecycle/SKILL.md
Expand Down Expand Up @@ -178,6 +178,9 @@ not `system_files/bluefin/preinstall.d/`. See [package-set.md](references/packag
- Using `arm:` / `intel:` checksum keys for a Linux cask — those keys are macOS-only and resolve to no checksum on Linux
- Adding unknown keys to `/usr/share/chairlift/config.yml` — ChairLift disables
the whole application on unknown page, group, or field names
- Placing a bundle Brewfile in a subdirectory of
`/usr/share/ublue-os/homebrew/` — ChairLift's `brew_bundles_group` does not
recurse, so the bundle silently never appears on the Applications page
- Writing `/etc/chairlift/config.yml` from image content or setup code; that
path is administrator-owned override state
- Bumping a version number or manual stamp to "trigger" a brew-preinstall re-run — the service is content-addressed; edit the Brewfile and the hash change triggers it automatically
Expand Down Expand Up @@ -208,6 +211,6 @@ After any change to `preinstall.d/` or `brew-preinstall`:

| File | Contents |
|---|---|
| [package-set.md](references/package-set.md) | Current 11-package default set, ChairLift managed cask, what belongs in preinstall.d, fzf/ujust bootstrap, opt-in Brewfiles, shared/ vs bluefin/ placement rule |
| [package-set.md](references/package-set.md) | Current 11-package default set, ChairLift managed cask, what belongs in preinstall.d, fzf/ujust bootstrap, opt-in Brewfiles, ChairLift brew bundles, shared/ vs bluefin/ placement rule |
| [service-mechanics.md](references/service-mechanics.md) | How brew-preinstall.service works, state file format, login flow, ChairLift config ownership, long-time user removal scenario, bonedigger-report integration, merging order, path convention |
| [placement-rules.md](references/placement-rules.md) | No rpm-ostree rule, what can move to brew, Homebrew 6.0 tap trust details, Starship shell initialization |
29 changes: 29 additions & 0 deletions docs/skills/brew-lifecycle/references/package-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,42 @@ These live in `system_files/shared/usr/share/ublue-os/homebrew/` (not in
- `ide.Brewfile`
- `k8s-tools.Brewfile`
- `swift.Brewfile`
- `video-wallpaper.Brewfile` — Hidamari, video wallpaper
- `wallpaper-slideshow.Brewfile` — Damask, wallpaper slideshow

Bluefin-specific (in `system_files/bluefin/`):
- `full-desktop.Brewfile` — GNOME Circle + community flatpaks

These are validated by the `validate-brewfiles.yaml` CI workflow on every PR
that touches `system_files/shared/usr/share/ublue-os/homebrew/**`.

### A second consumer: ChairLift `brew_bundles_group`

`ujust bbrew` is not the only reader of this directory. ChairLift's
`applications_page.brew_bundles_group` in
`/usr/share/chairlift/config.yml` points `bundles_paths` at
`/usr/share/ublue-os/homebrew` and renders one installable bundle per
**immediate** `*.Brewfile` child, naming the bundle after the filename stem.
So a file added here also appears on ChairLift's Applications page without any
further wiring — and the filename is user-visible UI text.

Two consequences when adding a bundle:

- The file must be a **direct** child of the directory and carry the exact
`.Brewfile` suffix. ChairLift does not recurse, and a misplaced file is
silently absent from the UI rather than an error.
- Names must be unique and readable as a bundle label
(`wallpaper-slideshow`, not `wallpaper_slideshow_v2`).

The bundle only covers **install**. Uninstall is ChairLift's
`flatpak_user_group` / `flatpak_system_group`; do not add an uninstall action
for a bundle, and do not invent a group to express one — an unknown group key
fails ChairLift's strict schema validation and disables the whole application.

CI's `scripts/validate-brewfiles.sh` validator skips `flatpak` lines (it only
validates `brew` and `cask` lines), so the unit tests in `test_chairlift_config.py`
are what pin these app ids.

---

## Brewfile scope: shared/ vs bluefin/ for all-variant packages
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
],
"description": "Manage OS-managed Homebrew packages. Use when adding/removing default brew packages, moving tools between RPM and brew, or auditing image-vs-brew placement.",
"version": "1.5",
"last_updated": "2026-09-12",
"last_updated": "2026-09-23",
"doc_type": "procedure"
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Video Wallpaper
# Hidamari plays a video file as the desktop background.
# Opt-in: ChairLift lists this as a bundle on its Applications page, and
# `ujust bbrew` offers it too.

flatpak "io.github.jeffshee.Hidamari"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Wallpaper Slideshow
# Damask cycles your desktop background through a folder or a featured set.
# Opt-in: ChairLift lists this as a bundle on its Applications page, and
# `ujust bbrew` offers it too.

flatpak "app.drey.Damask"
69 changes: 69 additions & 0 deletions tests/test_chairlift_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
ROOT
/ "system_files/shared/usr/share/ublue-os/homebrew/preinstall.d/chairlift.Brewfile"
)
#: The directory ChairLift's brew_bundles_group scans. Its image path is the
#: one in config.yml; this is the repo-side path of the same tree.
BUNDLES_DIR = ROOT / "system_files/shared/usr/share/ublue-os/homebrew"
BOOTC_POLICY = (
ROOT
/ "system_files/shared/usr/share/polkit-1/actions"
Expand Down Expand Up @@ -527,6 +530,72 @@ def test_bundles_paths_point_at_bluefin_brewfiles():
assert group["bundles_paths"] == ["/usr/share/ublue-os/homebrew"]


#: Bundles ChairLift renders on its Applications page, keyed by Brewfile name.
#: ChairLift derives the bundle's display name from the filename, so these
#: stems are user-visible. They must be immediate children of the directory in
#: brew_bundles_group.bundles_paths -- a nested file is never discovered -- and
#: the filename must carry the exact suffix ChairLift globs for.
EXPECTED_BUNDLES = {
"wallpaper-slideshow": "app.drey.Damask",
"video-wallpaper": "io.github.jeffshee.Hidamari",
}


def test_bundle_brewfiles_are_discoverable_by_chairlift():
"""Each bundle must be an immediate *.Brewfile child of bundles_paths.

ChairLift only scans the top level of each configured path and only for
the *.Brewfile suffix, so a misplaced file is silently absent from the UI
rather than an error -- the user just never sees the bundle. Pin both
halves of that discovery contract against the configured path.
"""
configured = _load_config()["applications_page"]["brew_bundles_group"][
"bundles_paths"
]
assert configured == ["/usr/share/ublue-os/homebrew"], (
"update BUNDLES_DIR if the image path in config.yml changed"
)
image_path = configured[0].removeprefix("/usr/share/ublue-os/homebrew")
assert image_path == "", "BUNDLES_DIR assumes the path is the scan root"

for stem in EXPECTED_BUNDLES:
bundle = BUNDLES_DIR / f"{stem}.Brewfile"
assert bundle.is_file(), f"missing bundle Brewfile: {bundle.name}"
assert bundle.parent == BUNDLES_DIR, (
f"{bundle.name} must be an immediate child of {BUNDLES_DIR.name}/; "
"ChairLift does not recurse"
)
assert bundle.stat().st_mode & 0o444 == 0o444, (
f"{bundle.name} must be world-readable so every user's ChairLift "
"can read it"
)


def test_bundle_brewfiles_declare_the_expected_flatpaks():
"""Pin the app ids: a typo here installs the wrong app, or none at all."""
for stem, app_id in EXPECTED_BUNDLES.items():
content = (BUNDLES_DIR / f"{stem}.Brewfile").read_text(encoding="utf-8")
declarations = [
line.strip()
for line in content.splitlines()
if line.strip() and not line.lstrip().startswith("#")
]
assert declarations == [f'flatpak "{app_id}"'], (
f"{stem}.Brewfile must declare exactly flatpak \"{app_id}\", "
f"found {declarations}"
)


def test_bundle_names_do_not_collide_with_other_discovered_brewfiles():
"""A duplicate stem would render two identically-named bundles across discoverable paths."""
all_brewfile_stems = []
for brewfile in ROOT.glob("system_files/**/usr/share/ublue-os/homebrew/*.Brewfile"):
all_brewfile_stems.append(brewfile.stem)
assert len(all_brewfile_stems) == len(set(all_brewfile_stems)), (
f"duplicate bundle names across homebrew directories: {all_brewfile_stems}"
)


def test_help_links_point_at_bluefin():
resources = _load_config()["help_page"]["help_resources_group"]
for key in ("website", "issues", "chat"):
Expand Down
Loading