From 267eca5a397a83d9cc88b20e462a75d91a6a63d0 Mon Sep 17 00:00:00 2001 From: sramkrishna Date: Wed, 23 Sep 2026 17:36:48 +0000 Subject: [PATCH 1/4] feat(chairlift): add wallpaper slideshow and video wallpaper brew bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChairLift's `applications_page.brew_bundles_group` scans immediate `*.Brewfile` children of `/usr/share/ublue-os/homebrew` and renders each as an installable bundle, so a file dropped in that directory reaches the Applications page with no further wiring. Add two opt-in bundles: - `wallpaper-slideshow.Brewfile` — `flatpak "app.drey.Damask"` - `video-wallpaper.Brewfile` — `flatpak "io.github.jeffshee.Hidamari"` Both are direct children of the scan root with the exact `.Brewfile` suffix, since ChairLift does not recurse and a misplaced file is silently absent from the UI. The bundle covers install only; uninstall stays with ChairLift's `flatpak_user_group` / `flatpak_system_group`, and no new group key was added because an unknown key fails ChairLift's strict schema validation and disables the whole application. `flatpak "..."` is a Bluefin convention that `validate-brewfiles.sh` and `brew bundle` both ignore, so `tests/test_chairlift_config.py` gains three tests pinning the app ids, the 0644 mode, and bundle-name uniqueness — the Brewfile validator alone would pass with zero package checks. The brew-lifecycle skill documents the second consumer of that directory and the non-recursive requirement; `docs/skills/index.*` is regenerated. Assisted-by: Claude via Claude Code Co-authored-by: Claude Hive-Run: projectbluefin/common#1159 Hive-Plan: projectbluefin/common#1159 implementation steps 1-3 Hive-Spec: projectbluefin/common#1159#testing-and-verification Signed-off-by: sramkrishna --- docs/skills/brew-lifecycle/SKILL.md | 7 +- .../brew-lifecycle/references/package-set.md | 30 +++++++++ docs/skills/index.json | 2 +- .../homebrew/video-wallpaper.Brewfile | 6 ++ .../homebrew/wallpaper-slideshow.Brewfile | 6 ++ tests/test_chairlift_config.py | 64 ++++++++++++++++++- 6 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile create mode 100644 system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile diff --git a/docs/skills/brew-lifecycle/SKILL.md b/docs/skills/brew-lifecycle/SKILL.md index c3b1c1fea..9d4e507b3 100644 --- a/docs/skills/brew-lifecycle/SKILL.md +++ b/docs/skills/brew-lifecycle/SKILL.md @@ -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 @@ -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 @@ -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 | diff --git a/docs/skills/brew-lifecycle/references/package-set.md b/docs/skills/brew-lifecycle/references/package-set.md index 9443ca58d..b10105176 100644 --- a/docs/skills/brew-lifecycle/references/package-set.md +++ b/docs/skills/brew-lifecycle/references/package-set.md @@ -101,6 +101,8 @@ 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 @@ -108,6 +110,34 @@ Bluefin-specific (in `system_files/bluefin/`): 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. + +`flatpak "..."` lines are a Bluefin convention, not stock Homebrew Bundle. The +`validate-brewfiles.sh` validator and `brew bundle` both ignore them, so a +flatpak-only Brewfile passes CI with zero package checks. Unit tests, not the +Brewfile validator, are what pin these app ids. + --- ## Brewfile scope: shared/ vs bluefin/ for all-variant packages diff --git a/docs/skills/index.json b/docs/skills/index.json index f6a940d47..d050518e6 100644 --- a/docs/skills/index.json +++ b/docs/skills/index.json @@ -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" }, { diff --git a/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile b/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile new file mode 100644 index 000000000..2444e70a3 --- /dev/null +++ b/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile @@ -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" \ No newline at end of file diff --git a/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile b/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile new file mode 100644 index 000000000..e35cfb21f --- /dev/null +++ b/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile @@ -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" \ No newline at end of file diff --git a/tests/test_chairlift_config.py b/tests/test_chairlift_config.py index 9c416e0df..6a70a9179 100644 --- a/tests/test_chairlift_config.py +++ b/tests/test_chairlift_config.py @@ -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" @@ -527,7 +530,66 @@ def test_bundles_paths_point_at_bluefin_brewfiles(): assert group["bundles_paths"] == ["/usr/share/ublue-os/homebrew"] -def test_help_links_point_at_bluefin(): +#: 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 oct(bundle.stat().st_mode & 0o777) == "0o644", ( + 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.""" + stems = [path.stem for path in BUNDLES_DIR.glob("*.Brewfile")] + assert len(stems) == len(set(stems)), f"duplicate bundle names: {stems}" resources = _load_config()["help_page"]["help_resources_group"] for key in ("website", "issues", "chat"): assert resources[key].startswith("https://"), f"{key} must be https" From de7c2afe879237718a12dc60a1d11b56074d44c9 Mon Sep 17 00:00:00 2001 From: castrojo Date: Fri, 25 Sep 2026 01:15:13 -0400 Subject: [PATCH 2/4] fix(brew): add trailing newlines to Brewfiles --- .../shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile | 2 +- .../usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile b/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile index 2444e70a3..59c50c900 100644 --- a/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile +++ b/system_files/shared/usr/share/ublue-os/homebrew/video-wallpaper.Brewfile @@ -3,4 +3,4 @@ # Opt-in: ChairLift lists this as a bundle on its Applications page, and # `ujust bbrew` offers it too. -flatpak "io.github.jeffshee.Hidamari" \ No newline at end of file +flatpak "io.github.jeffshee.Hidamari" diff --git a/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile b/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile index e35cfb21f..531b37a03 100644 --- a/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile +++ b/system_files/shared/usr/share/ublue-os/homebrew/wallpaper-slideshow.Brewfile @@ -3,4 +3,4 @@ # Opt-in: ChairLift lists this as a bundle on its Applications page, and # `ujust bbrew` offers it too. -flatpak "app.drey.Damask" \ No newline at end of file +flatpak "app.drey.Damask" From 4e616cc7ef8f1d34439653c05f37a7a6cb9adfef Mon Sep 17 00:00:00 2001 From: castrojo Date: Fri, 25 Sep 2026 19:00:23 -0400 Subject: [PATCH 3/4] test(chairlift): restore test_help_links_point_at_bluefin and refine docs - restore separate def test_help_links_point_at_bluefin() function header - clarify that CI's Brewfile validator skips flatpak lines (rather than brew bundle) - relax file mode check to assert world-readability without brittle umask dependence --- docs/skills/brew-lifecycle/references/package-set.md | 7 +++---- tests/test_chairlift_config.py | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/skills/brew-lifecycle/references/package-set.md b/docs/skills/brew-lifecycle/references/package-set.md index b10105176..d1429374a 100644 --- a/docs/skills/brew-lifecycle/references/package-set.md +++ b/docs/skills/brew-lifecycle/references/package-set.md @@ -133,10 +133,9 @@ The bundle only covers **install**. Uninstall is ChairLift's 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. -`flatpak "..."` lines are a Bluefin convention, not stock Homebrew Bundle. The -`validate-brewfiles.sh` validator and `brew bundle` both ignore them, so a -flatpak-only Brewfile passes CI with zero package checks. Unit tests, not the -Brewfile validator, are what pin these app ids. +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. --- diff --git a/tests/test_chairlift_config.py b/tests/test_chairlift_config.py index 6a70a9179..0c464aaac 100644 --- a/tests/test_chairlift_config.py +++ b/tests/test_chairlift_config.py @@ -565,7 +565,7 @@ def test_bundle_brewfiles_are_discoverable_by_chairlift(): f"{bundle.name} must be an immediate child of {BUNDLES_DIR.name}/; " "ChairLift does not recurse" ) - assert oct(bundle.stat().st_mode & 0o777) == "0o644", ( + assert bundle.stat().st_mode & 0o444 == 0o444, ( f"{bundle.name} must be world-readable so every user's ChairLift " "can read it" ) @@ -590,6 +590,9 @@ def test_bundle_names_do_not_collide_with_other_discovered_brewfiles(): """A duplicate stem would render two identically-named bundles.""" stems = [path.stem for path in BUNDLES_DIR.glob("*.Brewfile")] assert len(stems) == len(set(stems)), f"duplicate bundle names: {stems}" + + +def test_help_links_point_at_bluefin(): resources = _load_config()["help_page"]["help_resources_group"] for key in ("website", "issues", "chat"): assert resources[key].startswith("https://"), f"{key} must be https" From 1f0e2295b1bbd96404c9b0245b38595504286da6 Mon Sep 17 00:00:00 2001 From: castrojo <1264109+castrojo@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:17:53 -0400 Subject: [PATCH 4/4] test(chairlift): check bundle stem collision across all homebrew directories Compare bundle stems across both system_files/shared and system_files/bluefin homebrew directories to ensure no duplicate stems exist. --- tests/test_chairlift_config.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_chairlift_config.py b/tests/test_chairlift_config.py index 0c464aaac..e2d934ecc 100644 --- a/tests/test_chairlift_config.py +++ b/tests/test_chairlift_config.py @@ -587,9 +587,13 @@ def test_bundle_brewfiles_declare_the_expected_flatpaks(): def test_bundle_names_do_not_collide_with_other_discovered_brewfiles(): - """A duplicate stem would render two identically-named bundles.""" - stems = [path.stem for path in BUNDLES_DIR.glob("*.Brewfile")] - assert len(stems) == len(set(stems)), f"duplicate bundle names: {stems}" + """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():