From 138010dd5907941cbae3d9f875ae6bd1f1d7fcc1 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 19 Oct 2026 20:05:04 +0200 Subject: Add phase 2 design: packages browser and slackrepo builds Package browser with query bar, chips and detail pane over a helper inventory; slackrepo build/rebuild/update/remove and update-all as previewed jobs; a publish gate in the slackrepo config with a separate Publish action; signing key unlock through gpg-preset-passphrase since jobs have no terminal for pinentry. Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-10-19-sb-builder-phase2-design.md | 246 +++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md (limited to 'docs/superpowers/specs') diff --git a/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md b/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md new file mode 100644 index 0000000..f622d30 --- /dev/null +++ b/docs/superpowers/specs/2026-10-19-sb-builder-phase2-design.md @@ -0,0 +1,246 @@ +# sb-builder phase 2: packages and slackrepo builds + +Date: 2026-10-19 +Status: approved +Builds on: `2026-10-19-sb-builder-phase1-design.md` (helper, job model, GUI shell) + +## Goal + +Browse and search the ~10k packages of the SBo-danix tree, select packages +and run slackrepo `build`, `rebuild`, `update` and `remove` on them, plus +`update` of everything, as jobs. Every run is previewed first. Publishing +(the rsync push of the package repository) can be held back per run and +done later with a separate Publish action. Package signing works from the +GUI although jobs have no terminal. + +## Facts this design rests on + +Verified on the VM and in the slackrepo source (`~/Programming/GIT/GITHUB/slackrepo`): + +- slackrepo usage: `slackrepo {build|rebuild|update|remove} [--preview|--dry-run] [item...]`, + `--repo=SBo-danix`. Items may be bare package names. +- `--preview` on build/update does **not** set `OPT_DRY_RUN` (only revert and + remove do, `libexec/functions.d/cmdfunctions.sh:231,398`). Finish hooks that + test only `OPT_DRY_RUN` still run during a preview: today `gotify_hook` + sends a message and `manage_packages_hook` runs without `--dry-run`. +- The live config `/etc/slackrepo/slackrepo_SBo-danix.conf` defines start hook + `danix_gitfetch_hook` (fetch, rebase `danix-current` onto upstream; runs on + every invocation, previews included) and finish hooks + `manage_packages_hook bell_hook genrepos_hook gen_web_hook dep_template_hook rsync_push_hook recap_hook gotify_hook`. + It contains a Gotify key and site hostnames. Stale copies exist in + `repo-html-structure/` and in the upstream slackrepo clone. +- `genrepos_hook` runs `gen_repos_files.sh`, which signs with gpg. + `/root/.genreprc` sets `USE_GPGAGENT=1` and `REPOSOWNERGPG=`. + The agent uses `pinentry-curses` (needs a tty) with + `default-cache-ttl 14400`, `max-cache-ttl 21600`. With the key uncached + and no tty, `gen_repos_files.sh` aborts ("GPG test failed"). +- slackrepo DB (`database_SBo-danix_x86_64.sqlite3`): `revisions` row with + `dep='/'` holds the built `version` and `built` time per itemid + (`category/pkg`); `buildresults` holds the last `result` per itemid + (`ok`, `failed`, `bad`, `aborted`, `removed`). 213 of 799 `buildresults` + rows have legacy keys without a `/` and match no package. +- Hints: `/etc/slackrepo/SBo-danix/hintfiles/.hint`, 623 of them. + +## Part 1: VM side, in the slackrepo_setup repo + +slackrepo_setup becomes the source of truth for the slackrepo config. + +- `slackrepo_SBo-danix.conf`: imported from the VM. Site values move out to + `/etc/slackrepo/SBo-danix.local` (root-only, mode 600, not in git), which the + config sources: rsync target, rsync password and excludes files, Gotify URL + and key. The repo ships `SBo-danix.local.example` with placeholders. +- `sb-publish`: new script, the rsync push of `PKGREPO` that + `rsync_push_hook` does today. Reads its settings from the `.local` file. + `rsync_push_hook` calls it. +- Gates in the config: + - `rsync_push_hook`: return 0 when `SBB_PUBLISH=n`. + - `gotify_hook`: return 0 when `OPT_PREVIEW=y` (as well as `OPT_DRY_RUN=y`). + - `manage_packages_hook`: pass `--dry-run` when `OPT_PREVIEW=y` too. +- `gpg-agent.conf` for root, managed here: today's content plus + `allow-preset-passphrase`. Deploy runs `gpgconf --reload gpg-agent`. +- Terminal runs without `SBB_PUBLISH` behave as today, except that previews + no longer notify Gotify or touch packages. +- The stale config copy in `repo-html-structure` is removed (its own commit + there). +- Deploy (asks first): config to `/etc/slackrepo/`, `.local` created from the + current live values only if missing, `sb-publish` to `/usr/local/bin`, + `gpg-agent.conf` to `/root/.gnupg/`. + +## Part 2: helper (`sbb-remote` 0.2.0) + +### New config keys + +```toml +slackrepo_repo = "SBo-danix" +hintdir = "/etc/slackrepo/SBo-danix/hintfiles" +genreprc = "/root/.genreprc" +``` + +### `inventory` + +One JSON object `{"columns": [...], "rows": [[...], ...]}`, a row per +package directory `//.SlackBuild` in `repo_dir`: + +| column | source | +|---|---| +| `path` | `cat/pkg` | +| `version` | hint `VERSION` if the hint has one, else `.info` `VERSION` | +| `info_version` | `.info` `VERSION` | +| `hint_version` | hint `VERSION` or `null` | +| `built_version`, `built_time` | `revisions`, `dep='/'`, by path; `null` if none | +| `result` | `buildresults` by path; `null` if none | +| `maintainer`, `requires` | `.info` `MAINTAINER`, `REQUIRES` (list) | +| `summary` | `slack-desc` line 1, text inside the parentheses | +| `hint` | `/.hint` exists | + +`.info` and hint files are parsed as `KEY="value"` lines (backslash +continuations joined), never sourced. A missing DB yields `null` build +fields, not an error. + +### Job kinds + +| kind | argv | env | +|---|---|---| +| `slackrepo [--preview] [--no-publish] [pkg...]` | `slackrepo --repo= [--preview] pkg...` | `SBB_PUBLISH=n` with `--no-publish` | +| `publish` | `sb-publish` | | + +- `action` is one of `build`, `rebuild`, `update`, `remove`. +- Package names match `^[A-Za-z0-9._+-]+$` and must name a package in the + tree. Duplicates are dropped. +- `build`, `rebuild`, `remove` need at least one package; `update` with none + means all. +- Kinds now return `(argv, env)`; `_run` merges `env` into the environment. + Existing kinds return an empty env. `meta.json` records `env`. +- While the signing key is locked, `job-start` refuses a non-preview + `slackrepo` job with `{"error": "signing key locked"}`. Previews and + `publish` (which does not sign) are not affected. + +### Signing key + +- Key: `REPOSOWNERGPG` read from `genreprc` with a regex over + `REPOSOWNERGPG="..."` (never sourced). Keygrips from + `gpg --with-colons --with-keygrip --list-secret-keys ` (`grp` lines). +- `gpg-status`: `gpg-connect-agent "keyinfo " /bye` per keygrip; the key + is unlocked when every keygrip reports cached. Returns + `{"unlocked": bool, "key": ""}`; `{"unlocked": null}` when no + key is configured. +- `gpg-unlock`: reads the passphrase from stdin, runs + `gpg-preset-passphrase --preset ` per keygrip with the passphrase on + its stdin, then verifies by signing a temp file with + `gpg --batch --pinentry-mode error -u -bas`. On failure it runs + `gpg-preset-passphrase --forget` per keygrip and returns + `{"error": "bad passphrase"}`. The passphrase is never put on a command + line, in the environment, in a log or on disk. +- To verify in planning: whether a preset passphrase expires with the cache + TTLs or lasts until the agent restarts. The GUI checks state before every + real run either way. + +### Version + +`VERSION = "0.2.0"` in both helper and GUI; the existing banner asks for +`./install.sh --remote` until the VM is updated. + +## Part 3: GUI + +### Packages tab (`sb_builder/packages.py`, `Open > Packages`) + +Layout B from brainstorming: query bar, chip rows, table, detail pane, action +bar. + +- Model: `QAbstractTableModel` over the cached inventory, with a + `QSortFilterProxyModel` subclass doing the filtering. Columns: check, name, + category, version, built, status, maintainer. Sortable. +- Status, derived in the GUI: + + | status | rule | + |---|---| + | `never` | no `built_version` and no `result` | + | `failed` | `result` in `failed`, `bad`, `aborted` | + | `outdated` | `built_version` != `version` | + | `ok` | otherwise | + + Shown as a colored glyph plus the word (status colors from the Dashboard). + `# ponytail:` outdated compares versions only; slackrepo also rebuilds on + SlackBuild or dependency changes, which its preview shows. +- Query bar: whitespace-separated terms, all must match, case-insensitive + substring. Plain term: name or summary. `cat:`, `maint:`, `req:`, + `status:` match that field. +- Chips: overlay group (`personal`, `pentesting`, `upstream` = any other + category) and status group (`ok`, `failed`, `never`, `outdated`). OR within + a group, AND across groups and with the query; an empty group does not + filter. +- Detail pane for the current row: path, summary, `.info`, hint and built + versions, built time, last result, maintainer, REQUIRES, hint present. +- Selection: check column, "Select visible", "Clear", "N selected" label. + Actions apply to the checked packages, or to the current row when none is + checked. +- Action bar: Build, Update, Rebuild, Remove, Update all, and Publish + (confirm dialog, starts the `publish` job). +- Build/Update/Rebuild/Remove/Update all start the preview job + (`slackrepo --preview ...`) and open it in the Jobs tab. +- Inventory: fetched when the tab opens, on its Refresh button, and after any + `slackrepo` or `publish` job ends. While loading, the old rows stay and the + status line says "loading...". + +### Jobs tab additions + +- A finished `ok` preview of kind `slackrepo` shows [Run it] and a ☑ publish + checkbox (initial state from config `builds.publish`). Run it starts the + same action and packages without `--preview`, adding `--no-publish` when the + box is unticked. +- Before starting any non-preview `slackrepo` job (Run it, or anywhere else) + the GUI calls `gpg-status`; when locked it asks for the passphrase + (`QInputDialog` in password mode), calls `gpg-unlock` with the passphrase on + stdin, repeats on `bad passphrase`, then starts the job. Cancel aborts. +- `remote.call` gains an optional `stdin` bytes argument, written to the + QProcess and closed. + +### Main window and Dashboard + +- Status bar: signing key state, 🔒 locked / 🔓 unlocked, refreshed with the + stats poll; hidden when the helper reports no key. +- Dashboard: clicking the Packages tile opens the Packages tab, clicking the + Recent jobs card opens the Jobs tab. +- `Open` menu: Packages, Jobs. + +### GUI config + +```toml +[builds] +publish = true # initial state of the publish checkbox +``` + +## Error handling + +| case | behaviour | +|---|---| +| inventory slow (cold disk ~7s) | async; old rows stay, "loading..." | +| inventory call fails | status bar message, old rows stay | +| preview fails | no [Run it]; the log says why | +| key locked | password dialog; `bad passphrase` asks again; Cancel aborts | +| `job-start` refuses (locked) | same dialog, then retry once | +| busy | existing busy dialog | +| `genrepos` fails anyway | job `failed`, notification, slackrepo's message in the log | +| unknown package | helper rejects by name | + +## Testing + +stdlib `unittest`, headless, no VM. + +- Helper: `inventory` on a fake tree, hint dir and sqlite DB (hint version + wins, legacy keys ignored, missing DB gives nulls, continuation lines); + `slackrepo` kind (unknown package, `build` without packages, `update` + without packages, duplicate names, `--no-publish` sets + `SBB_PUBLISH=n` in env and in the child process); locked key refuses a real + run, not a preview (gpg check patched); `gpg-unlock` feeds stdin to a fake + `gpg-preset-passphrase` on `PATH` and forgets on failed verification. +- GUI: query parser and filter (prefixes, chips, OR/AND); status + derivation; action target is checked rows else current row; [Run it] after + an ok preview, with `--no-publish` when unticked; locked key opens the + dialog before the real job (dialog patched); Dashboard tile clicks open + tabs. +- slackrepo_setup: `bash -n` on config and `sb-publish`; source the config + with stub `rsync`, `curl` and `manage-packages.sh` on `PATH` and check the + gates with and without `SBB_PUBLISH=n` and `OPT_PREVIEW=y`. +- First real run with the user: unlock, preview an update of one package, + run it without publish, then Publish. -- cgit v1.2.3