From e025a7441afae3f982c8a7451c5a9131c1198f4a Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Thu, 27 Aug 2026 11:10:55 -0700 Subject: [PATCH 1/2] 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. -- 2.52.0 From b3d07bda0955d6589365f253843a558e470b7a64 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Thu, 27 Aug 2026 11:24:31 -0700 Subject: [PATCH 2/2] Fix real workflow bugs a review found: dead bind mount, blind error gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A review found the "Validate with makepkg and namcap" step's bind mount (docker run -v "$PWD/rendered:/work") would very likely fail on Gitea's own act_runner: a containerized job's $PWD isn't a path the daemon's host can resolve, so the mount would silently attach an empty directory instead of failing loudly — the same class of problem noted elsewhere for this exact environment. Switched to docker create + docker cp (in and back out) + docker start -a, the pattern already validated locally, which works regardless of where the daemon actually lives. Also found and fixed, most severe first: - The namcap error gate (`grep -q "^[a-zA-Z0-9_-]*bin E:"`) only matched one of namcap's two line shapes for reporting an error ("triple-c-bin E: ...") and missed the other ("PKGBUILD (triple-c-bin) E: ...") entirely — confirmed by reproducing both against a real namcap run. The PKGBUILD-level half of the safety net was dead. Replaced with a plain `grep -q " E: "`, confirmed to match both real shapes (and a split-package variant) and nothing else. - package()'s `ar x "Triple-C_${pkgver}_amd64.deb"` named the asset literally, defeating the whole point of the resolve step discovering the real filename from the release instead of assuming a pattern — a future Tauri bundler naming change would still break here with an opaque error. Changed to `ar x ./*_amd64.deb`, which `source=()` already guarantees matches exactly one file. - `pacman -Sy` before installing packages is the canonical Arch partial- upgrade footgun; changed to `pacman -Syu --noconfirm --needed`. - `${{ inputs.version }}` was interpolated directly into a shell step instead of routed through `env:`, unlike every other step in the file. - `git push origin master` assumes the local branch name after cloning a brand-new (not-yet-created) AUR repo's empty state is `master`, which depends on the runner's own `init.defaultBranch` if the server sends no symref. `git push origin HEAD:master` is unambiguous either way. - The private key was written with a plain redirect then chmod'd after, leaving a window where it's world-readable; now created at its final mode first via `install -m 600 /dev/null`. Added `-o IdentitiesOnly=yes` so a runner ssh-agent can't offer a different key first. - Added GH_PAT auth to the api.github.com calls, matching every other workflow in this repo, to avoid the unauthenticated 60/hour rate limit. - Fixed two comments: the `options` comment credited `!debug` for suppressing the empty debug-package directory, when it's actually `!strip` doing that (verified in a real build); and documented in the README that a hand-edit made directly in the AUR repo is silently reverted by the next dispatch, since every run renders fresh from this repo's template. All of the above re-verified with the same real end-to-end methodology as the original commit: real makepkg build, real namcap lint (clean), and the exact updated docker create/cp/start sequence run against a live container. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ --- .gitea/workflows/publish-aur-package.yml | 102 ++++++++++++++++++----- packaging/arch/PKGBUILD | 23 +++-- packaging/arch/README.md | 7 ++ 3 files changed, 104 insertions(+), 28 deletions(-) diff --git a/.gitea/workflows/publish-aur-package.yml b/.gitea/workflows/publish-aur-package.yml index 6d655aa..0ac5951 100644 --- a/.gitea/workflows/publish-aur-package.yml +++ b/.gitea/workflows/publish-aur-package.yml @@ -19,7 +19,10 @@ name: Publish AUR Package # 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. +# PKGBUILD's revision history is expected to live. A corollary worth knowing: +# a hand-edit made directly in the AUR repo (outside this workflow) is +# silently overwritten the next time this runs, since every run renders fresh +# from this repo's template rather than starting from AUR's current state. # # ## Required secret # @@ -51,16 +54,26 @@ jobs: - name: Resolve version and find the Linux asset id: resolve + env: + VERSION_INPUT: ${{ inputs.version }} + GH_PAT: ${{ secrets.GH_PAT }} run: | set -euo pipefail - VERSION="${{ inputs.version }}" - if [ -z "$VERSION" ]; then + # Authenticated when the secret is available (it is, everywhere + # else in this repo's workflows) to avoid the unauthenticated + # 60-requests/hour-per-IP cap; still works without it, just at that + # lower limit, since this hits nothing but a public repo's public + # releases. + AUTH=() + [ -n "${GH_PAT}" ] && AUTH=(-H "Authorization: Bearer ${GH_PAT}") + + if [ -z "${VERSION_INPUT}" ]; then echo "No version given — resolving the latest GitHub release" - RELEASE_JSON=$(curl -fsS "https://api.github.com/repos/${GITHUB_REPO}/releases/latest") + RELEASE_JSON=$(curl -fsS "${AUTH[@]}" "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}") + echo "Using requested version ${VERSION_INPUT}" + RELEASE_JSON=$(curl -fsS "${AUTH[@]}" "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/v${VERSION_INPUT}") fi TAG=$(echo "$RELEASE_JSON" | jq -r '.tag_name') @@ -72,9 +85,12 @@ jobs: # 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') + # forever until someone noticed. `head -1` guards against a release + # somehow carrying more than one matching asset, which would + # otherwise pass the emptiness check below and then break the + # download step with two URLs on one line. + DEB_URL=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | endswith("_amd64.deb")) | .browser_download_url' | head -1) + DEB_NAME=$(echo "$RELEASE_JSON" | jq -r '.assets[] | select(.name | endswith("_amd64.deb")) | .name' | head -1) if [ -z "$DEB_URL" ] || [ "$DEB_URL" = "null" ]; then echo "No *_amd64.deb asset found on release ${TAG}" >&2 exit 1 @@ -119,19 +135,22 @@ jobs: # 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 + # point: every substitution below fails loudly (an assertion / + # the checks 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' + # correct for what this workflow does. It is NOT correct for a + # dependency-only fix republished at the *same* pkgver: pkgrel + # would be forced back to 1, and no existing installation sees an + # upgrade. That case needs a manual pkgrel bump in the template + # before dispatching, which this workflow has no input for. + python3 - "$VERSION" "$DEB_NAME" "$DEB_SHA256" "$LICENSE_SHA256" "$GITHUB_REPO" <<'PY' import re, sys - version, deb_name, deb_sha, license_sha = sys.argv[1:5] - github_repo = "shadowdao/triple-c" + version, deb_name, deb_sha, license_sha, github_repo = sys.argv[1:6] with open("PKGBUILD") as f: text = f.read() @@ -167,9 +186,20 @@ jobs: - name: Validate with makepkg and namcap run: | set -euo pipefail - docker run --rm -v "$PWD/rendered:/work" -w /work archlinux:latest bash -c ' + + # A bind mount (`docker run -v "$PWD/...":/work`) is the more + # obvious way to write this, and was the first draft — but on a + # containerized Gitea act_runner job, `$PWD` is a path inside this + # job's own container, which the daemon's host cannot resolve; the + # mount would silently attach an empty directory instead of failing + # loudly. `docker cp` moves real bytes across that boundary + # regardless of where the daemon actually lives, which is what + # makes this work under both a bind-mount-capable runner and a + # containerized one. + docker pull archlinux:latest + CID=$(docker create -w /work archlinux:latest bash -c ' set -euo pipefail - pacman -Sy --noconfirm base-devel namcap sudo git openssh >/dev/null + pacman -Syu --noconfirm --needed 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 @@ -178,11 +208,24 @@ jobs: 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 + # Matches "triple-c-bin E:", "PKGBUILD (triple-c-bin) E:" and any + # split-package variant ("triple-c-bin-debug E:") alike — namcap + # uses more than one line shape for its two rule families, and + # namcap itself exits 0 regardless of what it reports, so this + # grep is the only thing standing between an E: and a green job. + if echo "$NAMCAP_OUT" | grep -q " E: "; then echo "namcap reported an error — see above" >&2 exit 1 fi - ' + ') + mkdir -p rendered + docker cp rendered/. "${CID}:/work" + # `docker start -a` streams output and its exit code is the + # container's own — the same failure this would have hit with a + # bind mount still fails the job the same way. + docker start -a "${CID}" + docker cp "${CID}:/work/.SRCINFO" rendered/.SRCINFO + docker rm -f "${CID}" >/dev/null - name: Push to AUR env: @@ -197,10 +240,19 @@ jobs: fi mkdir -p ~/.ssh + # Created with the final mode before any bytes land in it, rather + # than a plain redirect followed by chmod, which leaves the key + # world-readable for whatever window falls between the two calls. + install -m 600 /dev/null ~/.ssh/aur echo "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur - chmod 600 ~/.ssh/aur + # TOFU, not verification — accepted here because pinning AUR's + # actual host key needs a value fetched from somewhere trusted + # ahead of time, which this workflow doesn't have, and getting a + # pinned value wrong fails every future run rather than just this + # one. A keyscan failure below surfaces later as an opaque + # "Host key verification failed" rather than a clear one here. 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" + export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur -o IdentitiesOnly=yes -o UserKnownHostsFile=~/.ssh/known_hosts" git clone "${AUR_REPO}" aur-repo cp rendered/PKGBUILD rendered/.SRCINFO aur-repo/ @@ -213,4 +265,10 @@ jobs: exit 0 fi git commit -m "triple-c-bin: update to ${VERSION}" - git push origin master + # AUR itself uses `master`, which is what a fresh, not-yet-created + # AUR package's empty repo advertises on clone — but the *local* + # branch name after cloning an empty repo falls back to whatever + # this runner's `init.defaultBranch` is if the server sends no + # symref, so naming the destination explicitly is what keeps this + # working if that default is ever `main` instead of `master`. + git push origin HEAD:master diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 5643186..fe6d2dc 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -41,11 +41,15 @@ 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). +# no benefit. It's also what actually suppresses makepkg's debug-package +# machinery here (debug-package extraction requires strip; verified in a +# real build — with !strip alone, no debug package is produced at all). +# !debug is kept anyway, explicit about intent rather than relying on that +# side effect. Without either, makepkg built a usr/src/debug/triple-c-bin +# tree containing a dangling .build-id symlink, which is a real namcap +# error (not just the empty-directory warning it looks like) — there is no +# debug info in this release binary for the machinery to have extracted in +# the first place. options=('!strip' '!debug') # Tauri names the asset after `productName` verbatim ("Triple-C"), not the # lowercase Cargo binary name — verified against the real release, not @@ -68,7 +72,14 @@ package() { # 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" + # + # Globbed rather than named literally: the publish workflow discovers the + # real asset name from the release itself specifically so a Tauri bundler + # naming change can't silently break this — naming the file again here + # would throw that away and fail this one line with an opaque "No such + # file or directory" instead. `source=()` above guarantees exactly one + # `*_amd64.deb` entry, so the glob can only ever match that one file. + ar x ./*_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 index e9ff9b2..6c7a47a 100644 --- a/packaging/arch/README.md +++ b/packaging/arch/README.md @@ -44,3 +44,10 @@ 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. + +**A hand-edit made directly in the AUR repo is silently overwritten the +next time this workflow runs.** Every run renders fresh from *this* +repo's template rather than starting from whatever AUR's copy currently +looks like, so a quick fix pushed straight to AUR (bumping `pkgrel` for a +packaging-only issue, say) survives only until the next dispatch. Make +the fix here instead. -- 2.52.0