From 5d8635c23b03a79da461d3bab673d14ad42d06d4 Mon Sep 17 00:00:00 2001 From: Philippe Matray Date: Sat, 5 Sep 2026 01:48:50 +0200 Subject: [PATCH] feat(pkg): publish an installable package on every release macarchy-install#17. Built locally and read back with tar -tf: 12 commands in /usr/bin, both units and both timers with ExecStart repointed off %h, and the battery udev rule. This package is deliberately NOT a full install, and says so. Most of what install.sh lands is USER configuration -- theme hooks, shell plugins, the Hypr key file, starter configs -- which pacman may not write into $HOME. Those ship as templates under /usr/share/macarchy-core and the scriptlet names every destination, rather than shipping something that looks complete and is not. Running ./install.sh still does the whole job; the package exists so the commands and units can be managed by pacman. Carried over from the slice that proved this pattern (macarchy-install#16), each of which was a real failure there: the job hangs off release-please's own release_created output rather than a `release: [published]` trigger, which never fires because release-please creates the Release with GITHUB_TOKEN and GitHub raises no run from a GITHUB_TOKEN event; pkgver is rewritten from the tag rather than maintained through extra-files, which cannot parse a PKGBUILD and stops release PRs being built at all; and the units are repointed with a grep to confirm, because verbatim they give 203/EXEC. tests/test_pkgbuild.sh reads the code rather than the whole file -- comments name every artefact, so a whole-file grep passes even when the install line is gone -- and checks that the scriptlet names each $HOME destination, since the split is only honest while it does. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SYBhT1xfp3MQ1w3687F4Mp --- .github/workflows/release-please.yml | 71 +++++++++++++++++++++++++++- PKGBUILD | 67 ++++++++++++++++++++++++++ macarchy-core.install | 42 ++++++++++++++++ tests/test_pkgbuild.sh | 50 ++++++++++++++++++++ 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 PKGBUILD create mode 100644 macarchy-core.install create mode 100755 tests/test_pkgbuild.sh diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index df2ad93..6ffc9c7 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -14,8 +14,14 @@ on: push: branches: [main] # Manual re-run, e.g. after a token or permission failure. Idempotent: it re-opens or refreshes - # the same release PR rather than creating a second one. + # the same release PR rather than creating a second one. `package_tag` re-runs + # ONLY the packaging job for an existing tag, for a build that failed. workflow_dispatch: + inputs: + package_tag: + description: "Re-build and re-attach the package for this tag (e.g. v1.2.3). Leave empty for a normal run." + required: false + default: "" permissions: contents: write @@ -31,9 +37,72 @@ jobs: release-please: runs-on: ubuntu-latest timeout-minutes: 15 + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} steps: - uses: googleapis/release-please-action@v5 + id: release with: target-branch: main config-file: release-please-config.json manifest-file: .release-please-manifest.json + + # Attaches an installable package to the release the job above just cut, so a + # release is something you can `pacman -U` instead of a source zip. + # + # IT LIVES HERE, not in a workflow keyed on `release: [published]`, and that is + # the whole point: release-please creates the Release with the default + # GITHUB_TOKEN, and GitHub does not start workflow runs from events raised by + # GITHUB_TOKEN. A `release: published` trigger would never fire on the real + # path -- and would leave no failed run in the Actions tab to notice. This repo + # has already paid for that lesson once. + package: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' || inputs.package_tag != '' + runs-on: ubuntu-latest + # arch=('any'), so the x86_64 image is honest here: nothing is compiled, and + # the python package is co-located with its data rather than dropped into the + # BUILDING interpreter's site-packages. A repo that really compiles needs the + # other treatment -- macarchy-install#18. + container: archlinux:base-devel + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + with: + # The tag, never the default branch: re-running for an older tag must + # not build main's PKGBUILD and clobber that package onto the old release. + ref: ${{ needs.release-please.outputs.tag_name || inputs.package_tag }} + + - name: Build the package + env: + TAG: ${{ needs.release-please.outputs.tag_name || inputs.package_tag }} + # -Syu, not -Sy: a partial upgrade can link the fresh python against a + # glibc this image does not have, and it reads as a build bug. + # github-cli because `gh` lives on the RUNNER, not inside the container. + # makepkg refuses to run as root, so it gets a throwaway user. + # --nodeps because `depends` is a runtime contract for the target machine. + run: | + pacman -Syu --noconfirm --needed git python github-cli + # The tag is the truth. Rewriting pkgver here means the package can + # never carry a version the release does not, and release-please needs + # no extra-files entry to keep them in step. + sed -i "s/^pkgver=.*/pkgver=${TAG#v}/" PKGBUILD + grep -q "^pkgver=${TAG#v}$" PKGBUILD + useradd -m build && chown -R build:build . + su build -c 'makepkg -f --nodeps --noconfirm' + ls -l ./*.pkg.tar.* + + - name: Attach it to the release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.release-please.outputs.tag_name || inputs.package_tag }} + # A GLOB, not a name: PKGEXT is .zst here and .xz on the maintainer's + # machine, and hardcoding either uploads nothing on the other. The version + # check is what stops a mis-targeted manual re-run from clobbering an old + # release with a package built from a different tag. + run: | + test -n "$(echo ./*.pkg.tar.* )" + ls ./*.pkg.tar.* | grep -q "${TAG#v}" \ + || { echo "built package does not carry $TAG — refusing to upload"; exit 1; } + gh release upload "$TAG" ./*.pkg.tar.* --clobber --repo "$GITHUB_REPOSITORY" diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..e935719 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,67 @@ +# Maintainer: Philippe Matray +# +# macarchy-core is not shaped like the other packages in this org: most of what +# install.sh lands is USER CONFIGURATION -- theme hooks, shell plugins, a Hypr +# key file -- which pacman cannot write. So the package splits honestly: +# +# system half -> /usr/bin, /usr/lib/systemd/user, /usr/lib/udev/rules.d +# user half -> /usr/share/macarchy-core/, as templates the scriptlet names +# +# It is deliberately NOT a full install. The scriptlet says so and names every +# destination, rather than shipping something that looks complete and is not. +pkgname=macarchy-core +# Rewritten from the tag by the packaging job before makepkg runs. This value is +# the fallback for a manual makepkg from a checkout. +pkgver=0.4.1 +pkgrel=1 +pkgdesc="macOS behaviours for a MacBook on Omarchy/Asahi: dock, Cmd keys, Cmd+Tab, pinch gestures, auto-brightness" +arch=('any') +url="https://github.com/macarchy/macarchy-core" +license=('MIT') +install=macarchy-core.install +depends=('bash' 'python' 'brightnessctl') +optdepends=('omarchy: the theme hooks and shell plugins target it' + 'grim: bar-contrast samples the screen with it' + 'imagemagick: bar-contrast reduces the sample with it' + 'jq: reading the bar layer geometry' + 'hyprland: the gestures and key grammar') +source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz") +sha256sums=('SKIP') + +package() { + cd "$srcdir/$pkgname-$pkgver" + + # --- the system half ------------------------------------------------------ + local s + for s in style/* hardware/*; do + [[ -f $s && -x $s ]] || continue + install -Dm755 "$s" "$pkgdir/usr/bin/$(basename "$s")" + done + + # The units say ExecStart=%h/.local/bin/… because install.sh symlinks there. + # A package writes nothing into $HOME: verbatim they give 203/EXEC from a + # package that installed perfectly. + local u + for u in systemd/*.service; do + sed 's|%h/\.local/bin/|/usr/bin/|' "$u" > "$srcdir/$(basename "$u").pkg" + grep -q '^ExecStart=/usr/bin/' "$srcdir/$(basename "$u").pkg" # or fail the build + install -Dm644 "$srcdir/$(basename "$u").pkg" \ + "$pkgdir/usr/lib/systemd/user/$(basename "$u")" + done + for u in systemd/*.timer; do + install -Dm644 "$u" "$pkgdir/usr/lib/systemd/user/$(basename "$u")" + done + + install -Dm644 udev/90-battery-charge-limit.rules \ + "$pkgdir/usr/lib/udev/rules.d/90-battery-charge-limit.rules" + + # --- the user half, as templates ----------------------------------------- + # Every one of these belongs somewhere under $HOME that pacman may not touch. + # macarchy-core.install names each destination. + install -d "$pkgdir/usr/share/$pkgname" + cp -r hooks keys examples shell-plugins "$pkgdir/usr/share/$pkgname/" + cp -r agents "$pkgdir/usr/share/$pkgname/" + + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" +} diff --git a/macarchy-core.install b/macarchy-core.install new file mode 100644 index 0000000..bbcdad7 --- /dev/null +++ b/macarchy-core.install @@ -0,0 +1,42 @@ +post_install() { + cat <<'NOTE' + + macarchy-core's commands, units and udev rule are installed system-wide. + + The rest of what install.sh does is USER configuration, and pacman cannot write + into your $HOME. The pieces are under /usr/share/macarchy-core/ — copy the ones + you want: + + S=/usr/share/macarchy-core + + # theme hooks + install -Dm755 $S/hooks/macarchy-dock-theme ~/.config/omarchy/hooks/theme-set.d/ + install -Dm755 $S/hooks/dynamic-wallpaper ~/.config/omarchy/hooks/theme-set.d/ + install -Dm755 $S/hooks/macarchy-bar-contrast ~/.config/omarchy/hooks/theme-set.d/ + install -Dm755 $S/hooks/aquarium/macarchy-bar-contrast ~/.config/omarchy-aquarium/hooks/ + + # shell plugins (control centre, notification centre, Cmd+Tab switcher) + cp -r $S/shell-plugins/* ~/.config/omarchy/plugins/ + + # Cmd-key grammar for Hyprland + install -Dm644 $S/keys/macarchy-keys.lua ~/.config/hypr/macarchy-keys.lua + + # starter configs + ls $S/examples/ + + Then: + systemctl --user enable --now macarchy-auto-appearance.timer macarchy-bar-contrast.timer + + Running the repo's ./install.sh instead does all of this for you; the package + exists so the commands and units can be managed by pacman. + +NOTE +} + +post_upgrade() { + systemctl --user try-restart macarchy-auto-appearance.service macarchy-bar-contrast.service 2>/dev/null || true +} + +pre_remove() { + systemctl --user disable --now macarchy-auto-appearance.timer macarchy-bar-contrast.timer 2>/dev/null || true +} diff --git a/tests/test_pkgbuild.sh b/tests/test_pkgbuild.sh new file mode 100755 index 0000000..9598e00 --- /dev/null +++ b/tests/test_pkgbuild.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# tests/test_pkgbuild.sh — the package must carry what install.sh carries. +# +# macarchy-core splits: the commands, units and udev rule go system-wide, and +# everything that belongs under $HOME (theme hooks, shell plugins, the Hypr key +# file, the starter configs) ships as templates under /usr/share because pacman +# may not write there. That split is honest only while the scriptlet names every +# destination — so this checks both halves. macarchy-install#17. +set -uo pipefail +cd "$(dirname "$0")/.." + +fails=0 +check() { local name=$1; shift; if "$@"; then echo "ok $name"; else echo "FAIL $name"; fails=$((fails+1)); fi; } +# Comments name every artefact, so a whole-file grep passes even when the install +# line is gone. Read the code. +code() { grep -v '^[[:space:]]*#' PKGBUILD; } + +check "every command install.sh ships is packaged" \ + bash -c 'for s in style/* hardware/*; do [ -f "$s" ] && [ -x "$s" ] || continue; grep -q "style/\* hardware/\*" <(grep -v "^[[:space:]]*#" PKGBUILD) || exit 1; done' +check "the units are packaged" grep -q 'systemd/\*.service' <(code) +check "the timers are packaged" grep -q 'systemd/\*.timer' <(code) +check "the udev rule is packaged" grep -q '90-battery-charge-limit.rules' <(code) +check "the user half ships as templates" grep -q 'cp -r hooks keys examples shell-plugins' <(code) + +# The units point at $HOME for install.sh's benefit; verbatim that is 203/EXEC. +check "units are repointed off %h" grep -q "sed 's|%h/" <(code) +check "and the rewrite is checked" grep -q "grep -q '\^ExecStart=/usr/bin/'" <(code) +check "the shipped unit really says %h" grep -q '%h/' systemd/macarchy-auto-appearance.service + +# The split is only honest if the scriptlet names where each piece goes. +check "there is a scriptlet" grep -q 'install=macarchy-core.install' <(code) +for dest in "hooks/theme-set.d" "omarchy/plugins" "hypr/macarchy-keys.lua" "examples"; do + check "scriptlet names $dest" grep -q "$dest" macarchy-core.install +done +check "scriptlet admits it is partial" grep -q "pacman cannot write" macarchy-core.install + +# The workflow lessons from macarchy-install#16, each a real failure there. +WF=.github/workflows/release-please.yml +check "no standalone package workflow" [ ! -e .github/workflows/package.yml ] +check "the job hangs off release_created" grep -q 'release_created' "$WF" +check "not a release: published trigger" bash -c '! grep -q "types: \[published\]" '"$WF" +check "pkgver is rewritten from the tag" grep -q 'pkgver=\${TAG#v}' "$WF" +check "and the rewrite is verified" grep -q 'grep -q "\^pkgver=\${TAG#v}\$" PKGBUILD' "$WF" +check "extra-files is not used" bash -c '! grep -q "extra-files" release-please-config.json' +check "the upload globs" grep -q '\*.pkg.tar.\*' "$WF" +check "the upload clobbers" grep -q -- '--clobber' "$WF" +check "gh is installed in the container" grep -q 'github-cli' "$WF" + +(( fails == 0 )) && echo "all ok" || echo "$fails failed" +exit $(( fails > 0 ))