# sethLabels Packaging Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Implement the deployment-fork packaging pipeline defined by `sethlabels-docs/specs/2026-04-29-packaging-design.md` — produce installable `.deb` + AppImage artifacts on Debian-family Linux and a Homebrew tap formula for macOS, while honoring strict-zero-source-patches (Invariant I1). **Architecture:** All sethLabels content lives in NEW top-level dirs (`scripts/`, `packaging/`, `sethlabels-docs/`, `tests-impl/`) and one new repo-root file (`README.sethlabels.md`). The Homebrew tap lives in a separate Gitea repo (`homebrew-tap`). Pure-logic scripts (`compute-version.sh`, `check-no-upstream-edits.sh`, `deps-debian.sh`) are TDD'd with `bats-core`. Build scripts (`build-deb.sh`, `build-appimages.sh`) are validated by inline smoke tests (T1–T4 from spec §10) against the artifacts they produce. **Tech Stack:** Bash (with `set -euo pipefail`), CMake/CPack (already present upstream), `linuxdeploy` + `linuxdeploy-plugin-qt` (for AppImage bundling), `bats-core` (shell test framework, `apt install bats`), Homebrew Formula DSL (Ruby). **Spec discrepancies to fix during implementation:** 1. **Spec §5.2 omits `-D CPACK_PACKAGE_NAME=glabels-qt`.** Upstream `CMakeLists.txt:86` sets `CPACK_PACKAGE_NAME=glabels` — without the override the `.deb` would be named `glabels_${VERSION}_amd64.deb`, contradicting decision D6. Task 7 adds the override. 2. **Spec §F9 requires pinned linuxdeploy versions, no specific tag named.** Task 5 has the implementer query GitHub for the latest release tag of `linuxdeploy/linuxdeploy` and `linuxdeploy/linuxdeploy-plugin-qt`, then hardcode those tags in `scripts/lib/linuxdeploy.sh`. 3. **Spec §2 calls `.gitignore` "a one-time scaffold-time touch", but §5.5's allowlist permits ongoing edits.** Task 1 adds new entries (`build/`, `scripts/.cache/`, `*.AppImage`); the guardrail allows this since `.gitignore` is in the allowlist. **Plan-execution context:** This plan is best executed in a worktree via `superpowers:using-git-worktrees`. The plan does not create the worktree itself — the executor sets that up before starting Task 1. --- ### File Structure | Path | Responsibility | Created in | |------|----------------|------------| | `scripts/compute-version.sh` | Emit `-seth` to stdout. Pure logic. | Task 2 | | `scripts/check-no-upstream-edits.sh` | Guardrail enforcing I1; exits 1 on any non-allowlisted upstream-file edit. | Task 3 | | `scripts/lib/deps-debian.sh` | Single source of truth for build deps; sourceable + executable. | Task 4 | | `scripts/lib/linuxdeploy.sh` | Bootstrap + cache `linuxdeploy` and `linuxdeploy-plugin-qt`. | Task 5 | | `packaging/deb-metadata.env` | Maintainer + section + homepage for CPack DEB. | Task 6 | | `packaging/appimage-recipe.env` | linuxdeploy plugin allowlist + exclude list. | Task 6 | | `packaging/changelog.md` | Human-readable per-release notes. | Task 6 | | `scripts/build-deb.sh` | Driver: deps → guardrail → version → cmake → cpack → smoke (T1, T2). | Task 7 | | `scripts/build-appimages.sh` | Driver: deps → guardrail → version → cmake → linuxdeploy x2 → smoke (T3, T4). | Task 8 | | `scripts/README.md` | Operator-facing run instructions. | Task 9 | | `README.sethlabels.md` | Repo-root entry point: fork purpose, install methods, build path, link to spec. | Task 10 | | `tests-impl/test-compute-version.bats` | Bats tests for `compute-version.sh`. | Task 2 | | `tests-impl/test-check-no-upstream-edits.bats` | Bats tests for `check-no-upstream-edits.sh`. | Task 3 | | `tests-impl/test-deps-debian.bats` | Bats tests for `deps-debian.sh`. | Task 4 | | `tests-impl/run-all.sh` | One-shot runner: `bats tests-impl/*.bats`. | Task 1 | | `~/bin/homebrew-tap/Formula/glabels-qt.rb` | Brew formula (separate repo). | Task 11 | | `~/bin/homebrew-tap/README.md` | Tap install instructions. | Task 11 | **Why `tests-impl/` and not `tests/`?** Upstream has `test-data/` at root but no `tests/`. Naming our test dir `tests-impl/` (impl = implementation tests, sethLabels-namespaced) avoids any visual collision and keeps the strict-zero boundary unmistakable. --- ## Task 1: Directory skeleton + .gitignore additions **Files:** - Modify: `.gitignore` (append three new patterns inside the existing sethLabels section) - Create: `scripts/`, `scripts/lib/`, `packaging/`, `tests-impl/` (empty for now; populated by later tasks) - Create: `tests-impl/run-all.sh` - [ ] **Step 1: Verify clean working tree before starting** ```bash git status ``` Expected: `nothing to commit, working tree clean` and the current branch is `main` (or your worktree branch). If dirty, stop and resolve before proceeding. - [ ] **Step 2: Create the directory skeleton** ```bash mkdir -p scripts/lib packaging tests-impl ``` - [ ] **Step 3: Append build-artifact patterns to `.gitignore`** Open `.gitignore` and **append** to the existing `# === sethLabels (deployment fork) additions ===` section (do NOT modify upstream entries above it): ``` # Build outputs (out-of-tree) build/ # linuxdeploy + plugin caches (downloaded by scripts/lib/linuxdeploy.sh on first AppImage build) scripts/.cache/ # Locally-produced AppImage artifacts *.AppImage ``` - [ ] **Step 4: Verify the strict-zero allowlist still covers .gitignore** ```bash grep '\\.gitignore' sethlabels-docs/specs/2026-04-29-packaging-design.md ``` Expected: matches in §5.5 `allowed_pattern` and the §2 invariants table — `.gitignore` is allowlisted, so this edit does not violate I1. - [ ] **Step 5: Create `tests-impl/run-all.sh`** ```bash cat > tests-impl/run-all.sh <<'EOF' #!/usr/bin/env bash # Run the full bats test suite for sethLabels packaging scripts. # Requires: bats (apt install bats). set -euo pipefail cd "$(dirname "$0")" exec bats *.bats EOF chmod +x tests-impl/run-all.sh ``` - [ ] **Step 6: Verify the new dirs are tracked-empty (or .keep'd)** Empty dirs aren't tracked by git. We want the dirs visible after `git add`, so create a placeholder where needed and tracked-empty elsewhere is fine since later tasks populate them. ```bash ls -la scripts/ scripts/lib/ packaging/ tests-impl/ ``` Expected: each dir exists; `tests-impl/run-all.sh` is present and executable. - [ ] **Step 7: Commit** ```bash git add .gitignore scripts/ packaging/ tests-impl/ git commit -m "chore: add packaging directory skeleton + .gitignore build patterns" ``` (`scripts/` and `packaging/` will commit only if non-empty — if not, that's fine; later tasks add files and push them.) --- ## Task 2: `scripts/compute-version.sh` (TDD) **Files:** - Test: `tests-impl/test-compute-version.bats` - Create: `scripts/compute-version.sh` **What it does:** Emits `-seth` to stdout, where `` = `git describe --tags --abbrev=0 upstream/master` and `` = count of existing `-seth*` tags + 1. Pure logic, idempotent under serial single-author releases (spec §5.4). - [ ] **Step 1: Confirm `bats` is installed** ```bash which bats || sudo apt install -y bats bats --version ``` Expected: prints a version (>= 1.7). - [ ] **Step 2: Write the failing tests** Create `tests-impl/test-compute-version.bats`: ```bash #!/usr/bin/env bats # Tests for scripts/compute-version.sh # Invokes the real script against the real repo state. setup() { REPO_ROOT="$(git rev-parse --show-toplevel)" SCRIPT="$REPO_ROOT/scripts/compute-version.sh" } @test "script exists and is executable" { [ -x "$SCRIPT" ] } @test "output matches '-seth' format" { run "$SCRIPT" [ "$status" -eq 0 ] [[ "$output" =~ ^[0-9].+-seth[0-9]+$ ]] } @test "N=1 when no prior seth-tags exist" { # This test assumes a clean tag db. If there ARE existing seth-tags, this # test will report N>1 and that's the correct value. Skipped if any # upstream-tag-seth* tag already exists. upstream_tag=$(git describe --tags --abbrev=0 upstream/master) existing=$(git tag --list "${upstream_tag}-seth*" | wc -l) if [ "$existing" -gt 0 ]; then skip "seth-tags already exist (count=$existing); N=1 invariant only holds on first release" fi run "$SCRIPT" [ "$status" -eq 0 ] [[ "$output" == "${upstream_tag}-seth1" ]] } @test "N increments past existing seth-tags" { upstream_tag=$(git describe --tags --abbrev=0 upstream/master) # Create a fake seth-tag for this test, then clean up. fake_tag="${upstream_tag}-seth99" git tag "$fake_tag" 2>/dev/null || true run "$SCRIPT" git tag -d "$fake_tag" >/dev/null 2>&1 || true [ "$status" -eq 0 ] # Existing count was at least 1 (our fake), so N >= 2. n="${output##*-seth}" [ "$n" -ge 2 ] } @test "fails cleanly when upstream/master ref is missing" { # Run in a temp git repo with no upstream remote. tmp=$(mktemp -d) cd "$tmp" git init -q git commit --allow-empty -m "init" -q run "$SCRIPT" cd "$REPO_ROOT" rm -rf "$tmp" [ "$status" -ne 0 ] } ``` - [ ] **Step 3: Run tests; confirm they fail** ```bash bats tests-impl/test-compute-version.bats ``` Expected: all tests fail because `scripts/compute-version.sh` doesn't exist yet. The first test (`script exists and is executable`) reports `[ -x "$SCRIPT" ]` failed. - [ ] **Step 4: Write the minimal script to pass the tests** Create `scripts/compute-version.sh`: ```bash #!/usr/bin/env bash # Emit "-seth" version string to stdout. # Pure logic: no side effects. # # CALLER RESPONSIBILITY (per spec §5.4): the local tag db must be fresh. # If invoked outside the release flow, run `git fetch origin --tags` first # or risk a stale value. # # Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.4 (D4) set -euo pipefail upstream_tag=$(git describe --tags --abbrev=0 upstream/master) existing_count=$(git tag --list "${upstream_tag}-seth*" | wc -l | tr -d ' ') next_n=$((existing_count + 1)) echo "${upstream_tag}-seth${next_n}" ``` ```bash chmod +x scripts/compute-version.sh ``` - [ ] **Step 5: Run tests; confirm they pass** ```bash bats tests-impl/test-compute-version.bats ``` Expected: all tests pass (or the `N=1` test reports `skip` if you've already tagged a release in your worktree, which is fine). - [ ] **Step 6: Sanity-check the actual output** ```bash ./scripts/compute-version.sh ``` Expected: `3.99-master618-seth1` (or higher `seth` if you've tagged before). The `3.99-master618` part should match `git describe --tags --abbrev=0 upstream/master`. - [ ] **Step 7: Commit** ```bash git add scripts/compute-version.sh tests-impl/test-compute-version.bats git commit -m "feat: add compute-version.sh + bats tests" ``` --- ## Task 3: `scripts/check-no-upstream-edits.sh` (TDD) **Files:** - Test: `tests-impl/test-check-no-upstream-edits.bats` - Create: `scripts/check-no-upstream-edits.sh` **What it does:** Enforces Invariant I1. Exits 0 silently on clean state; exits 1 with a clear error listing violations otherwise. Catches BOTH committed and uncommitted edits (spec §5.5). - [ ] **Step 1: Write the failing tests** Create `tests-impl/test-check-no-upstream-edits.bats`: ```bash #!/usr/bin/env bats # Tests for scripts/check-no-upstream-edits.sh setup() { REPO_ROOT="$(git rev-parse --show-toplevel)" SCRIPT="$REPO_ROOT/scripts/check-no-upstream-edits.sh" TMP_REPO="" } teardown() { if [ -n "$TMP_REPO" ] && [ -d "$TMP_REPO" ]; then rm -rf "$TMP_REPO" fi } # --- Helpers --- # Build a minimal disposable repo that mimics the sethLabels structure with a # local "upstream/master" ref. Returns its path via stdout. make_test_repo() { local tmp=$(mktemp -d) cd "$tmp" git init -q -b master git config user.email "test@test" git config user.name "test" # Pretend-upstream files echo "upstream content" > UPSTREAM_FILE.md echo "real source" > glabels-source.cpp git add UPSTREAM_FILE.md glabels-source.cpp git commit -m "upstream base" -q # Create a local "upstream/master" ref pointing here git update-ref refs/remotes/upstream/master HEAD # Create a feature branch for sethLabels content git checkout -q -b main echo "$tmp" } # --- Tests --- @test "script exists and is executable" { [ -x "$SCRIPT" ] } @test "exits 0 on clean state with only allowlisted committed changes" { TMP_REPO=$(make_test_repo) cd "$TMP_REPO" mkdir -p scripts packaging sethlabels-docs .claude/handoffs echo "test" > CLAUDE.md echo "test" > scripts/something.sh echo "test" > packaging/x.env echo "test" > sethlabels-docs/spec.md echo "" >> .gitignore git add -A git commit -m "sethLabels additions" -q run "$SCRIPT" [ "$status" -eq 0 ] [ -z "$output" ] } @test "exits 1 when an upstream file is committed-modified" { TMP_REPO=$(make_test_repo) cd "$TMP_REPO" echo "evil edit" >> glabels-source.cpp git add glabels-source.cpp git commit -m "BAD: edit upstream file" -q run "$SCRIPT" [ "$status" -eq 1 ] [[ "$output" == *"glabels-source.cpp"* ]] [[ "$output" == *"strict-zero"* ]] } @test "exits 1 when an upstream file has uncommitted working-tree edits" { TMP_REPO=$(make_test_repo) cd "$TMP_REPO" echo "uncommitted evil edit" >> glabels-source.cpp run "$SCRIPT" [ "$status" -eq 1 ] [[ "$output" == *"glabels-source.cpp"* ]] } @test "exits 0 when only .gitignore is modified (allowlisted)" { TMP_REPO=$(make_test_repo) cd "$TMP_REPO" echo "*.tmp" >> .gitignore git add .gitignore git commit -m "extend gitignore" -q run "$SCRIPT" [ "$status" -eq 0 ] } @test "exits 0 when only CLAUDE.md / IDEA.md / DECISIONS.md / README.sethlabels.md are added" { TMP_REPO=$(make_test_repo) cd "$TMP_REPO" echo "x" > CLAUDE.md echo "x" > IDEA.md echo "x" > DECISIONS.md echo "x" > README.sethlabels.md git add -A git commit -m "add sethLabels root docs" -q run "$SCRIPT" [ "$status" -eq 0 ] } ``` - [ ] **Step 2: Run tests; confirm they fail** ```bash bats tests-impl/test-check-no-upstream-edits.bats ``` Expected: all tests fail (script doesn't exist). - [ ] **Step 3: Write the script to pass** Create `scripts/check-no-upstream-edits.sh`: ```bash #!/usr/bin/env bash # Enforce Invariant I1: no upstream-tracked file is ever edited. # Exits 0 on clean state, 1 on violation. # # Catches BOTH committed drift (commits unique to HEAD vs upstream/master) # AND working-tree drift (uncommitted local edits to tracked files). # # Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.5 (I1, F1) set -euo pipefail # Allowlist: files/dirs sethLabels is permitted to add or modify. # `.gitignore` is the one upstream-file exception (called out in spec §2). allowed_pattern='^\.gitignore$|^\.claude/|^scripts/|^packaging/|^sethlabels-docs/|^tests-impl/|^README\.sethlabels\.md$|^CLAUDE\.md$|^IDEA\.md$|^DECISIONS\.md$' committed=$(git diff --name-only upstream/master..HEAD 2>/dev/null || true) working=$(git diff --name-only HEAD 2>/dev/null || true) all_changes=$(printf "%s\n%s\n" "$committed" "$working" | sort -u | sed '/^$/d') if [ -z "$all_changes" ]; then exit 0 fi violations=$(echo "$all_changes" | grep -vE "$allowed_pattern" || true) if [ -n "$violations" ]; then echo "ERROR: strict-zero policy violated. The following upstream files have been modified:" echo "$violations" echo "" echo "See sethlabels-docs/specs/2026-04-29-packaging-design.md §I1." exit 1 fi exit 0 ``` ```bash chmod +x scripts/check-no-upstream-edits.sh ``` - [ ] **Step 4: Run tests; confirm they pass** ```bash bats tests-impl/test-check-no-upstream-edits.bats ``` Expected: all 6 tests pass. - [ ] **Step 5: Run the guardrail against the real repo** ```bash ./scripts/check-no-upstream-edits.sh && echo "CLEAN" ``` Expected: prints `CLEAN`. If it errors, you've accidentally touched an upstream file — investigate before continuing. - [ ] **Step 6: Commit** ```bash git add scripts/check-no-upstream-edits.sh tests-impl/test-check-no-upstream-edits.bats git commit -m "feat: add check-no-upstream-edits.sh + bats tests (enforces I1)" ``` --- ## Task 4: `scripts/lib/deps-debian.sh` (TDD) **Files:** - Test: `tests-impl/test-deps-debian.bats` - Create: `scripts/lib/deps-debian.sh` **What it does:** Single source of truth for build deps (spec §5.1). When sourced, exposes `SETHLABELS_DEPS` array. When executed, checks each dep is installed and prints an actionable `apt install ...` command if anything is missing. - [ ] **Step 1: Write the failing tests** Create `tests-impl/test-deps-debian.bats`: ```bash #!/usr/bin/env bats # Tests for scripts/lib/deps-debian.sh setup() { REPO_ROOT="$(git rev-parse --show-toplevel)" SCRIPT="$REPO_ROOT/scripts/lib/deps-debian.sh" } @test "script exists and is executable" { [ -x "$SCRIPT" ] } @test "sourceable: exposes SETHLABELS_DEPS array" { source "$SCRIPT" [ "${#SETHLABELS_DEPS[@]}" -gt 5 ] } @test "SETHLABELS_DEPS contains core build tools" { source "$SCRIPT" [[ " ${SETHLABELS_DEPS[*]} " == *" cmake "* ]] [[ " ${SETHLABELS_DEPS[*]} " == *" ninja-build "* ]] [[ " ${SETHLABELS_DEPS[*]} " == *" build-essential "* ]] } @test "SETHLABELS_DEPS contains Qt6 libraries" { source "$SCRIPT" [[ " ${SETHLABELS_DEPS[*]} " == *" qt6-base-dev "* ]] [[ " ${SETHLABELS_DEPS[*]} " == *" qt6-svg-dev "* ]] [[ " ${SETHLABELS_DEPS[*]} " == *" qt6-tools-dev "* ]] } @test "SETHLABELS_DEPS contains barcode + zlib deps" { source "$SCRIPT" [[ " ${SETHLABELS_DEPS[*]} " == *" zlib1g-dev "* ]] [[ " ${SETHLABELS_DEPS[*]} " == *" libqrencode-dev "* ]] [[ " ${SETHLABELS_DEPS[*]} " == *" libzint-dev "* ]] } @test "SETHLABELS_DEPS contains packaging tools" { source "$SCRIPT" [[ " ${SETHLABELS_DEPS[*]} " == *" dpkg-dev "* ]] [[ " ${SETHLABELS_DEPS[*]} " == *" fakeroot "* ]] [[ " ${SETHLABELS_DEPS[*]} " == *" wget "* ]] } @test "executed: prints status and exits 0 (when all installed) OR 1 with apt-install hint" { run "$SCRIPT" if [ "$status" -eq 0 ]; then [[ "$output" == *"All build dependencies present"* ]] else [[ "$output" == *"sudo apt install"* ]] fi } @test "executed: warns if not on Debian/Ubuntu" { # Simulate non-Debian by overriding /etc/os-release path via env var if [ -f /etc/os-release ] && grep -qE '^ID=(debian|ubuntu)' /etc/os-release; then skip "currently on Debian/Ubuntu — non-Debian path covered by source review" fi run "$SCRIPT" [[ "$output" == *"Debian"* || "$output" == *"Ubuntu"* ]] } ``` - [ ] **Step 2: Run tests; confirm they fail** ```bash bats tests-impl/test-deps-debian.bats ``` Expected: all fail. - [ ] **Step 3: Write the script to pass** Create `scripts/lib/deps-debian.sh`: ```bash #!/usr/bin/env bash # Single source of truth for sethLabels build dependencies on Debian-family Linux. # # When SOURCED: exposes SETHLABELS_DEPS array (no side effects). # When EXECUTED: verifies each dep is installed; prints actionable # `sudo apt install ...` command on missing deps; exits 1. # On clean state, prints "All build dependencies present." and exits 0. # # Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.1 set -euo pipefail SETHLABELS_DEPS=( build-essential cmake ninja-build pkg-config qt6-base-dev qt6-base-dev-tools qt6-svg-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools libqt6printsupport6 libqt6svg6 libqt6widgets6 libqt6xml6 libqt6gui6 libqt6concurrent6 libqt6core6 libqt6test6 zlib1g-dev libqrencode-dev libzint-dev libgnubarcode-dev file dpkg-dev fakeroot wget bats ) # Detect sourced vs. executed. # When sourced: BASH_SOURCE[0] != $0 # When executed: BASH_SOURCE[0] == $0 if [ "${BASH_SOURCE[0]}" != "${0}" ]; then return 0 2>/dev/null || exit 0 fi # --- Executed path --- # Sanity check the build host if [ ! -f /etc/os-release ]; then echo "WARNING: /etc/os-release missing; not Debian-family. This script is designed for Debian 13 / Ubuntu LTS." >&2 fi if [ -f /etc/os-release ]; then . /etc/os-release if [[ "${ID:-}" != "debian" && "${ID:-}" != "ubuntu" ]] && [[ "${ID_LIKE:-}" != *debian* && "${ID_LIKE:-}" != *ubuntu* ]]; then echo "WARNING: not running on Debian or Ubuntu (detected ID='${ID:-unknown}'). Build deps may differ." >&2 fi fi missing=() for pkg in "${SETHLABELS_DEPS[@]}"; do if ! dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q "install ok installed"; then missing+=("$pkg") fi done if [ "${#missing[@]}" -gt 0 ]; then echo "Missing build dependencies (${#missing[@]}):" for p in "${missing[@]}"; do echo " - $p" done echo "" echo "Install with:" echo " sudo apt install -y ${missing[*]}" exit 1 fi echo "All build dependencies present (${#SETHLABELS_DEPS[@]} packages verified)." ``` ```bash chmod +x scripts/lib/deps-debian.sh ``` - [ ] **Step 4: Run tests; confirm they pass** ```bash bats tests-impl/test-deps-debian.bats ``` Expected: all 8 tests pass. - [ ] **Step 5: Run the script directly** ```bash ./scripts/lib/deps-debian.sh ``` Expected: either `All build dependencies present.` (if you've installed everything) OR a `Missing build dependencies` listing followed by an `apt install` command. If missing, copy the printed command and run it now — it's needed for Tasks 7 and 8. - [ ] **Step 6: Commit** ```bash git add scripts/lib/deps-debian.sh tests-impl/test-deps-debian.bats git commit -m "feat: add deps-debian.sh (build-dep manifest + checker)" ``` --- ## Task 5: `scripts/lib/linuxdeploy.sh` **Files:** - Create: `scripts/lib/linuxdeploy.sh` **What it does:** Bootstraps `linuxdeploy` and `linuxdeploy-plugin-qt` to a script-local cache (`scripts/.cache/`) on first run. Pinned versions per spec §F9 (no rolling `continuous` tag). This task does NOT use TDD because it makes network calls. We validate it by running it and checking outputs. - [ ] **Step 1: Discover the latest pinned tags from GitHub** Per spec §F9, we must pin specific versions. Query GitHub for the latest releases: ```bash curl -s https://api.github.com/repos/linuxdeploy/linuxdeploy/releases/latest | grep -E '"tag_name"' | head -1 curl -s https://api.github.com/repos/linuxdeploy/linuxdeploy-plugin-qt/releases/latest | grep -E '"tag_name"' | head -1 ``` Record both tag values. As of spec time (2026-04-29) the linuxdeploy project uses rolling `continuous` releases plus dated `1-alpha-*` snapshots; pick the most recent dated `1-alpha-YYYYMMDD-N` tag from the releases page (NOT `continuous` — `continuous` violates F9). If only `continuous` is offered for the qt plugin, fall back to its `master`-pinned commit SHA noted in the response. For the rest of this task, substitute your discovered tags as `LINUXDEPLOY_TAG` and `LINUXDEPLOY_PLUGIN_QT_TAG` in the script below. - [ ] **Step 2: Write the script** Create `scripts/lib/linuxdeploy.sh` (replace the two `` placeholders below with the actual tags discovered in Step 1): ```bash #!/usr/bin/env bash # Bootstrap linuxdeploy + linuxdeploy-plugin-qt to a script-local cache. # # When SOURCED: exposes $LINUXDEPLOY_BIN and $LINUXDEPLOY_PLUGIN_QT_BIN paths # (downloads on first run if missing). # When EXECUTED: ensures both binaries are present and prints their paths. # # Pinned per spec §F9 — version bumps are deliberate, not transparent. # To bump: re-run discovery (Task 5 Step 1 of the implementation plan), update # the two TAG variables below, and verify a fresh AppImage build. # # Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §F9 set -euo pipefail # === PINNED VERSIONS (update deliberately per F9) === LINUXDEPLOY_TAG="" LINUXDEPLOY_PLUGIN_QT_TAG="" # ==================================================== CACHE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/.cache" LINUXDEPLOY_BIN="$CACHE_DIR/linuxdeploy-${LINUXDEPLOY_TAG}-x86_64.AppImage" LINUXDEPLOY_PLUGIN_QT_BIN="$CACHE_DIR/linuxdeploy-plugin-qt-${LINUXDEPLOY_PLUGIN_QT_TAG}-x86_64.AppImage" LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/${LINUXDEPLOY_TAG}/linuxdeploy-x86_64.AppImage" LINUXDEPLOY_PLUGIN_QT_URL="https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/${LINUXDEPLOY_PLUGIN_QT_TAG}/linuxdeploy-plugin-qt-x86_64.AppImage" ensure_tool() { local url="$1" out="$2" label="$3" if [ -x "$out" ]; then return 0 fi mkdir -p "$(dirname "$out")" echo "Downloading $label from $url ..." >&2 if ! wget -q --show-progress -O "$out" "$url"; then echo "ERROR: download failed for $label ($url)" >&2 rm -f "$out" return 1 fi chmod +x "$out" } ensure_tool "$LINUXDEPLOY_URL" "$LINUXDEPLOY_BIN" "linuxdeploy" ensure_tool "$LINUXDEPLOY_PLUGIN_QT_URL" "$LINUXDEPLOY_PLUGIN_QT_BIN" "linuxdeploy-plugin-qt" export LINUXDEPLOY_BIN LINUXDEPLOY_PLUGIN_QT_BIN if [ "${BASH_SOURCE[0]}" = "${0}" ]; then echo "linuxdeploy: $LINUXDEPLOY_BIN" echo "linuxdeploy-plugin-qt: $LINUXDEPLOY_PLUGIN_QT_BIN" fi ``` ```bash chmod +x scripts/lib/linuxdeploy.sh ``` - [ ] **Step 3: Verify the cache dir is gitignored** ```bash git check-ignore -v scripts/.cache/anything 2>&1 || echo "NOT IGNORED (problem)" ``` Expected: shows that `scripts/.cache/` matches a `.gitignore` rule (added in Task 1 Step 3). If it says "NOT IGNORED", revisit Task 1 Step 3. - [ ] **Step 4: Run the script (downloads ~30MB on first run)** ```bash ./scripts/lib/linuxdeploy.sh ``` Expected: prints two paths under `scripts/.cache/`. Both files must be executable AppImages. - [ ] **Step 5: Smoke-verify the downloaded tools work** ```bash "$(./scripts/lib/linuxdeploy.sh | head -1 | awk '{print $2}')" --version ``` Expected: prints linuxdeploy version banner. If FUSE is not available (some VMs / containers), set `APPIMAGE_EXTRACT_AND_RUN=1`: ```bash APPIMAGE_EXTRACT_AND_RUN=1 "$LINUXDEPLOY_BIN" --version ``` If this fails, ensure `libfuse2` is installed (`sudo apt install libfuse2`) or use the extract-and-run env var. - [ ] **Step 6: Re-run to verify caching (no re-download)** ```bash ./scripts/lib/linuxdeploy.sh ``` Expected: instant exit, no `Downloading ...` messages. - [ ] **Step 7: Commit** ```bash git add scripts/lib/linuxdeploy.sh git commit -m "feat: add linuxdeploy.sh bootstrap (pinned per F9)" ``` --- ## Task 6: Packaging metadata files **Files:** - Create: `packaging/deb-metadata.env` - Create: `packaging/appimage-recipe.env` - Create: `packaging/changelog.md` **What they do:** Static configuration files consumed by the build scripts. Keeping these out of the shell scripts means tweaking maintainer info, brew tap pins, or release notes is a single-line edit, not a script change. - [ ] **Step 1: Create `packaging/deb-metadata.env`** ```bash cat > packaging/deb-metadata.env <<'EOF' # CPack DEB metadata overrides — sourced by scripts/build-deb.sh. # All values are passed to cpack as -D CPACK_DEBIAN_PACKAGE_="$VALUE". # # Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.2 MAINTAINER="Seth Freiberg " SECTION="graphics" HOMEPAGE="https://glabels.org" # CPACK_PACKAGE_NAME override — required because upstream sets # CPACK_PACKAGE_NAME=glabels (CMakeLists.txt:86) and decision D6 wants glabels-qt. PACKAGE_NAME="glabels-qt" EOF ``` - [ ] **Step 2: Create `packaging/appimage-recipe.env`** ```bash cat > packaging/appimage-recipe.env <<'EOF' # linuxdeploy / linuxdeploy-plugin-qt configuration — sourced by scripts/build-appimages.sh. # Documents the bundling choices for both AppImages. # # Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.3 # Qt platform plugins to include (linuxdeploy-plugin-qt picks these up automatically; # documenting here for posterity). QT_PLATFORM_PLUGINS="xcb" # Image format plugins. The GUI app needs SVG/PNG support; batch CLI does not. QT_IMAGE_FORMAT_PLUGINS_GUI="svg" QT_IMAGE_FORMAT_PLUGINS_BATCH="" # Files in the AppDir we don't want bundled (linuxdeploy is greedy by default). APPDIR_EXCLUDE_GLOBS=() EOF ``` - [ ] **Step 3: Create `packaging/changelog.md`** ```bash cat > packaging/changelog.md <<'EOF' # sethLabels packaging changelog Per-release packaging notes. Each entry covers what changed in the *packaging*, not what changed upstream. For application-level changes, see the upstream `docs/CHANGELOG.md` and `git log upstream/master`. ## Format ``` ## - bullet describing what changed in this packaging release - ... ``` `` is `-seth` (e.g., `3.99-master618-seth1`), matching the git tag and the artifact filename. See spec §D4. --- ## (unreleased) - First end-to-end release dry run pending. EOF ``` - [ ] **Step 4: Smoke-source the env files** ```bash ( source packaging/deb-metadata.env && echo "MAINTAINER=$MAINTAINER" && echo "PACKAGE_NAME=$PACKAGE_NAME" ) ( source packaging/appimage-recipe.env && echo "QT_PLATFORM_PLUGINS=$QT_PLATFORM_PLUGINS" ) ``` Expected: prints the values, no errors. If `set -u` is on globally, sourcing should not error (no unset vars referenced). - [ ] **Step 5: Commit** ```bash git add packaging/ git commit -m "feat: add packaging metadata + initial changelog" ``` --- ## Task 7: `scripts/build-deb.sh` (with inline smoke tests T1, T2) **Files:** - Create: `scripts/build-deb.sh` **What it does:** End-to-end driver that produces `build/deb/glabels-qt_${VERSION}_amd64.deb` and runs smoke tests T1 (parse-ability) and T2 (binaries present) inline. Aborts with a clear error on any failure. - [ ] **Step 1: Write the script** Create `scripts/build-deb.sh`: ```bash #!/usr/bin/env bash # Build the sethLabels .deb package. # # Pipeline (spec §5.2): # 1. sanity check build host (Debian/Ubuntu, deps present) # 2. strict-zero guardrail # 3. compute version # 4. out-of-tree cmake build # 5. CPack with overrides # 6. inline smoke tests T1, T2 # 7. print artifact path for the operator # # Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.2 set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" echo "==> [1/6] Sanity check build host" "$REPO_ROOT/scripts/lib/deps-debian.sh" echo "==> [2/6] Strict-zero guardrail" "$REPO_ROOT/scripts/check-no-upstream-edits.sh" echo "==> [3/6] Compute version" VERSION="$("$REPO_ROOT/scripts/compute-version.sh")" echo " VERSION = $VERSION" echo "==> [4/6] Out-of-tree cmake build" BUILD_DIR="$REPO_ROOT/build/deb" rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" cmake -S "$REPO_ROOT" -B "$BUILD_DIR" -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build "$BUILD_DIR" --parallel echo "==> [5/6] CPack DEB generation" # shellcheck disable=SC1091 source "$REPO_ROOT/packaging/deb-metadata.env" cd "$BUILD_DIR" cpack -G DEB \ -D CPACK_PACKAGE_NAME="$PACKAGE_NAME" \ -D CPACK_PACKAGE_VERSION="$VERSION" \ -D CPACK_DEBIAN_PACKAGE_NAME="$PACKAGE_NAME" \ -D CPACK_DEBIAN_PACKAGE_MAINTAINER="$MAINTAINER" \ -D CPACK_DEBIAN_PACKAGE_SECTION="$SECTION" \ -D CPACK_DEBIAN_PACKAGE_HOMEPAGE="$HOMEPAGE" \ -D CPACK_DEBIAN_PACKAGE_SHLIBDEPS=ON \ -D CPACK_DEBIAN_FILE_NAME=DEB-DEFAULT cd "$REPO_ROOT" # Resolve the actual artifact filename (CPack uses DEB-DEFAULT naming convention). DEB_ARTIFACT=$(ls "$BUILD_DIR"/${PACKAGE_NAME}_*.deb 2>/dev/null | head -1) if [ -z "$DEB_ARTIFACT" ] || [ ! -f "$DEB_ARTIFACT" ]; then echo "ERROR: expected .deb artifact not found in $BUILD_DIR" >&2 ls -la "$BUILD_DIR" >&2 exit 1 fi echo "==> [6/6] Smoke tests" # T1: dpkg-deb --info parses, version field matches. echo " T1: dpkg-deb --info" T1_OUT=$(dpkg-deb --info "$DEB_ARTIFACT") if ! echo "$T1_OUT" | grep -qE "^ Version: ${VERSION}$"; then echo "ERROR: T1 failed — version field in .deb does not match \$VERSION=$VERSION" >&2 echo "$T1_OUT" >&2 exit 1 fi echo " T1: PASS" # T2: dpkg-deb --contents includes both binaries. echo " T2: dpkg-deb --contents" T2_OUT=$(dpkg-deb --contents "$DEB_ARTIFACT") if ! echo "$T2_OUT" | grep -q '/usr/bin/glabels-qt'; then echo "ERROR: T2 failed — /usr/bin/glabels-qt missing from .deb" >&2 exit 1 fi if ! echo "$T2_OUT" | grep -q '/usr/bin/glabels-batch-qt'; then echo "ERROR: T2 failed — /usr/bin/glabels-batch-qt missing from .deb" >&2 exit 1 fi echo " T2: PASS" # Optional: lintian (warnings-only, non-fatal during battle-test). if command -v lintian >/dev/null 2>&1; then echo " lintian (advisory):" lintian "$DEB_ARTIFACT" || true fi echo "" echo "Artifact: $DEB_ARTIFACT" ``` ```bash chmod +x scripts/build-deb.sh ``` - [ ] **Step 2: Run the build** ```bash ./scripts/build-deb.sh ``` Expected: walks through all 6 steps, ends with `Artifact: build/deb/glabels-qt_3.99-master618-seth1_amd64.deb`. Wall time ~2 minutes on a modern machine. If T1 fails: investigate whether `CPACK_PACKAGE_VERSION` was applied correctly; some upstream `CMakeLists.txt` edits may need a clean build. If T2 fails: check `cmake --install --prefix=/tmp/install build/deb && ls /tmp/install/usr/bin/` to verify upstream's install rules produced both binaries. If only one is present, it's an upstream issue (not a sethLabels bug). - [ ] **Step 3: Inspect the artifact manually** ```bash DEB=$(ls build/deb/glabels-qt_*.deb | head -1) dpkg-deb --info "$DEB" dpkg-deb --contents "$DEB" | head -30 ``` Expected: `Package: glabels-qt`, `Version: 3.99-master618-seth1` (or your computed version), and a sane file listing showing `/usr/bin/...`, `/usr/share/applications/...`, etc. - [ ] **Step 4: Optionally test-install on the build host (non-destructive smoke)** ```bash sudo apt install -y "./$DEB" glabels-qt --version sudo apt remove -y glabels-qt ``` Skip if you'd rather only test on a clean VM (T5 in the release flow). - [ ] **Step 5: Commit** ```bash git add scripts/build-deb.sh git commit -m "feat: add build-deb.sh with inline smoke tests T1, T2" ``` --- ## Task 8: `scripts/build-appimages.sh` (with inline smoke tests T3, T4) **Files:** - Create: `scripts/build-appimages.sh` **What it does:** Produces TWO AppImages — `sethlabels-gui-${VERSION}-x86_64.AppImage` (full Qt6 GUI) and `sethlabels-batch-${VERSION}-x86_64.AppImage` (CLI batch tool, leaner) — using `linuxdeploy` + `linuxdeploy-plugin-qt`. Inline smoke tests T3 (batch `--version`) and T4 (gui `--help` under `QT_QPA_PLATFORM=minimal`). - [ ] **Step 1: Write the script** Create `scripts/build-appimages.sh`: ```bash #!/usr/bin/env bash # Build sethLabels AppImages (GUI + batch). # # Pipeline (spec §5.3): # 1. sanity / guardrail / version-compute (same as build-deb.sh) # 2. out-of-tree cmake build with CMAKE_INSTALL_PREFIX=/usr # 3. cmake --install to staging AppDir # 4. linuxdeploy bundle GUI AppImage # 5. re-stage AppDir for batch-only, linuxdeploy bundle batch AppImage # 6. inline smoke tests T3, T4 # 7. print artifact paths # # Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.3 set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" echo "==> [1/6] Sanity check build host" "$REPO_ROOT/scripts/lib/deps-debian.sh" echo "==> [1/6] Strict-zero guardrail" "$REPO_ROOT/scripts/check-no-upstream-edits.sh" echo "==> [1/6] Compute version" VERSION="$("$REPO_ROOT/scripts/compute-version.sh")" echo " VERSION = $VERSION" # Bootstrap linuxdeploy + plugin-qt; defines $LINUXDEPLOY_BIN and $LINUXDEPLOY_PLUGIN_QT_BIN. # shellcheck disable=SC1091 source "$REPO_ROOT/scripts/lib/linuxdeploy.sh" # linuxdeploy looks for the plugin in PATH; symlink into the cache dir suffices. PLUGIN_DIR="$(dirname "$LINUXDEPLOY_PLUGIN_QT_BIN")" PATH="$PLUGIN_DIR:$PATH" # Plugin file must be named exactly `linuxdeploy-plugin-qt` (no version suffix). PLUGIN_LINK="$PLUGIN_DIR/linuxdeploy-plugin-qt" ln -sf "$LINUXDEPLOY_PLUGIN_QT_BIN" "$PLUGIN_LINK" chmod +x "$PLUGIN_LINK" echo "==> [2/6] Out-of-tree cmake build (install prefix /usr)" BUILD_DIR="$REPO_ROOT/build/appimage" APPDIR_GUI="$BUILD_DIR/AppDir-gui" APPDIR_BATCH="$BUILD_DIR/AppDir-batch" rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" cmake -S "$REPO_ROOT" -B "$BUILD_DIR" -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/usr cmake --build "$BUILD_DIR" --parallel echo "==> [3/6] Stage install tree to AppDirs" DESTDIR="$APPDIR_GUI" cmake --install "$BUILD_DIR" # Batch AppDir gets its own copy so we can prune Qt plugins that GUI needs but batch doesn't. DESTDIR="$APPDIR_BATCH" cmake --install "$BUILD_DIR" # Sanity: both AppDirs must contain both binaries (we strip later, not here). test -x "$APPDIR_GUI/usr/bin/glabels-qt" || { echo "ERROR: GUI binary missing in AppDir-gui" >&2; exit 1; } test -x "$APPDIR_BATCH/usr/bin/glabels-batch-qt" || { echo "ERROR: batch binary missing in AppDir-batch" >&2; exit 1; } echo "==> [4/6] Bundle GUI AppImage" DESKTOP_FILE="$APPDIR_GUI/usr/share/applications/glabels-qt.desktop" ICON_FILE="$APPDIR_GUI/usr/share/icons/hicolor/scalable/apps/glabels.svg" # Upstream's actual desktop filename may vary — list what's there if missing. if [ ! -f "$DESKTOP_FILE" ]; then ALT_DESKTOP=$(find "$APPDIR_GUI/usr/share/applications" -name '*.desktop' | head -1) if [ -n "$ALT_DESKTOP" ]; then DESKTOP_FILE="$ALT_DESKTOP" else echo "ERROR: no .desktop file found in $APPDIR_GUI/usr/share/applications" >&2 exit 1 fi fi cd "$BUILD_DIR" APPIMAGE_EXTRACT_AND_RUN=1 \ "$LINUXDEPLOY_BIN" \ --appdir "$APPDIR_GUI" \ --plugin qt \ --executable "$APPDIR_GUI/usr/bin/glabels-qt" \ --desktop-file "$DESKTOP_FILE" \ --icon-file "$ICON_FILE" \ --output appimage GUI_RAW=$(ls "$BUILD_DIR"/*GUI*.AppImage "$BUILD_DIR"/*glabels-qt*.AppImage 2>/dev/null | head -1) GUI_OUT="$REPO_ROOT/sethlabels-gui-${VERSION}-x86_64.AppImage" mv "$GUI_RAW" "$GUI_OUT" chmod +x "$GUI_OUT" cd "$REPO_ROOT" echo "==> [5/6] Bundle batch AppImage" # Batch doesn't need a desktop file or icon (CLI only). cd "$BUILD_DIR" APPIMAGE_EXTRACT_AND_RUN=1 \ "$LINUXDEPLOY_BIN" \ --appdir "$APPDIR_BATCH" \ --plugin qt \ --executable "$APPDIR_BATCH/usr/bin/glabels-batch-qt" \ --create-desktop-file \ --output appimage BATCH_RAW=$(ls "$BUILD_DIR"/*batch*.AppImage 2>/dev/null | head -1) BATCH_OUT="$REPO_ROOT/sethlabels-batch-${VERSION}-x86_64.AppImage" mv "$BATCH_RAW" "$BATCH_OUT" chmod +x "$BATCH_OUT" cd "$REPO_ROOT" echo "==> [6/6] Smoke tests" # T3: batch AppImage --version exits 0 with non-empty output. echo " T3: batch --version" T3_OUT=$(APPIMAGE_EXTRACT_AND_RUN=1 "$BATCH_OUT" --version 2>&1) || { echo "ERROR: T3 failed — batch AppImage --version exited non-zero" >&2 echo "$T3_OUT" >&2 exit 1 } if [ -z "$T3_OUT" ]; then echo "ERROR: T3 failed — batch AppImage --version produced empty output" >&2 exit 1 fi echo " T3: PASS ($(echo "$T3_OUT" | head -1))" # T4: GUI AppImage --help exits 0 under headless Qt platform. echo " T4: gui --help (QT_QPA_PLATFORM=minimal)" APPIMAGE_EXTRACT_AND_RUN=1 QT_QPA_PLATFORM=minimal "$GUI_OUT" --help >/tmp/sethlabels-gui-help 2>&1 || { echo "ERROR: T4 failed — GUI AppImage --help exited non-zero" >&2 cat /tmp/sethlabels-gui-help >&2 exit 1 } echo " T4: PASS" echo "" echo "Artifacts:" echo " $GUI_OUT" echo " $BATCH_OUT" ``` ```bash chmod +x scripts/build-appimages.sh ``` - [ ] **Step 2: Run the build** ```bash ./scripts/build-appimages.sh ``` Expected: walks through all 6 steps, ends with two artifact paths. Wall time ~5 minutes on a modern machine. If `linuxdeploy` errors with "no $XDG_RUNTIME_DIR" or similar, set the env var: `export XDG_RUNTIME_DIR=/tmp/runtime-$USER && mkdir -p $XDG_RUNTIME_DIR`. If T3 or T4 fails: see spec §F2 — Qt plugin omissions are the usual culprit. Re-run with `LINUXDEPLOY_OUTPUT_VERSION=$VERSION` and inspect the bundle's `usr/plugins/` dir for missing platform/imageformats plugins. - [ ] **Step 3: Smoke-test the artifacts manually** ```bash GUI_OUT=$(ls sethlabels-gui-*.AppImage | head -1) BATCH_OUT=$(ls sethlabels-batch-*.AppImage | head -1) ls -la "$GUI_OUT" "$BATCH_OUT" APPIMAGE_EXTRACT_AND_RUN=1 "$BATCH_OUT" --version APPIMAGE_EXTRACT_AND_RUN=1 QT_QPA_PLATFORM=minimal "$GUI_OUT" --help | head -10 ``` Expected: GUI is ~50–80 MB (bundles Qt6), batch is ~20–40 MB (no GUI plugins). Both run cleanly. - [ ] **Step 4: Verify .gitignore catches the AppImages** ```bash git status ``` Expected: no `*.AppImage` files appear in the untracked list (matched by the `*.AppImage` rule added in Task 1 Step 3). - [ ] **Step 5: Commit** ```bash git add scripts/build-appimages.sh git commit -m "feat: add build-appimages.sh with inline smoke tests T3, T4" ``` --- ## Task 9: `scripts/README.md` **Files:** - Create: `scripts/README.md` **What it does:** Operator-facing run instructions. Documents the order to run scripts, prerequisites, and where artifacts land. Mirrored as the canonical recipe for future CI YAML wrapping (spec I3). - [ ] **Step 1: Write `scripts/README.md`** ```bash cat > scripts/README.md <<'EOF' # sethLabels build scripts Canonical recipe for building sethLabels artifacts. CI YAML at the public-flip will call these scripts unmodified — no logic moves into YAML (spec §I3). ## Quick reference ``` ./scripts/lib/deps-debian.sh # check / install build deps ./scripts/check-no-upstream-edits.sh # enforce strict-zero (I1) ./scripts/compute-version.sh # emit -seth ./scripts/build-deb.sh # → build/deb/glabels-qt__amd64.deb ./scripts/build-appimages.sh # → sethlabels-{gui,batch}--x86_64.AppImage ``` ## Prerequisites Debian 13 (Trixie) or Ubuntu 24.04 LTS. Run: ``` ./scripts/lib/deps-debian.sh ``` If anything is missing, the script prints the exact `sudo apt install ...` command to run. `bats` (bash test framework) is in the dep list — it's required for the implementation tests under `tests-impl/`. `linuxdeploy` and `linuxdeploy-plugin-qt` are NOT apt-installable; they're downloaded automatically by `scripts/lib/linuxdeploy.sh` to `scripts/.cache/` on first AppImage build. ## Versioning `-seth` (e.g., `3.99-master618-seth1`). The `` counter is computed from existing git tags matching `-seth*`. See spec §D4. **Caller responsibility:** the local tag db must be fresh before running `compute-version.sh`. Run `git fetch origin --tags` first if you're not inside the release flow (which fetches tags as step 1). ## Release flow See spec §6 for the canonical step-by-step. TL;DR: ``` git fetch --all --tags git rebase upstream/master ./scripts/check-no-upstream-edits.sh ./scripts/build-deb.sh # ~2 min ./scripts/build-appimages.sh # ~5 min VERSION=$(./scripts/compute-version.sh) git tag "$VERSION" git push origin main --tags # Create Gitea release for $VERSION; attach the three artifacts. # Bump ../homebrew-tap/Formula/glabels-qt.rb (tag + revision); commit; push. # Smoke verify on a clean Debian 13 VM (T5). ``` ## Layout ``` scripts/ ├── README.md ← this file ├── compute-version.sh ← pure logic; emits version string ├── check-no-upstream-edits.sh ← guardrail enforcing I1 ├── build-deb.sh ← end-to-end .deb pipeline ├── build-appimages.sh ← end-to-end AppImage pipeline (GUI + batch) ├── lib/ │ ├── deps-debian.sh ← build-dep manifest + checker │ └── linuxdeploy.sh ← linuxdeploy + plugin-qt bootstrapper └── .cache/ ← gitignored; linuxdeploy AppImages cache ``` ## Tests ``` ./tests-impl/run-all.sh ``` Runs the bats suite for pure-logic scripts. Build-script smoke tests (T1–T4) are inline in `build-deb.sh` and `build-appimages.sh` — they fire automatically on each build. ## Spec The design rationale, invariants, and failure modes live in [`../sethlabels-docs/specs/2026-04-29-packaging-design.md`](../sethlabels-docs/specs/2026-04-29-packaging-design.md). Read it before changing any script. EOF ``` - [ ] **Step 2: Verify the README renders sensibly** ```bash head -40 scripts/README.md ``` Expected: clean Markdown, no obvious typos. - [ ] **Step 3: Commit** ```bash git add scripts/README.md git commit -m "docs: add scripts/README.md (operator run guide)" ``` --- ## Task 10: `README.sethlabels.md` (repo-root entry) **Files:** - Create: `README.sethlabels.md` **What it does:** Repo-root sethLabels entry point. Names the fork's purpose, points readers at install methods, and links to the upstream README and design spec. Strict-zero forbids modifying upstream `README.md`, hence the `.sethlabels.md` suffix. - [ ] **Step 1: Write `README.sethlabels.md`** ```bash cat > README.sethlabels.md <<'EOF' # sethLabels > Deployment fork of [glabels-qt](https://github.com/j-evins/glabels-qt) — Qt6 > label designer / printer, packaged for Debian-family Linux and macOS. This is **not** a code fork. The upstream application is unchanged; sethLabels exists solely to publish installable binary artifacts that upstream explicitly does not provide ("Currently there are no self-hosted binary snapshot releases available… I encourage you to try building the code yourself" — upstream README). For the application itself — what it does, screenshots, full feature list — see the upstream [`README.md`](README.md). ## Install ### Debian / Ubuntu (`.deb`) Download the latest `.deb` from the [releases page](https://git.sethpc.xyz/Seth/sethLabels/releases), then: ``` sudo apt install ./glabels-qt__amd64.deb glabels-qt --version ``` ### Any Linux (AppImage) Download `sethlabels-gui--x86_64.AppImage` from the [releases page](https://git.sethpc.xyz/Seth/sethLabels/releases), make it executable, and run it: ``` chmod +x sethlabels-gui--x86_64.AppImage ./sethlabels-gui--x86_64.AppImage ``` A separate `sethlabels-batch--x86_64.AppImage` provides the CLI for scripted / mail-merge use. ### macOS (Homebrew) ``` brew tap seth/tap https://git.sethpc.xyz/Seth/homebrew-tap.git brew install seth/tap/glabels-qt ``` The explicit URL form is needed because brew defaults to GitHub for tap names. First install builds Qt6 + glabels-qt from source (~5–10 min one-time cost; see spec §D2). Subsequent updates are a fast `brew upgrade`. ## Build from source If you'd rather build the artifacts yourself instead of downloading a release: ``` git clone https://git.sethpc.xyz/Seth/sethLabels.git cd sethLabels ./scripts/lib/deps-debian.sh # check / install build deps ./scripts/build-deb.sh # → build/deb/glabels-qt_*.deb ./scripts/build-appimages.sh # → sethlabels-{gui,batch}-*.AppImage ``` See [`scripts/README.md`](scripts/README.md) for full operator docs. ## How this fork works sethLabels is a **deployment fork**: every sethLabels addition lives in NEW files in NEW top-level directories (`scripts/`, `packaging/`, `sethlabels-docs/`, `tests-impl/`, plus this file). Upstream files are never edited. The single allowlisted exception is `.gitignore`. This discipline is enforced by `scripts/check-no-upstream-edits.sh`. The `-seth` versioning preserves the upstream-lineage in every artifact. Periodic `git rebase upstream/master` is conflict-free by construction. ## Spec & decisions - [Design spec](sethlabels-docs/specs/2026-04-29-packaging-design.md) — invariants, decisions, build pipeline, failure modes - [Decision log](DECISIONS.md) — settled choices + rejected alternatives - [Project brief](IDEA.md) — plain-language motivation ## License The upstream code is licensed under [GPL-3.0](LICENSE). sethLabels-specific files (everything in the dirs listed above, plus this file) are licensed under the same terms. ## Upstream - Upstream: https://github.com/j-evins/glabels-qt (Jaye Evins / glabels.org) - This fork: https://git.sethpc.xyz/Seth/sethLabels - Brew tap: https://git.sethpc.xyz/Seth/homebrew-tap EOF ``` - [ ] **Step 2: Verify guardrail still passes** ```bash ./scripts/check-no-upstream-edits.sh && echo CLEAN ``` Expected: `CLEAN`. `README.sethlabels.md` is in the allowlist (added in Task 3 Step 3's `allowed_pattern`). - [ ] **Step 3: Commit** ```bash git add README.sethlabels.md git commit -m "docs: add README.sethlabels.md (fork entry point)" ``` --- ## Task 11: Homebrew tap repo (separate Gitea repo) **Files (in a separate repo at `~/bin/homebrew-tap/`):** - Create: `~/bin/homebrew-tap/Formula/glabels-qt.rb` - Create: `~/bin/homebrew-tap/README.md` - Create: Gitea repo `git.sethpc.xyz/Seth/homebrew-tap` **What it does:** Provides the macOS install path per spec §7 + §D2. Build-from-source on the user's Mac via brew. This task does NOT modify the sethLabels repo. It creates a parallel sibling repo. - [ ] **Step 1: Create the local repo skeleton** ```bash mkdir -p ~/bin/homebrew-tap/Formula cd ~/bin/homebrew-tap git init -q -b main git config user.email "seth@sethfreiberg.com" git config user.name "Seth Freiberg" ``` - [ ] **Step 2: Create `Formula/glabels-qt.rb`** ```bash cat > Formula/glabels-qt.rb <<'EOF' class GlabelsQt < Formula desc "gLabels Label Designer (Qt/C++) — Seth's packaging fork" homepage "https://glabels.org" url "https://git.sethpc.xyz/Seth/sethLabels.git", tag: "PLACEHOLDER_FILLED_AT_FIRST_RELEASE", revision: "PLACEHOLDER_FILLED_AT_FIRST_RELEASE" license "GPL-3.0-only" head "https://git.sethpc.xyz/Seth/sethLabels.git", branch: "main" depends_on "cmake" => :build depends_on "ninja" => :build depends_on "pkgconf" => :build depends_on "qt" depends_on "zlib" depends_on "qrencode" => :recommended # optional barcode backend depends_on "zint" => :recommended # optional barcode backend def install system "cmake", "-S", ".", "-B", "build", "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", *std_cmake_args system "cmake", "--build", "build" system "cmake", "--install", "build" end test do assert_match "gLabels", shell_output("#{bin}/glabels-batch-qt --version") end end EOF ``` The `tag:` and `revision:` placeholders are filled at the first release (Task 12 Step 6). - [ ] **Step 3: Create `README.md` for the tap** ```bash cat > README.md <<'EOF' # Seth's Homebrew tap Homebrew tap publishing macOS install for [sethLabels](https://git.sethpc.xyz/Seth/sethLabels). ## Install ``` brew tap seth/tap https://git.sethpc.xyz/Seth/homebrew-tap.git brew install seth/tap/glabels-qt ``` The explicit URL form is required because Homebrew defaults to GitHub for tap names. When this repo is mirrored to GitHub at the public-flip, the URL becomes implicit and the tap command shortens to `brew tap seth/tap`. ## Formulae | Formula | Description | |---------|-------------| | `glabels-qt` | [gLabels label designer (Qt/C++)](https://git.sethpc.xyz/Seth/sethLabels) — Seth's packaging fork of glabels-qt | ## How this works `brew install seth/tap/glabels-qt` clones the sethLabels git tag pinned in `Formula/glabels-qt.rb`, builds Qt6 + glabels-qt from source, and installs to `/opt/homebrew/`. First install takes ~5–10 minutes. Subsequent `brew upgrade glabels-qt` runs are fast (only the version-bumped formula re-builds). ## Per-release maintenance Each sethLabels release is one commit on this repo: bump `tag:` and `revision:` in `Formula/glabels-qt.rb`. No other edits expected. ## Spec Design rationale lives in the sethLabels repo: [`sethlabels-docs/specs/2026-04-29-packaging-design.md`](https://git.sethpc.xyz/Seth/sethLabels/src/branch/main/sethlabels-docs/specs/2026-04-29-packaging-design.md) §D2, §7. EOF ``` - [ ] **Step 4: Initial commit on local tap repo** ```bash cd ~/bin/homebrew-tap git add Formula/glabels-qt.rb README.md git commit -m "chore: scaffold tap with glabels-qt formula" ``` - [ ] **Step 5: Create Gitea remote and push** Use the `gitea` CLI per global instructions: ```bash cd ~/bin/homebrew-tap gitea create homebrew-tap --description "Homebrew tap publishing macOS install for sethLabels" gitea remote homebrew-tap gitea push ``` Expected: `gitea create` prints the new repo URL; `gitea remote` sets `origin`; `gitea push` pushes `main`. - [ ] **Step 6: Verify the tap can be browsed** ```bash curl -s https://git.sethpc.xyz/Seth/homebrew-tap/raw/branch/main/Formula/glabels-qt.rb | head -10 ``` Expected: prints the first 10 lines of the formula. - [ ] **Step 7: Return to sethLabels repo** ```bash cd ~/bin/sethLabels ``` The sethLabels repo has no commits in this task — the tap is a separate repo. The next task is the first end-to-end release of sethLabels itself. --- ## Task 12: First end-to-end release dry run (operator checklist) **Files modified:** - Modify: `~/bin/homebrew-tap/Formula/glabels-qt.rb` (Step 9 — replace placeholder tag and revision) - Modify: `packaging/changelog.md` (Step 5 — add the seth1 entry) **What it does:** Walks the spec §6 release flow end-to-end. Produces real artifacts, tags the sethLabels repo, attaches artifacts to a Gitea release, bumps the brew formula, and (optionally) verifies T5 install on a clean Debian 13 VM. - [ ] **Step 1: Refresh tags + rebase** ```bash cd ~/bin/sethLabels git fetch --all --tags git rebase upstream/master ``` Expected: rebase is conflict-free (strict-zero invariant). If it conflicts, STOP — something has slipped past the guardrail. - [ ] **Step 2: Run guardrail explicitly** ```bash ./scripts/check-no-upstream-edits.sh && echo CLEAN ``` Expected: `CLEAN`. - [ ] **Step 3: Build the .deb** ```bash ./scripts/build-deb.sh ``` Expected: ends with `Artifact: build/deb/glabels-qt__amd64.deb`. Note the version string for use in later steps. - [ ] **Step 4: Build the AppImages** ```bash ./scripts/build-appimages.sh ``` Expected: ends with two `Artifacts:` lines naming the GUI and batch AppImages. - [ ] **Step 5: Update `packaging/changelog.md`** Edit `packaging/changelog.md` to convert the `## (unreleased)` block to a real version block. Replace the existing `## (unreleased)` section with: ```markdown ## - First end-to-end release of sethLabels packaging pipeline. - `.deb` produced via CMake CPack with strict-zero `-D` overrides (no upstream edits). - AppImages (GUI + batch) bundled via linuxdeploy + linuxdeploy-plugin-qt, pinned per F9. - Brew tap initial publish at `git.sethpc.xyz/Seth/homebrew-tap`. ## (unreleased) - (no changes since ) ``` Substitute `` with the value from Step 3 and `` with today's ISO date. ```bash git add packaging/changelog.md git commit -m "docs: changelog for " ``` - [ ] **Step 6: Compute and tag** ```bash VERSION=$(./scripts/compute-version.sh) echo "Tagging $VERSION" git tag "$VERSION" git push origin main --tags ``` Expected: tag pushes successfully. If the `seth` count is unexpectedly high, you forgot the `git fetch --all --tags` in Step 1 or there are leftover tags from local experiments. - [ ] **Step 7: Create Gitea release with artifacts attached** Use the Gitea API per `~/bin/GITEA_API.md`: ```bash DEB=$(ls build/deb/glabels-qt_*.deb | head -1) GUI=$(ls sethlabels-gui-*.AppImage | head -1) BATCH=$(ls sethlabels-batch-*.AppImage | head -1) # Read token from the standard location. GITEA_TOKEN=$(cat ~/.config/gitea/token) GITEA_BASE="https://git.sethpc.xyz/api/v1" # Create the release. RELEASE_JSON=$(curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"tag_name\":\"$VERSION\",\"name\":\"sethLabels $VERSION\",\"body\":\"See packaging/changelog.md for notes.\"}" \ "$GITEA_BASE/repos/Seth/sethLabels/releases") RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") echo "Created release ID $RELEASE_ID" # Attach all three artifacts. for f in "$DEB" "$GUI" "$BATCH"; do echo "Attaching $f" curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \ -F "attachment=@${f}" \ "$GITEA_BASE/repos/Seth/sethLabels/releases/$RELEASE_ID/assets?name=$(basename "$f")" \ >/dev/null done echo "Release URL: https://git.sethpc.xyz/Seth/sethLabels/releases/tag/$VERSION" ``` Expected: prints the release URL; opening it shows the three attachments. - [ ] **Step 8: Verify download URLs are public** ```bash curl -sI "https://git.sethpc.xyz/Seth/sethLabels/releases/download/$VERSION/$(basename "$DEB")" | head -1 ``` Expected: `HTTP/2 200` or `HTTP/2 302` (redirect to the asset). - [ ] **Step 9: Bump `homebrew-tap/Formula/glabels-qt.rb`** ```bash cd ~/bin/homebrew-tap TAG="$VERSION" REVISION=$(cd ~/bin/sethLabels && git rev-list -n 1 "$VERSION") echo "Pinning tag=$TAG revision=$REVISION" # Replace placeholders. sed -i "s|tag: \"PLACEHOLDER_FILLED_AT_FIRST_RELEASE\"|tag: \"$TAG\"|" Formula/glabels-qt.rb sed -i "s|revision: \"PLACEHOLDER_FILLED_AT_FIRST_RELEASE\"|revision: \"$REVISION\"|" Formula/glabels-qt.rb # Verify the file is well-formed Ruby (parse-only check; doesn't run Homebrew). ruby -c Formula/glabels-qt.rb git diff Formula/glabels-qt.rb git add Formula/glabels-qt.rb git commit -m "bump glabels-qt to $TAG" git push origin main ``` Expected: `ruby -c` reports `Syntax OK`; `git push` succeeds. - [ ] **Step 10: Optional — T5 fresh-VM smoke test** Per spec §10, T5 is "install on a clean Debian 13 VM and run `glabels-qt --version`". This is the strongest signal that `dpkg-shlibdeps` produced a correct depends list. Recommended on every `seth1` release; skippable on `seth2`+ packaging-only fixes. If you have a clean Debian 13 VM available: ``` # On the clean VM: wget https://git.sethpc.xyz/Seth/sethLabels/releases/download//glabels-qt__amd64.deb sudo apt install -y ./glabels-qt__amd64.deb glabels-qt --version ``` Expected: install succeeds; `--version` exits 0 and prints the version string. If `apt install` errors with unmet deps, the `dpkg-shlibdeps` calculation was wrong (spec §F8). Mitigation: override via `CPACK_DEBIAN_PACKAGE_DEPENDS` in `scripts/build-deb.sh`, rebuild, re-tag as `seth2`. - [ ] **Step 11: Update CLAUDE.md to reflect post-implementation state** ```bash cd ~/bin/sethLabels ``` Open `CLAUDE.md`, find the `## Current State` block, and replace its content with: ```markdown ## Current State - **Phase:** post-first-release. Pipeline live. First tag: . Three artifacts attached to the Gitea release. Brew tap bumped to match. - **Repo:** `git.sethpc.xyz/Seth/sethLabels` (default branch `main`). Tap: `git.sethpc.xyz/Seth/homebrew-tap`. Upstream: `j-evins/glabels-qt` (`upstream` remote). - **Deploy targets live:** Debian-family Linux (`.deb` + AppImage) and macOS via Homebrew tap. - **Next release:** rebase, build, tag, attach, bump tap. See `scripts/README.md` and spec §6. ``` ```bash git add CLAUDE.md git commit -m "docs: refresh CLAUDE.md to post-first-release phase" git push origin main ``` - [ ] **Step 12: Write a session handoff** Per Seth's global persistence convention, create a handoff document capturing the session's outcome: ```bash # Use the session-handoff skill to create the handoff document with proper structure. # (See ~/.claude/CLAUDE.md → Persistence Partition → Session close). ``` The handoff filename pattern is `.claude/handoffs/YYYY-MM-DD-HHMMSS-first-release.md`. --- ## Self-review checklist After all 12 tasks complete, the implementer should verify: - [ ] **Spec coverage:** every section of `sethlabels-docs/specs/2026-04-29-packaging-design.md` maps to a task — §2 (invariants enforced by Task 3); §3 (decisions reflected throughout); §4 (file structure produced); §5.1–5.5 (Tasks 4–7 in order); §6 (Task 12); §7 (Task 11); §F1–F9 (each guarded somewhere — F1=Task 3, F2=Task 8 inline T4, F3=Task 4, F4=Task 11, F5=Task 2, F6=Task 9, F7=Task 12 step 1 rebase verification, F8=Task 12 step 10 T5, F9=Task 5 explicit pinning); §10 (T1–T4 inline; T5 in Task 12 step 10). - [ ] **No upstream files modified:** `git diff --name-only upstream/master..HEAD` shows only allowlisted paths. - [ ] **All bats tests pass:** `./tests-impl/run-all.sh` reports green. - [ ] **The .deb installs cleanly** on a fresh Debian 13 VM and `glabels-qt --version` exits 0. - [ ] **Both AppImages run** under `APPIMAGE_EXTRACT_AND_RUN=1` (no FUSE dependency). - [ ] **Brew formula parses:** `ruby -c Formula/glabels-qt.rb` reports `Syntax OK`. (Live `brew install` test on a Mac is recommended but not gate-blocking.) - [ ] **Release flow steps 1–9 of spec §6 ran cleanly** with no manual deviations.