Add a Gitea Actions workflow that publishes the Linux AppImage.
AppImage release / appimage (push) Successful in 9m35s

The job builds with scripts/build-appimage.sh on an existing v* tag and uploads the AppImage as a release asset.

Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
Cursor Agentandchen committed 2026-10-11 06:59:54 +00:00
1 parent f243ff78ef
commit ed55f02983
1 file changed
+274
+274
View File
@@ -0,0 +1,274 @@
# Builds the Linux AppImage with scripts/build-appimage.sh and uploads it
# to the release for an existing v* tag. Pushing this file does not run a build.
# The act_runner label ubuntu-latest is docker://catthehacker/ubuntu:full-latest.
name: AppImage release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Existing v* tag to publish. Leave empty when this run is already on a v* tag."
required: false
type: string
permissions:
contents: write
jobs:
appimage:
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve the existing release tag
id: tag
shell: bash
env:
INPUT_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || '' }}
run: |
set -euo pipefail
TAG=""
if [[ "${GITHUB_REF_TYPE:-}" == "tag" && "${GITHUB_REF_NAME:-}" == v* ]]; then
TAG="${GITHUB_REF_NAME}"
fi
if [[ -n "${INPUT_TAG}" ]]; then
if [[ "${INPUT_TAG}" != v* ]]; then
echo "The tag input must match v*, for example v0.1.0." >&2
exit 1
fi
TAG="${INPUT_TAG}"
fi
if [[ -z "${TAG}" || "${TAG}" != v* ]]; then
echo "This workflow publishes an AppImage for a tag that already exists and starts with v, such as v0.1.0." >&2
echo "Push that tag, or run this workflow on it. It does not create a tag." >&2
exit 1
fi
if ! git ls-remote --exit-code origin "refs/tags/${TAG}" >/dev/null; then
echo "Tag ${TAG} is not on the server. This workflow does not create tags." >&2
exit 1
fi
if [[ "${GITHUB_REF_NAME:-}" != "${TAG}" ]]; then
git fetch origin "refs/tags/${TAG}:refs/tags/${TAG}"
git checkout --detach "refs/tags/${TAG}"
fi
echo "tag=${TAG}" >> "${GITHUB_OUTPUT}"
echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"
- name: Install build tools
shell: bash
run: |
set -euo pipefail
if [[ "$(id -u)" -eq 0 ]]; then
apt_get() { apt-get "$@"; }
else
apt_get() { sudo apt-get "$@"; }
fi
export DEBIAN_FRONTEND=noninteractive
apt_get update
apt_get install -y --no-install-recommends \
ca-certificates \
curl \
xz-utils \
binutils \
git \
jq \
file \
unzip
apt_get install -y --no-install-recommends libfuse2 \
|| apt_get install -y --no-install-recommends libfuse2t64 \
|| echo "No libfuse2 package in this image. AppImage tools will extract instead of mounting."
need_node=0
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
need_node=1
else
major="$(node -p 'Number(process.versions.node.split(".")[0])')"
if [[ "${major}" -lt 20 ]]; then
need_node=1
fi
fi
if [[ "${need_node}" -eq 1 ]]; then
tarball="$(curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt | awk '/linux-x64\.tar\.xz$/ { print $2; exit }')"
if [[ -z "${tarball}" ]]; then
echo "Could not find a Node.js 22 linux-x64 tarball." >&2
exit 1
fi
curl -fL --retry 3 -o "/tmp/${tarball}" "https://nodejs.org/dist/latest-v22.x/${tarball}"
if [[ "$(id -u)" -eq 0 ]]; then
tar -xJf "/tmp/${tarball}" -C /usr/local --strip-components=1
else
sudo tar -xJf "/tmp/${tarball}" -C /usr/local --strip-components=1
fi
fi
command -v node
command -v npm
command -v curl
command -v tar
command -v strip
node -v
npm -v
- name: Build the AppImage
shell: bash
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
run: |
set -euo pipefail
# npm omits devDependencies when NODE_ENV=production. electron-builder is one of them.
env -u NODE_ENV bash scripts/build-appimage.sh
- name: Require the AppImage to be under 600MB
id: appimage
shell: bash
run: |
set -euo pipefail
APPIMAGE="$(find dist -maxdepth 1 -name '*.AppImage' -printf '%T@ %p\n' | sort -nr | head -1 | cut -d' ' -f2-)"
if [[ -z "${APPIMAGE}" || ! -f "${APPIMAGE}" ]]; then
echo "No AppImage was produced in dist/." >&2
exit 1
fi
SIZE="$(stat -c%s "${APPIMAGE}")"
LIMIT=$((600 * 1024 * 1024))
if (( SIZE > LIMIT )); then
echo "AppImage is ${SIZE} bytes, over 600MB." >&2
exit 1
fi
echo "Built ${APPIMAGE} (${SIZE} bytes)."
echo "path=${APPIMAGE}" >> "${GITHUB_OUTPUT}"
echo "name=$(basename "${APPIMAGE}")" >> "${GITHUB_OUTPUT}"
- name: Upload the AppImage to the release
shell: bash
env:
GITEA_TOKEN: ${{ github.token }}
TAG: ${{ steps.tag.outputs.tag }}
TARGET_SHA: ${{ steps.tag.outputs.sha }}
APPIMAGE_PATH: ${{ steps.appimage.outputs.path }}
ASSET_NAME: ${{ steps.appimage.outputs.name }}
run: |
set -euo pipefail
TOKEN="${GITEA_TOKEN:-${GITHUB_TOKEN:-}}"
if [[ -z "${TOKEN}" ]]; then
echo "No job token. act_runner must provide a token that can publish releases." >&2
exit 1
fi
if [[ ! -f "${APPIMAGE_PATH}" ]]; then
echo "AppImage not found at ${APPIMAGE_PATH}." >&2
exit 1
fi
SIZE="$(stat -c%s "${APPIMAGE_PATH}")"
LIMIT=$((600 * 1024 * 1024))
if (( SIZE > LIMIT )); then
echo "AppImage is ${SIZE} bytes, over 600MB." >&2
exit 1
fi
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
AUTH=( -H "Authorization: token ${TOKEN}" )
ref_code="$(curl -sS -o /tmp/tag-ref.json -w "%{http_code}" "${AUTH[@]}" "${API}/git/refs/tags/${TAG}")"
if [[ "${ref_code}" != "200" ]]; then
echo "Tag ${TAG} is not on the server (HTTP ${ref_code}). This workflow does not create tags." >&2
cat /tmp/tag-ref.json >&2
exit 1
fi
NOTES_FILE="$(mktemp)"
cat > "${NOTES_FILE}" <<EOF
Download \`${ASSET_NAME}\` from the assets on this release.
Make it executable and start it:
\`\`\`bash
chmod +x ${ASSET_NAME}
./${ASSET_NAME}
\`\`\`
If it does not start because FUSE is missing, run it this way instead:
\`\`\`bash
APPIMAGE_EXTRACT_AND_RUN=1 ./${ASSET_NAME}
\`\`\`
EOF
# The workflow YAML indent is stripped, but keep the notes flush left.
sed -i 's/^[[:space:]]*//' "${NOTES_FILE}"
TITLE="Recorder Tab Converter ${TAG}"
create_body="$(jq -n \
--arg tag "${TAG}" \
--arg name "${TITLE}" \
--rawfile body "${NOTES_FILE}" \
--arg target "${TARGET_SHA}" \
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:false, target_commitish:$target}')"
create_code="$(curl -sS -o /tmp/release.json -w "%{http_code}" \
-X POST "${AUTH[@]}" \
-H "Content-Type: application/json" \
--data "${create_body}" \
"${API}/releases")"
if [[ "${create_code}" == "201" ]]; then
RELEASE_ID="$(jq -r '.id' /tmp/release.json)"
elif [[ "${create_code}" == "409" ]]; then
get_code="$(curl -sS -o /tmp/release.json -w "%{http_code}" "${AUTH[@]}" "${API}/releases/tags/${TAG}")"
if [[ "${get_code}" != "200" ]]; then
echo "Release for ${TAG} already exists but could not be read (HTTP ${get_code})." >&2
cat /tmp/release.json >&2
exit 1
fi
RELEASE_ID="$(jq -r '.id' /tmp/release.json)"
patch_body="$(jq -n --arg name "${TITLE}" --rawfile body "${NOTES_FILE}" '{name:$name, body:$body}')"
patch_code="$(curl -sS -o /tmp/release-patch.json -w "%{http_code}" \
-X PATCH "${AUTH[@]}" \
-H "Content-Type: application/json" \
--data "${patch_body}" \
"${API}/releases/${RELEASE_ID}")"
if [[ "${patch_code}" != "200" ]]; then
echo "Could not update the release notes (HTTP ${patch_code})." >&2
cat /tmp/release-patch.json >&2
exit 1
fi
else
echo "Could not create the release (HTTP ${create_code})." >&2
cat /tmp/release.json >&2
exit 1
fi
if [[ -z "${RELEASE_ID}" || "${RELEASE_ID}" == "null" ]]; then
echo "The release id is missing." >&2
exit 1
fi
mapfile -t asset_ids < <(jq -r --arg name "${ASSET_NAME}" '.assets[]? | select(.name == $name) | .id' /tmp/release.json)
for asset_id in "${asset_ids[@]}"; do
[[ -z "${asset_id}" ]] && continue
del_code="$(curl -sS -o /tmp/asset-delete.json -w "%{http_code}" \
-X DELETE "${AUTH[@]}" \
"${API}/releases/${RELEASE_ID}/assets/${asset_id}")"
if [[ "${del_code}" != "204" && "${del_code}" != "200" ]]; then
echo "Could not replace the existing asset ${ASSET_NAME} (HTTP ${del_code})." >&2
cat /tmp/asset-delete.json >&2
exit 1
fi
done
enc_name="$(jq -rn --arg name "${ASSET_NAME}" '$name|@uri')"
upload_code="$(curl -sS -o /tmp/upload.json -w "%{http_code}" \
-X POST "${AUTH[@]}" \
-F "attachment=@${APPIMAGE_PATH};filename=${ASSET_NAME}" \
"${API}/releases/${RELEASE_ID}/assets?name=${enc_name}")"
if [[ "${upload_code}" != "201" ]]; then
echo "Upload failed with HTTP ${upload_code}." >&2
cat /tmp/upload.json >&2
exit 1
fi
echo "Uploaded ${ASSET_NAME} (${SIZE} bytes) to release ${TAG}."
jq -r '.browser_download_url // empty' /tmp/upload.json