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.