From d561ce03d5879b2686f85d67c77afb86c373220b Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Thu, 3 Sep 2026 08:29:58 -0700 Subject: [PATCH] Anchor the update channel tag, and stop shipping a duplicate AppImage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects in the update channel, both visible in 0.4.20 and 0.4.21. **The channel tag does not survive.** `publish-update-channel.sh` created the GitHub release, uploaded both assets and verified each URL returned 200 — the job log shows it succeeding at 00:38. By 13:04 the tag was gone and every installed copy was checking a 404. Gitea push-mirrors this repo to GitHub every four hours, and a mirror push deletes remote refs with no local counterpart. `linux-latest` was created by GitHub's release API and never existed as a Gitea tag, so the mirror removed it. Versioned tags were never affected because `create-tag` creates them in Gitea first. So the tag is now anchored in Gitea, and before the GitHub release rather than after, so there is no window where the two disagree. Its absence fails the step instead of warning, because it is the only thing keeping the channel alive. Worth stating plainly: publishing correctly is not evidence the channel still works, and the verification that passed at 00:38 could not have caught a failure that arrives twelve hours later. **Every release carried the AppImage twice.** The channel's stable-named copy sat beside the versioned one, where the release job's `*.AppImage` glob picked it up — so v0.4.21 published `Triple-C_0.4.21_amd64.AppImage` and `Triple-C_x86_64.AppImage`, byte-identical at 86,686,200 bytes each, and `sync-to-github` copied both to the mirror. 80 MB of duplicate per release, under a name that reads like a different build. That is how it was noticed. The channel pair now lives in `bundle/appimage/update-channel/`, out of the glob's reach, and a guard fails the build if more than one AppImage is left beside the release. Verified by planting a second one: it fails. One appimagetool quirk found while moving it — zsyncmake writes the .zsync into the working directory, not beside the image it describes, so it has to be collected rather than assumed in place. The existing guard caught that too. Verified against the real 0.4.19 artifact: exactly one AppImage at top level, the channel pair in its own directory, update string still resolving to the fixed tag, and the wayland fallback intact. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm --- .gitea/workflows/build-app-preview.yml | 1 - .gitea/workflows/build-app.yml | 10 +++++-- scripts/finalize-appimage.sh | 35 ++++++++++++++++------- scripts/publish-update-channel.sh | 39 ++++++++++++++++++++++++-- 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/build-app-preview.yml b/.gitea/workflows/build-app-preview.yml index 371d8a5..5c5c432 100644 --- a/.gitea/workflows/build-app-preview.yml +++ b/.gitea/workflows/build-app-preview.yml @@ -335,7 +335,6 @@ jobs: run: | mkdir -p artifacts cp app/src-tauri/target/release/bundle/appimage/*.AppImage artifacts/ 2>/dev/null || true - cp app/src-tauri/target/release/bundle/appimage/*.zsync artifacts/ 2>/dev/null || true ls -la artifacts/ # Assets, not workflow artifacts — see the note at the top of this file. diff --git a/.gitea/workflows/build-app.yml b/.gitea/workflows/build-app.yml index 0d4aa02..224b63b 100644 --- a/.gitea/workflows/build-app.yml +++ b/.gitea/workflows/build-app.yml @@ -200,8 +200,10 @@ jobs: - name: Collect artifacts run: | mkdir -p artifacts + # The versioned AppImage only. The update channel's copy lives in + # bundle/appimage/update-channel/ precisely so this glob cannot pick + # it up and publish an 80 MB duplicate under a second name. cp app/src-tauri/target/release/bundle/appimage/*.AppImage artifacts/ 2>/dev/null || true - cp app/src-tauri/target/release/bundle/appimage/*.zsync artifacts/ 2>/dev/null || true ls -la artifacts/ - name: Upload to Gitea release @@ -286,7 +288,11 @@ jobs: if: gitea.event_name == 'push' env: GH_PAT: ${{ secrets.GH_PAT }} - run: bash scripts/publish-update-channel.sh artifacts + GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + GITEA_SHA: ${{ gitea.sha }} + run: | + bash scripts/publish-update-channel.sh \ + app/src-tauri/target/release/bundle/appimage/update-channel build-macos: runs-on: macos-latest diff --git a/scripts/finalize-appimage.sh b/scripts/finalize-appimage.sh index 502bc87..06ea5e9 100755 --- a/scripts/finalize-appimage.sh +++ b/scripts/finalize-appimage.sh @@ -91,6 +91,11 @@ HOOK="apprun-hooks/triple-c-wayland-fallback.sh" APPIMAGE_TOOL_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage" APP_ID="com.triple-c.desktop" +# The channel pair lives in its own directory. Left beside the versioned image +# they are picked up by the release job's `*.AppImage` glob, and every release +# then carries an eighty-megabyte byte-identical duplicate under a second name +# — which is exactly as confusing on a downloads page as it sounds. +CHANNEL_DIR="update-channel" STABLE_NAME="Triple-C_x86_64.AppImage" UPDATE_TAG="linux-latest" UPDATE_INFO="zsync|https://github.com/shadowdao/triple-c/releases/download/${UPDATE_TAG}/${STABLE_NAME}.zsync" @@ -211,13 +216,18 @@ chmod +x "$tool" # --appimage-extract-and-run: CI runners generally have no FUSE. # -u embeds the update string and writes "$STABLE_NAME.zsync" beside the image. +mkdir -p "$CHANNEL_DIR" ARCH=x86_64 "$tool" --appimage-extract-and-run \ - -u "$UPDATE_INFO" "$root" "$STABLE_NAME" >/dev/null -chmod +x "$STABLE_NAME" + -u "$UPDATE_INFO" "$root" "$CHANNEL_DIR/$STABLE_NAME" >/dev/null +chmod +x "$CHANNEL_DIR/$STABLE_NAME" # The versioned name is what the per-version release publishes; the stable one -# and its .zsync go to the rolling tag. Same bytes, two names. -cp "$STABLE_NAME" "$appimage" +# and its .zsync go to the rolling tag. Same bytes, two names, two places. +# zsyncmake writes the .zsync into the working directory, not beside the image +# it describes, so it has to be collected rather than assumed in place. +[ -e "$STABLE_NAME.zsync" ] && mv "$STABLE_NAME.zsync" "$CHANNEL_DIR/" + +cp "$CHANNEL_DIR/$STABLE_NAME" "$appimage" chmod +x "$appimage" # The guards are the test. Each one is a way the repack could look like it @@ -245,13 +255,18 @@ grep -q "^Categories=.\+" "$out"/*.desktop || fail "Categories is still empty." # the URL it fetched the .zsync from. That is exactly why the output is named # for the fixed tag: a versioned name here resolves to the build the client # already has. -[ -e "$STABLE_NAME" ] || fail "the stable-named image is missing." -[ -e "$STABLE_NAME.zsync" ] || fail "appimagetool wrote no $STABLE_NAME.zsync." +[ -e "$CHANNEL_DIR/$STABLE_NAME" ] || fail "the stable-named image is missing." +[ -e "$CHANNEL_DIR/$STABLE_NAME.zsync" ] || fail "appimagetool wrote no .zsync." -readelf -p .upd_info "$STABLE_NAME" 2>/dev/null | grep -q "$UPDATE_TAG" \ +readelf -p .upd_info "$CHANNEL_DIR/$STABLE_NAME" 2>/dev/null | grep -q "$UPDATE_TAG" \ || fail "the image carries no update information for the $UPDATE_TAG tag." -grep -aq "^Filename: $STABLE_NAME$" "$STABLE_NAME.zsync" \ +grep -aq "^Filename: $STABLE_NAME$" "$CHANNEL_DIR/$STABLE_NAME.zsync" \ || fail "the .zsync names something other than $STABLE_NAME." -echo "OK: $appimage prefers the host $LIB (fallback kept), carries AppStream" -echo " metadata, and updates from the $UPDATE_TAG tag via $STABLE_NAME.zsync." +# The versioned release must carry one AppImage, not two. This is the guard +# for the duplicate that shipped in 0.4.20 and 0.4.21. +count="$(ls -1 *.AppImage 2>/dev/null | wc -l)" +[ "$count" = "1" ] || fail "expected 1 AppImage beside the release, found $count." + +echo "OK: $appimage prefers the host $LIB (fallback kept) and carries AppStream" +echo " metadata. Channel pair in $CHANNEL_DIR/, updating from the $UPDATE_TAG tag." diff --git a/scripts/publish-update-channel.sh b/scripts/publish-update-channel.sh index 8126bcc..2d07298 100755 --- a/scripts/publish-update-channel.sh +++ b/scripts/publish-update-channel.sh @@ -17,7 +17,22 @@ # It writes to GitHub rather than Gitea because that mirror is where updates # are pulled from. Needs GH_PAT with contents write on the mirror. # -# Usage: GH_PAT=... publish-update-channel.sh +# **The tag has to exist in Gitea, not just on GitHub, and that is the whole +# reason this script touches Gitea at all.** Gitea push-mirrors this repo to +# GitHub, and a mirror push deletes remote refs that have no local counterpart. +# A tag created only by GitHub's release API therefore survives until the next +# mirror run and then vanishes — which is exactly what happened to 0.4.20 and +# 0.4.21: the release was created and both URLs verified 200 at 00:38, and the +# 13:04 mirror deleted the tag, leaving every installed copy checking a 404. +# Versioned tags never had this problem because `create-tag` creates them in +# Gitea first. So does this one, now, and before the GitHub release rather than +# after, so there is no window where the two disagree. +# +# Note what this means for verification: publishing correctly is not evidence +# the channel still works hours later. The Gitea tag is what makes it durable, +# so its absence is treated as a failure rather than a warning. +# +# Usage: GH_PAT=... GITEA_TOKEN=... GITEA_SHA=... publish-update-channel.sh set -euo pipefail @@ -26,7 +41,12 @@ TAG="linux-latest" API="https://api.github.com/repos/$REPO" ASSETS=("Triple-C_x86_64.AppImage" "Triple-C_x86_64.AppImage.zsync") +GITEA_API="${GITEA_API:-https://repo.anhonesthost.net/api/v1}" +GITEA_REPO="${GITEA_REPO:-CyberCoveLLC/Triple-C}" + : "${GH_PAT:?GH_PAT is required to publish the update channel}" +: "${GITEA_TOKEN:?GITEA_TOKEN is required to anchor the $TAG tag against the mirror}" +: "${GITEA_SHA:?GITEA_SHA is required to point the $TAG tag at this build}" dir="${1:?usage: publish-update-channel.sh }" cd "$dir" @@ -35,6 +55,21 @@ for asset in "${ASSETS[@]}"; do done gh() { curl -sf -H "Authorization: Bearer $GH_PAT" -H "Accept: application/vnd.github+json" "$@"; } +tea() { curl -sf -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" "$@"; } + +# Anchor the tag in Gitea first — see the header. Moved rather than left +# alone: it has to name this build, and the mirror will carry whatever Gitea +# holds over the top of GitHub's copy. +echo "==> Anchoring the $TAG tag in Gitea at ${GITEA_SHA:0:9}" +tea -X DELETE "$GITEA_API/repos/$GITEA_REPO/tags/$TAG" >/dev/null 2>&1 || true +tea -X POST "$GITEA_API/repos/$GITEA_REPO/tags" \ + -d "{\"tag_name\": \"$TAG\", \"target\": \"$GITEA_SHA\", \"message\": \"Rolling Linux update channel\"}" \ + >/dev/null + +# Not best-effort. Without this tag the mirror removes GitHub's and the +# channel dies silently somewhere between now and four hours from now. +tea "$GITEA_API/repos/$GITEA_REPO/tags/$TAG" >/dev/null 2>&1 \ + || { echo "FAILED: the $TAG tag does not exist in Gitea; the mirror would delete GitHub's copy." >&2; exit 1; } echo "==> Looking for the $TAG release" release="$(gh "$API/releases/tags/$TAG" 2>/dev/null || true)" @@ -92,4 +127,4 @@ for asset in "${ASSETS[@]}"; do echo " $code $url" done -echo "OK: $TAG updated." +echo "OK: $TAG updated, and anchored in Gitea so the mirror preserves it."