Skip to content
Latchkey

Build AppImage workflow (linx-systems/clamui)

The Build AppImage workflow from linx-systems/clamui, explained and optimized by Latchkey.

C

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.

Grade your own workflow free or run it on Latchkey →
Source: linx-systems/clamui.github/workflows/build-appimage.ymlLicense MITView source

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

workflow (.yml)
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

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:

This workflow runs 1 job per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow