Add a Gitea Actions workflow that publishes the Linux AppImage.
AppImage release / appimage (push) Successful in 9m35s
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:
1 file changed
+274
@@ -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
|
||||||
Reference in new issue
Block a user