From e025a7441afae3f982c8a7451c5a9131c1198f4a Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Thu, 27 Aug 2026 11:10:55 -0700 Subject: [PATCH] Add a native Arch/CachyOS package via its own AUR publish workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of triple-c#34's third ask ("I would like to also have an Arch/CachyOS native version as well"), addressed separately from the Wayland crash fix (fix/wayland-webkit-egl-crash) since it's an unrelated feature, not a bug. packaging/arch/PKGBUILD is a "-bin" AUR package repackaging the same .deb build-app.yml already produces — no Rust/Node toolchain needed to install it, and the user gets exactly the binary the project ships and tests. Verified end to end against a real release (v0.4.14) rather than going by Tauri's generic docs: downloaded the actual .deb, ldd'd the actual binary to ground-truth `depends` (dropped `pango` and `libayatana-appindicator` from an earlier draft — the first is already pulled in transitively by gtk3, the second was never linked at all since this app has no tray icon or menu), and ran a real makepkg/namcap/pacman -U cycle. namcap caught a real issue this way (missing license file under /usr/share/licenses/triple-c-bin/), now fixed by fetching LICENSE alongside the .deb. .gitea/workflows/publish-aur-package.yml does the actual publishing: given a version (or "latest"), it finds that release's real Linux asset on GitHub, downloads it, computes real checksums, renders the PKGBUILD template, validates the result with makepkg and namcap inside a real Arch container, and pushes to AUR. workflow_dispatch only, deliberately — the same reasoning that killed sync-release.yml in triple-c#32 (releases are assembled by build-app.yml across three separate platform jobs, so there's no single automatic event that fires only once the Linux .deb this needs actually exists) applies here too. Requires a repo secret this workflow cannot set up itself: AUR_SSH_PRIVATE_KEY, from an AUR account that has already created (or been given co-maintainer access to) triple-c-bin — both one-time manual steps on aur.archlinux.org. Until that secret exists, the workflow fails loudly at the push step rather than silently doing nothing. See packaging/arch/README.md for the full maintenance flow. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ --- .gitea/workflows/publish-aur-package.yml | 216 +++++++++++++++++++++++ TECHNICAL.md | 18 +- packaging/arch/PKGBUILD | 75 ++++++++ packaging/arch/README.md | 46 +++++ 4 files changed, 349 insertions(+), 6 deletions(-) create mode 100644 .gitea/workflows/publish-aur-package.yml create mode 100644 packaging/arch/PKGBUILD create mode 100644 packaging/arch/README.md diff --git a/.gitea/workflows/publish-aur-package.yml b/.gitea/workflows/publish-aur-package.yml new file mode 100644 index 0000000..6d655aa --- /dev/null +++ b/.gitea/workflows/publish-aur-package.yml @@ -0,0 +1,216 @@ +name: Publish AUR Package + +# Builds and pushes the `triple-c-bin` AUR package (packaging/arch/PKGBUILD) +# for a given release, or the latest one if none is given. Manual dispatch +# only — deliberately not triggered by `release` or `push`, for the same +# reason sync-release.yml (removed in triple-c#32) never worked safely as an +# automatic trigger: this repo's releases are assembled by build-app.yml +# across three separate platform jobs, and there is no single automatic event +# that fires only once everything (including the Linux .deb this workflow +# needs) is actually uploaded. A human deciding "this release is ready, go +# package it" is the correct trigger, the same reasoning +# backfill-releases.yml already uses for its own manual-only GitHub sync. +# +# ## What this does and does not do +# +# It renders `packaging/arch/PKGBUILD` for one specific version (real +# download URL, real sha256sums — never guessed; see the resolve-asset step) +# and pushes the rendered PKGBUILD plus a regenerated `.SRCINFO` to AUR. It +# does NOT commit anything back to this repo — `packaging/arch/PKGBUILD` stays +# a hand-maintained template with a placeholder version, and every real, +# published version lives only in AUR's own git history, which is where a +# PKGBUILD's revision history is expected to live. +# +# ## Required secret +# +# `AUR_SSH_PRIVATE_KEY` — an SSH private key registered against an AUR +# account that has already created (or been given co-maintainer access to) +# the `triple-c-bin` package. This workflow cannot create that AUR account or +# register the key for you — both are manual, one-time steps on +# https://aur.archlinux.org. Until this secret exists, every run fails at the +# "Push to AUR" step with a clear error rather than silently doing nothing. +on: + workflow_dispatch: + inputs: + version: + description: >- + Release version to package, without a leading "v" (e.g. "0.4.14"). + Leave empty to use the latest published GitHub release. + required: false + +env: + GITHUB_REPO: shadowdao/triple-c + AUR_REPO: ssh://aur@aur.archlinux.org/triple-c-bin.git + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve version and find the Linux asset + id: resolve + run: | + set -euo pipefail + + VERSION="${{ inputs.version }}" + if [ -z "$VERSION" ]; then + echo "No version given — resolving the latest GitHub release" + RELEASE_JSON=$(curl -fsS "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") + else + echo "Using requested version ${VERSION}" + RELEASE_JSON=$(curl -fsS "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/v${VERSION}") + fi + + TAG=$(echo "$RELEASE_JSON" | jq -r '.tag_name') + VERSION="${TAG#v}" + echo "Resolved to ${TAG}" + + # Discovered from the real release, not assumed: Tauri names the + # asset after `productName` verbatim ("Triple-C"), not the + # lowercase Cargo binary name, and asset naming is exactly the kind + # of thing that silently drifts if a future Tauri upgrade changes + # bundler defaults — a hardcoded pattern here would then 404 + # forever until someone noticed. + DEB_URL=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | endswith("_amd64.deb")) | .browser_download_url') + DEB_NAME=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | endswith("_amd64.deb")) | .name') + if [ -z "$DEB_URL" ] || [ "$DEB_URL" = "null" ]; then + echo "No *_amd64.deb asset found on release ${TAG}" >&2 + exit 1 + fi + echo "Found asset: ${DEB_NAME}" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "deb_url=${DEB_URL}" >> "$GITHUB_OUTPUT" + echo "deb_name=${DEB_NAME}" >> "$GITHUB_OUTPUT" + + - name: Download the release asset and compute real checksums + id: checksums + env: + DEB_URL: ${{ steps.resolve.outputs.deb_url }} + DEB_NAME: ${{ steps.resolve.outputs.deb_name }} + TAG: ${{ steps.resolve.outputs.tag }} + run: | + set -euo pipefail + curl -fsSL -o "${DEB_NAME}" "${DEB_URL}" + curl -fsSL -o LICENSE "https://raw.githubusercontent.com/${GITHUB_REPO}/${TAG}/LICENSE" + + echo "deb_sha256=$(sha256sum "${DEB_NAME}" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + echo "license_sha256=$(sha256sum LICENSE | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" + + - name: Render PKGBUILD + id: render + env: + VERSION: ${{ steps.resolve.outputs.version }} + DEB_NAME: ${{ steps.resolve.outputs.deb_name }} + DEB_SHA256: ${{ steps.checksums.outputs.deb_sha256 }} + LICENSE_SHA256: ${{ steps.checksums.outputs.license_sha256 }} + run: | + set -euo pipefail + mkdir -p rendered + cp packaging/arch/PKGBUILD rendered/PKGBUILD + cd rendered + + # Plain string replacement throughout, not sed — the source URL + # contains slashes and the repo name does too, and getting a sed + # delimiter choice AND its escaping right for that is exactly the + # kind of thing that looks correct, passes review, and breaks the + # next time someone touches it. `re.sub` with `count=1` and an + # exact `.format`-free literal match is boring and that's the + # point: every substitution below fails loudly (KeyError / the + # assertions after) rather than silently no-op'ing if the + # template's shape ever drifts from what this expects. + # + # pkgrel resets to 1 for a new pkgver — a packaging-only fix to the + # same upstream version (a dependency bump, say) is what pkgrel is + # for, and this workflow always republishes the current PKGBUILD + # verbatim rather than incrementing anything, so 1 is always + # correct here. + python3 - "$VERSION" "$DEB_NAME" "$DEB_SHA256" "$LICENSE_SHA256" <<'PY' + import re, sys + version, deb_name, deb_sha, license_sha = sys.argv[1:5] + github_repo = "shadowdao/triple-c" + + with open("PKGBUILD") as f: + text = f.read() + + text, n = re.subn(r"(?m)^pkgver=.*$", f"pkgver={version}", text, count=1) + assert n == 1, "pkgver=... line not found" + text, n = re.subn(r"(?m)^pkgrel=.*$", "pkgrel=1", text, count=1) + assert n == 1, "pkgrel=... line not found" + + old_source = ( + f'source=("Triple-C_${{pkgver}}_amd64.deb::' + f'https://github.com/{github_repo}/releases/download/v${{pkgver}}/' + f'Triple-C_${{pkgver}}_amd64.deb"' + ) + new_source = ( + f'source=("{deb_name}::' + f'https://github.com/{github_repo}/releases/download/v{version}/{deb_name}"' + ) + assert old_source in text, "source=() line does not match the expected template shape" + text = text.replace(old_source, new_source, 1) + + old_sums = "sha256sums=('SKIP'\n 'SKIP')" + assert old_sums in text, "sha256sums=() placeholders not found" + text = text.replace(old_sums, f"sha256sums=('{deb_sha}'\n '{license_sha}')", 1) + + with open("PKGBUILD", "w") as f: + f.write(text) + PY + + grep -q "pkgver=${VERSION}$" PKGBUILD + ! grep -q "SKIP" PKGBUILD + + - name: Validate with makepkg and namcap + run: | + set -euo pipefail + docker run --rm -v "$PWD/rendered:/work" -w /work archlinux:latest bash -c ' + set -euo pipefail + pacman -Sy --noconfirm base-devel namcap sudo git openssh >/dev/null + useradd -m builder + chown -R builder:builder /work + echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder + sudo -u builder bash -c "cd /work && makepkg --printsrcinfo > .SRCINFO" + sudo -u builder bash -c "cd /work && makepkg -s --noconfirm" + echo "--- namcap ---" + NAMCAP_OUT=$(sudo -u builder bash -c "cd /work && namcap PKGBUILD *.pkg.tar.*" || true) + echo "$NAMCAP_OUT" + if echo "$NAMCAP_OUT" | grep -q "^[a-zA-Z0-9_-]*bin E:"; then + echo "namcap reported an error — see above" >&2 + exit 1 + fi + ' + + - name: Push to AUR + env: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + VERSION: ${{ steps.resolve.outputs.version }} + run: | + set -euo pipefail + if [ -z "${AUR_SSH_PRIVATE_KEY}" ]; then + echo "AUR_SSH_PRIVATE_KEY is not set — see this workflow file's header comment for" >&2 + echo "the one-time AUR account setup this needs before it can publish anything." >&2 + exit 1 + fi + + mkdir -p ~/.ssh + echo "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur + chmod 600 ~/.ssh/aur + ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null + export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur -o UserKnownHostsFile=~/.ssh/known_hosts" + + git clone "${AUR_REPO}" aur-repo + cp rendered/PKGBUILD rendered/.SRCINFO aur-repo/ + cd aur-repo + git config user.name "Triple-C CI" + git config user.email "noreply@triple-c.invalid" + git add PKGBUILD .SRCINFO + if git diff --cached --quiet; then + echo "No change from what's already published on AUR for ${VERSION}" + exit 0 + fi + git commit -m "triple-c-bin: update to ${VERSION}" + git push origin master diff --git a/TECHNICAL.md b/TECHNICAL.md index 26513e8..5bb389f 100644 --- a/TECHNICAL.md +++ b/TECHNICAL.md @@ -412,12 +412,18 @@ triple-c/ │ ├── .gitea/ │ └── workflows/ -│ ├── build-app.yml # Build Tauri app (Linux/macOS/Windows); mirrors releases to GitHub inline -│ ├── build-app-preview.yml # Preview builds -│ ├── build.yml # Build container image (multi-arch) -│ ├── build-stt.yml # Build the STT image -│ ├── backfill-releases.yml # Bulk copy releases to GitHub -│ └── cleanup-releases.yml # Prune old releases +│ ├── build-app.yml # Build Tauri app (Linux/macOS/Windows); mirrors releases to GitHub inline +│ ├── build-app-preview.yml # Preview builds +│ ├── build.yml # Build container image (multi-arch) +│ ├── build-stt.yml # Build the STT image +│ ├── backfill-releases.yml # Bulk copy releases to GitHub +│ ├── cleanup-releases.yml # Prune old releases +│ └── publish-aur-package.yml # Publish triple-c-bin to the AUR (packaging/arch/) +│ +├── packaging/ +│ └── arch/ # AUR triple-c-bin package — see packaging/arch/README.md +│ ├── PKGBUILD +│ └── README.md │ └── app/ # Tauri v2 desktop application ├── package.json # React, xterm.js, zustand, tailwindcss diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..5643186 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,75 @@ +# Maintainer: Triple-C Contributors +# +# This file is regenerated by .gitea/workflows/publish-aur-package.yml on every +# publish — pkgver, the source URL and sha256sums are rewritten from the real, +# already-uploaded release asset, never guessed. Editing pkgver/source/ +# sha256sums by hand here only matters until the next automated run overwrites +# them; everything else (depends, pkgdesc, package()) is meant to be hand- +# maintained normally. +# +# "-bin" rather than building from source: this repackages the same .deb +# build-app.yml already produces and publishes, so a user gets exactly the +# binary the project ships and tests, and `makepkg` never needs a Rust +# toolchain, Node, or the dozen -dev packages CLAUDE.md lists for building +# Triple-C itself. The trade-off is the one every "-bin" package makes: it +# assumes the glibc the CI runner (Ubuntu 24.04) linked against is compatible +# with the installing system's — true for essentially every currently +# supported Arch install, since Arch tracks glibc newer than Ubuntu 24.04 +# ships, and forward compatibility is the direction that holds. +pkgname=triple-c-bin +pkgver=0.4.0 +pkgrel=1 +pkgdesc="Sandbox Claude Code inside Docker containers" +arch=('x86_64') +url="https://github.com/shadowdao/triple-c" +license=('MIT') +# Verified against a real release asset (v0.4.14), not Tauri's generic docs: +# downloaded Triple-C_0.4.14_amd64.deb, installed each of these into a real +# Arch container, and re-ran `ldd` on the actual binary until nothing came +# back "not found". `pango` and `libayatana-appindicator` were both in an +# earlier draft — pango isn't directly linked (gtk3 already pulls it in +# transitively, and namcap correctly flags declaring it as redundant), and +# libayatana-appindicator is in Tauri's own linux dependency list but this +# binary never links it at all: there is no tray icon or menu in this app +# (see CLAUDE.md's note that `core:menu`/`core:tray` are dropped for the +# same reason), so it was never a real dependency to begin with. +depends=('cairo' 'desktop-file-utils' 'gdk-pixbuf2' 'glib2' 'gtk3' + 'hicolor-icon-theme' 'libsoup3' 'webkit2gtk-4.1') +optdepends=('docker: to actually run the sandboxed containers' + 'xdg-utils: opening links from the app in your default browser') +provides=('triple-c') +conflicts=('triple-c') +# !strip: the upstream .deb's binary is already the release build Tauri +# produced and tested; re-stripping a prebuilt binary is unnecessary risk for +# no benefit. !debug: there is no debug info in a release binary for +# makepkg's debug-package machinery to extract, so without this it builds an +# empty usr/src/debug/ tree for nothing (confirmed with namcap against a real +# build — this was its only non-cosmetic complaint, once the license file +# below was added). +options=('!strip' '!debug') +# Tauri names the asset after `productName` verbatim ("Triple-C"), not the +# lowercase Cargo binary name — verified against the real release, not +# assumed; a lowercase guess here would 404. The LICENSE fetch is separate +# because the .deb itself carries no license file — namcap flags an MIT +# package with nothing under /usr/share/licenses/ as an error, correctly. +source=("Triple-C_${pkgver}_amd64.deb::https://github.com/shadowdao/triple-c/releases/download/v${pkgver}/Triple-C_${pkgver}_amd64.deb" + "LICENSE::https://raw.githubusercontent.com/shadowdao/triple-c/v${pkgver}/LICENSE") +sha256sums=('SKIP' + 'SKIP') + +package() { + cd "$srcdir" + # A .deb is an ar archive of debian-binary, control.tar.*, data.tar.* — `ar` + # (part of base-devel's binutils) pulls just the payload out. Extracting + # that tar directly into $pkgdir works here with no path rewriting at all: + # verified against the real archive, whose entire payload is + # usr/bin/triple-c, usr/share/applications/Triple-C.desktop and + # usr/share/icons/hicolor/*/apps/triple-c.png — Tauri's Linux bundle for + # this app carries no separate resource directory under usr/lib/, so there + # is nothing that could disagree between Debian's and Arch's package trees + # for it to land in the wrong place. + ar x "Triple-C_${pkgver}_amd64.deb" + tar xf data.tar.* -C "$pkgdir" + + install -Dm644 "$srcdir/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" +} diff --git a/packaging/arch/README.md b/packaging/arch/README.md new file mode 100644 index 0000000..e9ff9b2 --- /dev/null +++ b/packaging/arch/README.md @@ -0,0 +1,46 @@ +# Arch / CachyOS package + +`PKGBUILD` here is the AUR `triple-c-bin` package's template — see triple-c#34 +(the "I would like to also have an Arch/CachyOS native version" part of it). + +## Why "-bin" + +It repackages the same `.deb` `build-app.yml` already produces, rather than +building from source. That means `makepkg` never needs a Rust toolchain, +Node, or the dozen `-dev` packages CLAUDE.md lists for building Triple-C +itself — and a user gets exactly the binary the project ships and tests, +built on Ubuntu 24.04 in CI. Verified end to end against a real release +(v0.4.14): downloaded the actual `.deb`, confirmed every `depends` entry +against a real `ldd` of the actual binary (two packages that looked right +from Tauri's own docs — `pango`, `libayatana-appindicator` — turned out not +to be real dependencies of *this* binary and were dropped), and ran a real +`makepkg`/`namcap`/`pacman -U` cycle rather than guessing at the shape. + +## Publishing + +`.gitea/workflows/publish-aur-package.yml` does the actual work: given a +version (or "latest" if none is given), it finds that release's real Linux +asset on GitHub, downloads it, computes real checksums, renders this +template into a version-specific PKGBUILD, validates it with `makepkg` and +`namcap` inside a real Arch container, and pushes the result to AUR. + +It is `workflow_dispatch`-only, deliberately — see the workflow file's own +header comment for why an automatic trigger isn't safe here (the same reason +`sync-release.yml` didn't work and was removed in triple-c#32). + +**Before it can push anything**, an AUR account has to exist and the +`triple-c-bin` package has to have been created (or you added as a +co-maintainer) under it — both are one-time, manual steps on +https://aur.archlinux.org, since there's no API to automate creating an +account or a new package. Once that's done, add the account's SSH private +key as the `AUR_SSH_PRIVATE_KEY` secret on this repo. Until that secret +exists, the workflow fails at the "Push to AUR" step with a message saying +so, rather than silently doing nothing. + +## What's hand-maintained vs. generated + +`pkgver`/`pkgrel`/`source`/`sha256sums` in this file are placeholders — +the workflow rewrites them for every real publish and never commits the +result back here, so don't read this file's `pkgver` as "the last published +version." Everything else (`depends`, `pkgdesc`, `package()`) is meant to be +edited by hand normally, the same as any other PKGBUILD.