Build AppImage workflow (linx-systems/clamui)
The Build AppImage workflow from linx-systems/clamui, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Build AppImage workflow from the linx-systems/clamui repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Build AppImage
on:
push:
branches: [master]
tags: ["v*"]
pull_request:
branches: [master]
workflow_dispatch:
inputs:
sign:
description: "Sign the AppImage with GPG"
type: boolean
default: false
workflow_call:
inputs:
sign:
description: "Sign the AppImage with GPG"
type: boolean
default: false
jobs:
build-appimage:
permissions:
contents: read
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
submodules: recursive
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
python3 python3-pip python3-gi python3-gi-cairo \
gir1.2-gtk-4.0 gir1.2-adw-1 \
libgtk-4-dev libadwaita-1-dev \
libfuse2t64 wget rsync
- name: Build AppImage
env:
# Embed update information for delta updates on tag (release) builds.
# AppImageUpdate, AppImageLauncher, and AM use this to perform efficient
# zsync-based updates that only download changed blocks.
UPDATE_INFORMATION: ${{ startsWith(github.ref, 'refs/tags/') && 'gh-releases-zsync|linx-systems|clamui|latest|ClamUI-*-x86_64.AppImage.zsync' || '' }}
run: ./appimage/build-appimage.sh
- name: Test AppImage (smoke test)
run: |
chmod +x ClamUI-*.AppImage
# Extract AppImage for testing
./ClamUI-*.AppImage --appimage-extract
# Test 1: Python interpreter works
./squashfs-root/usr/bin/python3 --version
# Set up bundled Python environment (mirrors AppRun)
HERE="$(pwd)/squashfs-root"
export PYTHONHOME="$HERE/usr"
PYTHON_VER=$(./squashfs-root/usr/bin/python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
export PYTHONPATH="$HERE/usr/lib/python${PYTHON_VER}/site-packages:$HERE/usr/lib/python${PYTHON_VER}"
export GI_TYPELIB_PATH="$HERE/usr/lib/girepository-1.0"
export LD_LIBRARY_PATH="$HERE/usr/lib:$HERE/usr/lib/x86_64-linux-gnu"
# Test 2: ClamUI module imports successfully
./squashfs-root/usr/bin/python3 -c "
from src.main import main
print('src.main: OK')
"
# Test 3: GTK4/Adwaita bindings load
./squashfs-root/usr/bin/python3 -c "
from gi.repository import Gtk, Adw
print('GTK:', Gtk._version)
print('Adwaita: OK')
"
# Test 4: Core modules import (validates bundling)
./squashfs-root/usr/bin/python3 -c "
from src.core.scanner import Scanner
from src.core.settings_manager import SettingsManager
from src.core.quarantine.manager import QuarantineManager
print('Core modules: OK')
"
# Cleanup extracted files
rm -rf squashfs-root
# Import GPG key for tag builds or when sign input is true
- name: Import GPG key
if: startsWith(github.ref, 'refs/tags/') || inputs.sign == true
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
id: gpg
# Sign AppImage on tag builds or when sign input is true
- name: Sign AppImage
if: startsWith(github.ref, 'refs/tags/') || inputs.sign == true
env:
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
# Download appimagetool for signing
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
# Extract the existing AppImage
APPIMAGE=$(ls ClamUI-*.AppImage)
./$APPIMAGE --appimage-extract
# Build signing args - re-embed update info so it survives re-packaging
SIGN_ARGS=(--sign --sign-key "${{ steps.gpg.outputs.fingerprint }}")
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
SIGN_ARGS+=(-u "gh-releases-zsync|linx-systems|clamui|latest|ClamUI-*-x86_64.AppImage.zsync")
fi
# Re-package with signature (and update info if tag build)
./appimagetool-x86_64.AppImage "${SIGN_ARGS[@]}" \
squashfs-root/ "$APPIMAGE"
# Cleanup
rm -rf squashfs-root appimagetool-x86_64.AppImage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v7
with:
name: appimage-package
path: |
ClamUI-*.AppImage
ClamUI-*.AppImage.zsync
retention-days: 7
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Build AppImage on: push: branches: [master] tags: ["v*"] pull_request: branches: [master] workflow_dispatch: inputs: sign: description: "Sign the AppImage with GPG" type: boolean default: false workflow_call: inputs: sign: description: "Sign the AppImage with GPG" type: boolean default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-appimage: timeout-minutes: 30 permissions: contents: read runs-on: latchkey-small steps: - name: Checkout repository uses: actions/checkout@v7 with: submodules: recursive - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y \ python3 python3-pip python3-gi python3-gi-cairo \ gir1.2-gtk-4.0 gir1.2-adw-1 \ libgtk-4-dev libadwaita-1-dev \ libfuse2t64 wget rsync - name: Build AppImage env: # Embed update information for delta updates on tag (release) builds. # AppImageUpdate, AppImageLauncher, and AM use this to perform efficient # zsync-based updates that only download changed blocks. UPDATE_INFORMATION: ${{ startsWith(github.ref, 'refs/tags/') && 'gh-releases-zsync|linx-systems|clamui|latest|ClamUI-*-x86_64.AppImage.zsync' || '' }} run: ./appimage/build-appimage.sh - name: Test AppImage (smoke test) run: | chmod +x ClamUI-*.AppImage # Extract AppImage for testing ./ClamUI-*.AppImage --appimage-extract # Test 1: Python interpreter works ./squashfs-root/usr/bin/python3 --version # Set up bundled Python environment (mirrors AppRun) HERE="$(pwd)/squashfs-root" export PYTHONHOME="$HERE/usr" PYTHON_VER=$(./squashfs-root/usr/bin/python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") export PYTHONPATH="$HERE/usr/lib/python${PYTHON_VER}/site-packages:$HERE/usr/lib/python${PYTHON_VER}" export GI_TYPELIB_PATH="$HERE/usr/lib/girepository-1.0" export LD_LIBRARY_PATH="$HERE/usr/lib:$HERE/usr/lib/x86_64-linux-gnu" # Test 2: ClamUI module imports successfully ./squashfs-root/usr/bin/python3 -c " from src.main import main print('src.main: OK') " # Test 3: GTK4/Adwaita bindings load ./squashfs-root/usr/bin/python3 -c " from gi.repository import Gtk, Adw print('GTK:', Gtk._version) print('Adwaita: OK') " # Test 4: Core modules import (validates bundling) ./squashfs-root/usr/bin/python3 -c " from src.core.scanner import Scanner from src.core.settings_manager import SettingsManager from src.core.quarantine.manager import QuarantineManager print('Core modules: OK') " # Cleanup extracted files rm -rf squashfs-root # Import GPG key for tag builds or when sign input is true - name: Import GPG key if: startsWith(github.ref, 'refs/tags/') || inputs.sign == true uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} id: gpg # Sign AppImage on tag builds or when sign input is true - name: Sign AppImage if: startsWith(github.ref, 'refs/tags/') || inputs.sign == true env: APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: | # Download appimagetool for signing wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool-x86_64.AppImage # Extract the existing AppImage APPIMAGE=$(ls ClamUI-*.AppImage) ./$APPIMAGE --appimage-extract # Build signing args - re-embed update info so it survives re-packaging SIGN_ARGS=(--sign --sign-key "${{ steps.gpg.outputs.fingerprint }}") if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then SIGN_ARGS+=(-u "gh-releases-zsync|linx-systems|clamui|latest|ClamUI-*-x86_64.AppImage.zsync") fi # Re-package with signature (and update info if tag build) ./appimagetool-x86_64.AppImage "${SIGN_ARGS[@]}" \ squashfs-root/ "$APPIMAGE" # Cleanup rm -rf squashfs-root appimagetool-x86_64.AppImage - name: Upload AppImage artifact uses: actions/upload-artifact@v7 with: name: appimage-package path: | ClamUI-*.AppImage ClamUI-*.AppImage.zsync retention-days: 7
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
1 third-party action is referenced by a movable tag. Pin it to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
- Network fetches
This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.