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
4 changes: 2 additions & 2 deletions .github/workflows/validate-chairlift-config.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Validate ChairLift Config

# Networked drift gate for /usr/share/chairlift/config.yml. It validates
# against the ChairLift release the frostyard/tap cask pins, not upstream
# main; the pin lives in one constant, CHAIRLIFT_SCHEMA_REF in
# against the ChairLift release the ublue-os/homebrew-tap cask pins, not
# upstream main; the pin lives in one constant, CHAIRLIFT_SCHEMA_REF in
# tests/check-chairlift-config, and moves only when the cask does.
#
# This is deliberately NOT part of `just check`: that gate must stay
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ repos:
- id: end-of-file-fixer
# Verbatim upstream ChairLift artwork; keep it byte-identical to
# the release the cask pins so its sha256 stays verifiable.
exclude: ^system_files/shared/usr/share/icons/hicolor/.*/apps/org\.frostyard\.ChairLift.*\.svg$
exclude: ^system_files/shared/usr/share/icons/hicolor/.*/apps/io\.projectbluefin\.chairlift.*\.svg$
- id: trailing-whitespace
- id: check-merge-conflict
- id: detect-private-key
- id: check-added-large-files
# Same artwork: upstream ships these two app icons well over the
# 500 KiB default and we do not re-encode vendored assets.
exclude: ^system_files/shared/usr/share/icons/hicolor/scalable/apps/org\.frostyard\.ChairLift.*\.svg$
exclude: ^system_files/shared/usr/share/icons/hicolor/scalable/apps/io\.projectbluefin\.chairlift.*\.svg$
- repo: https://github.com/rhysd/actionlint
rev: 03d0035246f3e81f36aed592ffb4bebf33a03106 # v1.7.7
hooks:
Expand Down
18 changes: 10 additions & 8 deletions docs/skills/brew-lifecycle/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,13 @@ ChairLift is a managed cask installed for every user through
`system_files/shared/usr/share/ublue-os/homebrew/preinstall.d/chairlift.Brewfile`:

```ruby
tap "frostyard/tap", trusted: true
cask "frostyard/tap/chairlift"
tap "ublue-os/tap", trusted: true
cask "ublue-os/tap/chairlift"
```

The tap line requires `trusted: true`; Homebrew 6 blocks untrusted taps. The
cask must remain pinned upstream in `frostyard/tap` rather than being replaced
with a local mutable download in common.
cask must remain pinned upstream in `ublue-os/tap` rather than being
replaced with a local mutable download in common.

Bluefin owns `/usr/share/chairlift/config.yml`, shipped from
`system_files/shared/usr/share/chairlift/config.yml`. `/etc/chairlift/config.yml`
Expand All @@ -124,7 +124,7 @@ reboot and an update uupd already staged gets re-locked), or
Desktop integration ships from the image, not the cask. Homebrew has one
shared prefix, so the cask's `~/.local/share` desktop entry and icons only
ever reach the first user to run `brew bundle`. `common` ships the upstream
desktop file at `/usr/share/applications/org.frostyard.ChairLift.desktop`
desktop file at `/usr/share/applications/io.projectbluefin.chairlift.desktop`
(`Exec=/home/linuxbrew/.linuxbrew/bin/chairlift-wrapper`) and the three
upstream icons under `/usr/share/icons/hicolor/`, so every user gets a
launcher.
Expand All @@ -136,7 +136,9 @@ Images that supply their own ChairLift migration may invoke
common's existing behavior is unchanged. The opt-in excludes the entire
`chairlift.Brewfile` from tapping, hashing, bundling and managed state, and
protects the historical unqualified, Frostyard-qualified and Bluefin-qualified
ChairLift names from OS-diet removal. Other packages keep their usual lifecycle.
ChairLift cask names (`chairlift`, `frostyard/tap/chairlift` and
`ublue-os/tap/chairlift`) from OS-diet removal. Other packages keep
their usual lifecycle.
Keep that Brewfile dedicated to ChairLift; do not place unrelated packages in it.

The caller must capture any old managed-state authorization before invoking this
Expand All @@ -154,8 +156,8 @@ call an older script that ignores the option.

