From 0149b24a33859ca044c06fd258744ae3ee0e1282 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Wed, 16 Sep 2026 08:52:09 -0400 Subject: [PATCH] ci(release): publish signed updater assets alongside tarball After packaging, stage the bare AniTrack-linux-amd64 binary (the updater swaps os.Executable in place; the user tarball single top-level directory would fail the single-entry rule), sign it with wails3 updater sign using UPDATER_SIGNING_KEY (missing secret is a hard error, key shredded after use), split the output into .sha512/.sig sidecars, and extend the idempotent replace-upload loop from 1 asset to all 4. Tarball flow untouched; rc tags get signed assets too, which the beta-channel live proof needs. --- .gitea/workflows/release.yml | 78 ++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 92f3b60..302a0e1 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -245,7 +245,38 @@ jobs: echo "Created new release for ${TAG}" fi - - name: Upload release archive + - name: Build updater asset and sign + env: + UPDATER_SIGNING_KEY: ${{ secrets.UPDATER_SIGNING_KEY }} + run: | + set -euo pipefail + : "${VERSION:?VERSION missing from version-guard step}" + export PATH="${HOME}/go/bin:${PATH}" + : "${UPDATER_SIGNING_KEY:?Missing secret UPDATER_SIGNING_KEY. Add the updater private key under Settings → Secrets → Actions.}" + # Bare updater binary: the updater swaps os.Executable() in place, + # so it takes the single binary, not the user tarball (whose + # top-level directory would fail the single-entry rule). + mkdir -p updater-dist + cp build/bin/AniTrack updater-dist/AniTrack-linux-amd64 + printf '%s' "${UPDATER_SIGNING_KEY}" > updater-dist/updater.key + chmod 600 updater-dist/updater.key + wails3 updater sign -key updater-dist/updater.key updater-dist/AniTrack-linux-amd64 > updater-dist/sign.json + # Split the sign output into the sidecar assets the Gitea provider + # fetches: .sha512 (sha512sum format) + .sig (base64). + python3 - <<'EOF' + import base64, binascii, json + entries = json.load(open('updater-dist/sign.json')) + entry = next(e for e in entries if e['filename'].endswith('AniTrack-linux-amd64')) + assert entry['digestAlgo'] == 'sha512', entry + assert entry['signatureAlgo'] == 'ed25519ph', entry + digest_hex = binascii.hexlify(base64.b64decode(entry['digest'])).decode() + open('updater-dist/AniTrack-linux-amd64.sha512', 'w').write(f"{digest_hex} AniTrack-linux-amd64\n") + open('updater-dist/AniTrack-linux-amd64.sig', 'w').write(entry['signature'].strip() + '\n') + EOF + shred -u updater-dist/updater.key + ls -la updater-dist/ + + - name: Upload release assets env: TOKEN: ${{ secrets.REGISTRY_TOKEN }} REPO: ${{ gitea.repository }} @@ -255,27 +286,30 @@ jobs: : "${ARCHIVE:?ARCHIVE missing from packaging step}" API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases" AUTH="Authorization: token ${TOKEN}" - test -f "${ARCHIVE}" || { echo "::error::${ARCHIVE} not found" >&2; exit 1; } + for ASSET in "${ARCHIVE}" updater-dist/AniTrack-linux-amd64 updater-dist/AniTrack-linux-amd64.sha512 updater-dist/AniTrack-linux-amd64.sig; do + test -f "${ASSET}" || { echo "::error::${ASSET} not found" >&2; exit 1; } - RID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty')" - if [ -z "${RID}" ]; then - echo "::error::No release found for tag ${TAG} after create step" >&2 - exit 1 - fi + RID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty')" + if [ -z "${RID}" ]; then + echo "::error::No release found for tag ${TAG} after create step" >&2 + exit 1 + fi - # Replace a same-named asset so re-runs stay idempotent. - AID="$(curl -sS -H "${AUTH}" "${API}/${RID}/assets" | jq -r --arg n "${ARCHIVE}" '.[] | select(.name==$n) | .id // empty')" - if [ -n "${AID}" ]; then - curl -sS -X DELETE -H "${AUTH}" "${API}/${RID}/assets/${AID}" >/dev/null - echo "Deleted existing asset id=${AID} (${ARCHIVE})" - fi + # Replace a same-named asset so re-runs stay idempotent. + ANAME="$(basename "${ASSET}")" + AID="$(curl -sS -H "${AUTH}" "${API}/${RID}/assets" | jq -r --arg n "${ANAME}" '.[] | select(.name==$n) | .id // empty')" + if [ -n "${AID}" ]; then + curl -sS -X DELETE -H "${AUTH}" "${API}/${RID}/assets/${AID}" >/dev/null + echo "Deleted existing asset id=${AID} (${ANAME})" + fi - resp="$(curl -sS -w '\n%{http_code}' -X POST -H "${AUTH}" \ - -F "attachment=@${ARCHIVE}" "${API}/${RID}/assets?name=${ARCHIVE}")" - code="$(printf '%s' "${resp}" | tail -n1)" - rbody="$(printf '%s' "${resp}" | sed '$d')" - if [ "${code}" -ge 400 ]; then - echo "::error::Asset upload ${code}: ${rbody}" >&2 - exit 1 - fi - echo "Uploaded ${ARCHIVE} to release id=${RID}" + resp="$(curl -sS -w '\n%{http_code}' -X POST -H "${AUTH}" \ + -F "attachment=@${ASSET}" "${API}/${RID}/assets?name=${ANAME}")" + code="$(printf '%s' "${resp}" | tail -n1)" + rbody="$(printf '%s' "${resp}" | sed '$d')" + if [ "${code}" -ge 400 ]; then + echo "::error::Asset upload ${code}: ${rbody}" >&2 + exit 1 + fi + echo "Uploaded ${ANAME} to release id=${RID}" + done