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."