Homebrew 6.0 syntax — `trusted: true` is required:
```ruby
tap "frostyard/tap", trusted: true
cask "frostyard/tap/chairlift"
tap "ublue-os/tap", trusted: true
cask "ublue-os/tap/chairlift"
```
Without `trusted: true` the tap is blocked and the formula is silently
unavailable. See [placement-rules.md](references/placement-rules.md#homebrew-60-tap-trust-required-as-of-2026-06-11).
Expand Down
7 changes: 4 additions & 3 deletions docs/skills/brew-lifecycle/references/package-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,15 @@ provision managed packages for every variant. The CLI set contains 11 packages:
installs ChairLift for every user:

```ruby
tap "frostyard/tap", trusted: true
cask "frostyard/tap/chairlift"
tap "ublue-os/tap", trusted: true
cask "ublue-os/tap/chairlift"
```

This is OS-managed like the default formula set: add the cask and every user
gets it on next login after update; remove it and users whose state file shows
it as managed have it uninstalled. The `trusted: true` tap flag is required.
The cask must remain pinned upstream in `frostyard/tap`; do not vendor an
The cask must remain pinned upstream in `ublue-os/tap`, where it
tracks the rebranded `projectbluefin/chairlift` releases; do not vendor an
unpinned replacement cask into common.

The cask's desktop entry and icons land in the installing user's
Expand Down
42 changes: 27 additions & 15 deletions docs/skills/brew-lifecycle/references/service-mechanics.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,23 @@ ChairLift is a managed cask installed for every user from
Keep both lines load-bearing:

```ruby
tap "frostyard/tap", trusted: true
cask "frostyard/tap/chairlift"
tap "ublue-os/tap", trusted: true
cask "ublue-os/tap/chairlift"
```

Homebrew 6 requires `trusted: true` for the Frostyard tap, and the cask must
remain pinned upstream in `frostyard/tap`.
Homebrew 6 requires `trusted: true` for the tap, and the cask must remain
pinned upstream in `ublue-os/tap`, which tracks the rebranded
`projectbluefin/chairlift` releases.

Machines upgrading from the pre-rebrand `frostyard/tap/chairlift` cask are
migrated by `brew-preinstall` before it bundles. Detection must read
*installed* state only (`brew info --json=v2 --installed`, which enumerates the
Caskroom and resolves each entry from its own installed caskfile): once both
taps are present, the bare token `chairlift` is ambiguous, so
`brew info --cask chairlift` either errors or answers for the new, uninstalled
cask. An inconclusive answer migrates rather than skips — the bundle that runs
immediately afterwards repairs a redundant uninstall, while a skipped migration
strands the user on v0.10.1 with the hash already stamped.

Bluefin owns the maintainer defaults at `/usr/share/chairlift/config.yml`
(`system_files/shared/usr/share/chairlift/config.yml` in this repo). Admins own
Expand Down Expand Up @@ -162,21 +173,22 @@ user-scope artifacts are first-user-wins.

| Path | Source |
|---|---|
| `/usr/share/applications/org.frostyard.ChairLift.desktop` | upstream `data/org.frostyard.ChairLift.desktop`, `Exec=` rewritten to the absolute wrapper path |
| `/usr/share/icons/hicolor/scalable/apps/org.frostyard.ChairLift.svg` | upstream, verbatim |
| `/usr/share/icons/hicolor/scalable/apps/org.frostyard.ChairLift-flower.svg` | upstream, verbatim |
| `/usr/share/icons/hicolor/symbolic/apps/org.frostyard.ChairLift-symbolic.svg` | upstream, verbatim |
| `/usr/share/applications/io.projectbluefin.chairlift.desktop` | upstream `data/io.projectbluefin.chairlift.desktop`, `Exec=` rewritten to the absolute wrapper path |
| `/usr/share/icons/hicolor/scalable/apps/io.projectbluefin.chairlift.svg` | upstream, verbatim |
| `/usr/share/icons/hicolor/scalable/apps/io.projectbluefin.chairlift-flower.svg` | upstream, verbatim |
| `/usr/share/icons/hicolor/symbolic/apps/io.projectbluefin.chairlift-symbolic.svg` | upstream, verbatim |

All four are vendored from ChairLift v0.10.1 (GPL-3.0, `frostyard/chairlift`)
and must be refreshed from the tag the cask pins whenever it is bumped. The
three icons are byte-identical to upstream, so the claim is checkable:
All four are vendored from ChairLift v0.12.2 (GPL-3.0,
`projectbluefin/chairlift`) and must be refreshed from the tag the cask pins
whenever it is bumped. The three icons are byte-identical to upstream, so the
claim is checkable:

```bash
BASE=https://raw.githubusercontent.com/frostyard/chairlift/v0.10.1/data/icons/hicolor
BASE=https://raw.githubusercontent.com/projectbluefin/chairlift/v0.12.2/data/icons/hicolor
cd system_files/shared/usr/share/icons/hicolor
for icon in scalable/apps/org.frostyard.ChairLift.svg \
scalable/apps/org.frostyard.ChairLift-flower.svg \
symbolic/apps/org.frostyard.ChairLift-symbolic.svg; do
for icon in scalable/apps/io.projectbluefin.chairlift.svg \
scalable/apps/io.projectbluefin.chairlift-flower.svg \
symbolic/apps/io.projectbluefin.chairlift-symbolic.svg; do
diff <(curl -fsSL "$BASE/$icon") "$icon" && echo "ok $icon"
done
```
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/ci-pitfalls/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ This is networked and syncs the declared taps, so it stays outside `just check`.

Sync the complete declared tap set before checking any package names. Per-file
tap setup makes bare-name resolution depend on traversal order: ChairLift adds
`frostyard/tap`, which also provides the five wallpaper casks in `ublue-os/tap`.
`ublue-os/tap`, which also provides the wallpaper casks.
Use fully qualified names in `artwork.Brewfile`; do not hide the collision by
isolating taps or skipping unchanged Brewfiles. Zed's Linux cask lives in
`ublue-os/tap`, not `ublue-os/experimental-tap`.
Expand Down
7 changes: 4 additions & 3 deletions docs/skills/workflow-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Load this when you need to understand **what each GitHub workflow in `projectblu
|---|---|---|
| `validate.yml` | Main PR gate: submodule drift, `just check`, shellcheck, image-registry guard, dconf parity, pre-commit | Tightening repo-local validation or policy guards |
| `validate-brewfiles.yaml` | Validates Brewfile correctness | Changing Brewfile structure or Brewfile validation rules |
| `validate-chairlift-config.yaml` | Checks `/usr/share/chairlift/config.yml` against the schema of the pinned ChairLift release (`CHAIRLIFT_SCHEMA_REF`, currently `v0.10.1`); path-filtered plus a weekly cron | Changing the ChairLift maintainer config, cask pin, or upstream schema assumptions |
| `validate-chairlift-config.yaml` | Checks `/usr/share/chairlift/config.yml` against the schema of the pinned ChairLift release (`CHAIRLIFT_SCHEMA_REF`, currently `v0.12.2`); path-filtered plus a weekly cron | Changing the ChairLift maintainer config, cask pin, or upstream schema assumptions |
| `unit-tests.yml` | Runs `pytest` + `bats` on `system_files/**`, `tests/**`, and the `Justfile`. Triggers on PR, push to `main`, and `merge_group`. | Adding or changing unit tests, or changing the paths they cover |
| `build.yml` | Builds and publishes the `common` OCI layer on merge. Runs parallel per-arch jobs (x86_64 on `ubuntu-24.04`, aarch64 on `ubuntu-24.04-arm`). Build uses rootless `buildah-build`; after build, `sudo skopeo copy` promotes the image into root storage so `push-image` (which uses `sudo podman push`) can find it. Then a `manifest` job assembles the multi-arch manifest, logs into GHCR, signs with keyless OIDC, generates SBOM, and attests SLSA L2. Downstream propagation is handled by Renovate (bluefin/bluefin-lts, ~3h) and dakota's daily cron — there is no direct dispatch from this workflow. | Changing how the shared layer is built or pushed |
| `pr-e2e.yml` | Pre-merge composed-image gate for the PR's common layer (composes + runs common suite via `run-testsuite.yml`) | Changing how PR-time downstream composition is tested |
Expand All @@ -57,8 +57,9 @@ Load this when you need to understand **what each GitHub workflow in `projectblu
`validate-chairlift-config.yaml` validates against an external upstream schema
that can drift without a common commit. It fetches ChairLift's page, group, and
field names and fails closed because unknown keys disable the whole
application. It reads the tag the `frostyard/tap` cask pins rather than
upstream `main`, so it cannot green-light a key the shipped binary rejects.
application. It reads the tag the `ublue-os/tap` cask pins rather
than upstream `main`, so it cannot green-light a key the shipped binary
rejects.
It is a separate workflow on purpose: `just check` must stay hermetic, so no
networked gate belongs in the repo-wide PR check.

Expand Down
4 changes: 2 additions & 2 deletions system_files/shared/usr/libexec/bootc-update-stage
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# bootc-update-stage — privileged helper invoked via ChairLift's fixed
# org.frostyard.ChairLift.bootc.stage polkit action (pkexec exec.path is
# io.projectbluefin.chairlift.bootc.stage polkit action (pkexec exec.path is
# pinned to this exact path; see
# system_files/shared/usr/share/polkit-1/actions/org.frostyard.ChairLift.bootc.policy).
# system_files/shared/usr/share/polkit-1/actions/io.projectbluefin.chairlift.bootc.policy).
#
# Stages the next bootc system image update. Plain `bootc upgrade` fetches
# the new image and queues it as a staged deployment; ostree-finalize-staged
Expand Down
81 changes: 78 additions & 3 deletions system_files/shared/usr/libexec/brew-preinstall
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ eval "$("${BREW_BIN}" shellenv)"
# installability before processing the Brewfile's own tap lines, so on a
# system that has never seen the tap it skips the cask as "requires macOS"
# and still exits 0 — the cask is silently never installed (first boot of
# any image shipping a tap+cask pair, e.g. frostyard/tap + chairlift).
# any image shipping a tap+cask pair, e.g. ublue-os/tap + chairlift).
# Vendored taps are also trusted: brew refuses casks from untrusted taps,
# and the Brewfile's own `trusted:` flag doesn't apply to an already-present
# tap. This runs every boot, before the hash early-exit, because
Expand Down Expand Up @@ -102,6 +102,79 @@ if [[ "${current_hash}" == "${stored_hash}" ]]; then
fi

echo "brew-preinstall: Brewfiles changed (${current_hash:0:12}...), applying..."
bundle_failed=0

# Migrate legacy frostyard/tap/chairlift before bundling new ublue-os tap cask.
# Homebrew's cask_installed? matches on the un-namespaced token ("chairlift")
# and skips upgrade across different taps, leaving users stranded on v0.10.1.
#
# Gate on *installation*, not resolvability: the bare token resolves from any
# tapped source, so a machine that merely has frostyard/tap tapped (ChairLift
# never installed) would otherwise uninstall a non-installed cask, fail, and
# retry the whole run every boot.
#
# Ask what is *installed*, never what the bare token resolves to: ublue-os/tap
# was tapped above, so both taps now provide "chairlift" and
# `brew info --cask chairlift` either raises an ambiguity error or answers for
# the new, still-uninstalled cask. Either answer would skip the migration,
# brew bundle's token-matching cask_installed? would then see ChairLift as
# present, the run would exit 0 and stamp the hash — stranding the user on the
# pre-rebrand v0.10.1 build forever. `brew info --json=v2 --installed`
# enumerates the Caskroom instead and resolves each entry from its own
# installed caskfile, so the tap it reports is the tap that installed it.
#
# Inconclusive answers migrate rather than skip: a redundant uninstall is
# repaired by the bundle that runs immediately after, a skipped migration is
# not repaired at all. Only images that actually bundle the rebranded cask
# take part, so a Brewfile still pinning the old bare token is left alone.
if [[ "${external_chairlift}" -eq 0 ]] \
&& grep -qs "ublue-os/tap/chairlift" "${brewfiles[@]}" \
&& brew list --cask chairlift &>/dev/null; then
installed_chairlift_tap=$(brew info --json=v2 --installed 2>/dev/null \
| jq -r 'first(.casks[]? | select(.token == "chairlift") | .tap // empty) // empty' \
2>/dev/null || true)
# Fully qualified, so this lookup can never be ambiguous.
tapped_chairlift_version=$(brew info --cask --json=v2 ublue-os/tap/chairlift 2>/dev/null \
| jq -r 'first(.casks[]?.version // empty) // empty' 2>/dev/null || true)
installed_chairlift_version=$(brew list --cask --versions chairlift 2>/dev/null \
| awk 'NR == 1 { print $2 }' || true)

chairlift_migrated=0
if [[ "${installed_chairlift_tap}" == "ublue-os/tap" ]]; then
chairlift_migrated=1
elif [[ -z "${installed_chairlift_tap}" \
&& -n "${tapped_chairlift_version}" \
&& "${installed_chairlift_version}" == "${tapped_chairlift_version}" ]]; then
# Installed caskfile records no tap (older brew, or a repaired
# receipt), but the installed build already matches the cask the new
# tap ships — nothing legacy left to migrate.
chairlift_migrated=1
fi

if [[ "${chairlift_migrated}" -eq 0 ]]; then
echo "brew-preinstall: migrating legacy chairlift (installed tap: ${installed_chairlift_tap:-unknown}) before bundle..."
# Name the legacy cask explicitly while its tap is still present;
# once it is gone the bare token is unambiguous again.
legacy_chairlift_token=chairlift
frostyard_tapped=0
if brew tap 2>/dev/null | grep -Fxq frostyard/tap; then
legacy_chairlift_token=frostyard/tap/chairlift
frostyard_tapped=1
fi
if brew uninstall --cask "${legacy_chairlift_token}"; then
# Drop the frozen pre-rebrand tap so the bare token can never
# resolve back to it. Best-effort: a leftover tap is harmless
# once the cask is gone, and other casks may still need it.
if [[ "${frostyard_tapped}" -eq 1 ]]; then
brew untap frostyard/tap \
|| echo "brew-preinstall: warning: could not untap frostyard/tap"
fi
else
echo "brew-preinstall: error: failed to uninstall legacy ${legacy_chairlift_token}"
bundle_failed=1
fi
fi
fi

# Snapshot installed names before brew bundle. A declaration that was already
# installed at this point belongs to the user unless the previous state file
Expand Down Expand Up @@ -144,7 +217,6 @@ fi
# Install all packages declared in Brewfiles (brew bundle is idempotent).
# Continue after an individual failure so independent Brewfiles still install,
# but leave state untouched so the complete run is retried.
bundle_failed=0
for brewfile in "${brewfiles[@]}"; do
echo "brew-preinstall: bundling ${brewfile}"
# Capture output for the skipped-cask check, then echo it. Do NOT pipe
Expand Down Expand Up @@ -197,7 +269,10 @@ previous_packages=$(jq -r '.packages[]? // empty' "${STATE_FILE}" 2>/dev/null \
# work even if the old Brewfile has already disappeared from the new image.
previous_casks=$(jq -r --argjson external "${external_chairlift}" '
.casks[]? // empty |
select($external == 0 or (. != "chairlift" and . != "frostyard/tap/chairlift" and . != "ublue-os/tap/chairlift"))
select($external == 0
or (. != "chairlift"
and . != "frostyard/tap/chairlift"
and . != "ublue-os/tap/chairlift"))
' "${STATE_FILE}" 2>/dev/null \
| sort -u || true)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[Desktop Entry]
# Vendored from ChairLift v0.10.1 (data/org.frostyard.ChairLift.desktop),
# https://github.com/frostyard/chairlift — GPL-3.0. The only edit is the
# Vendored from ChairLift v0.12.2 (data/io.projectbluefin.chairlift.desktop),
# https://github.com/projectbluefin/chairlift — GPL-3.0. The only edit is the
# absolute Exec= path below.
#
# Homebrew's chairlift cask installs this entry and its icons under the
Expand All @@ -11,7 +11,7 @@
# docs/skills/brew-lifecycle/references/service-mechanics.md.
Name=ChairLift
Exec=/home/linuxbrew/.linuxbrew/bin/chairlift-wrapper
Icon=org.frostyard.ChairLift
Icon=io.projectbluefin.chairlift
Terminal=false
Type=Application
Categories=System;GTK;
Expand Down
Loading
Loading