# Linux packaging workflow - builds .deb and .rpm packages

name: Linux Packages

on:
  workflow_call:
  workflow_dispatch:

jobs:
  # Build .deb and .rpm packages for Linux
  build-linux-packages:
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            arch: amd64
            rpm_arch: x86_64
            runner: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            arch: arm64
            rpm_arch: aarch64
            runner: ubuntu-latest
    runs-on: ${{ matrix.runner }}
    container:
      image: ubuntu:20.04
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      DEBIAN_FRONTEND: noninteractive
    steps:
      - name: Install system dependencies
        run: |
          apt-get update
          apt-get install -y --no-install-recommends \
            ca-certificates \
            curl \
            git \
            build-essential \
            binutils \
            pkg-config \
            libclang-dev \
            debhelper \
            dpkg-dev \
            fakeroot \
            lintian

      - uses: actions/checkout@v6
        with:
          persist-credentials: false
          submodules: recursive

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      # rust-toolchain.toml overrides the targets parameter above, so we need to explicitly add it
      - name: Add target
        run: rustup target add ${{ matrix.target }}

      - name: Install cross-compilation tools
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          apt-get update
          apt-get install -y --no-install-recommends \
            gcc-aarch64-linux-gnu \
            g++-aarch64-linux-gnu \
            libc6-dev-arm64-cross \
            binutils-aarch64-linux-gnu

      - name: Install arm64 runtime libraries for shlibdeps
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          # dpkg-shlibdeps (run by dh_shlibdeps during the .deb build) resolves
          # the cross-built binary's library deps against installed dpkg metadata.
          # libc6-dev-arm64-cross installs into /usr/aarch64-linux-gnu with no
          # shlibs/symbols files, so dpkg-shlibdeps can neither find the libraries
          # nor map them to package names. Enable the arm64 architecture and pull
          # the real runtime libraries from the Ubuntu ports mirror; this both
          # satisfies the lookup and lets ${shlibs:Depends} resolve to correct,
          # versioned dependencies (libc6, libgcc-s1).
          sed -i 's|^deb http|deb [arch=amd64] http|g' /etc/apt/sources.list
          {
            echo 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports focal main restricted universe multiverse'
            echo 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports focal-updates main restricted universe multiverse'
          } >> /etc/apt/sources.list
          dpkg --add-architecture arm64
          apt-get update
          apt-get install -y --no-install-recommends libc6:arm64 libgcc-s1:arm64

      - name: Install cargo-generate-rpm
        run: |
          cargo install cargo-generate-rpm

      - name: Build release binary
        run: |
          if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
            export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
            export BINDGEN_EXTRA_CLANG_ARGS="--sysroot=/usr/aarch64-linux-gnu"
          fi
          cargo build --release --target ${{ matrix.target }}

      - name: Strip binary to reduce size
        run: |
          if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
            aarch64-linux-gnu-strip target/${{ matrix.target }}/release/fresh
          else
            strip target/${{ matrix.target }}/release/fresh
          fi
          ls -la target/${{ matrix.target }}/release/fresh

      - name: Setup target symlink for packaging
        run: |
          # cargo-generate-rpm's asset paths are relative to the crate dir;
          # symlink so they resolve to the workspace target/.
          ln -s ../../target crates/fresh-editor/target

      - name: Sync debian/changelog with Cargo.toml version
        run: ./scripts/update-debian-changelog.sh

      - name: Build .deb package
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
          BINDGEN_EXTRA_CLANG_ARGS: --sysroot=/usr/aarch64-linux-gnu
        run: |
          # -b: binary-only, -d: don't check build-deps (rustup provides
          # rust, not apt), -us -uc: don't sign. debian/rules reuses the
          # already-built binary at target/${{ matrix.target }}/release/fresh.
          dpkg-buildpackage -b -d -us -uc --host-arch ${{ matrix.arch }}

      - name: Build .rpm package
        working-directory: crates/fresh-editor
        run: |
          # Provenance receipt shipped inside the .rpm (see Cargo.toml
          # [package.metadata.generate-rpm] assets). target/ is symlinked to
          # the workspace target above.
          # `pkg_arch` is rpm's spelling (x86_64/aarch64), recorded here so the
          # next update looks up the right .rpm on the release feed instead of
          # re-deriving the arch from the running binary's target triple.
          sh ../../scripts/write-install-receipt.sh dnf target/install-receipt.toml \
            target=${{ matrix.target }} pkg_arch=${{ matrix.rpm_arch }}
          cargo generate-rpm --target ${{ matrix.target }}

      - name: Collect packages
        run: |
          mkdir -p packages
          # dpkg-buildpackage drops the .deb in the parent directory of the
          # source tree by convention.
          find .. -maxdepth 1 -name "fresh-editor_*_${{ matrix.arch }}.deb" -exec cp {} packages/ \;
          # RPM lands in workspace target via the symlink set up above.
          find target/${{ matrix.target }}/generate-rpm -name "*.rpm" -exec cp {} packages/ \;
          # Checksum sidecars: `fresh --cmd update` downloads these packages and
          # verifies them against `<asset>.sha256` before installing, and fails
          # closed if the sidecar is missing.
          (cd packages && for f in *.deb *.rpm; do sha256sum "$f" > "$f.sha256"; done)
          ls -la packages/

      - name: Upload Linux packages
        uses: actions/upload-artifact@v6
        with:
          name: artifacts-linux-packages-${{ matrix.arch }}
          path: packages/*

  # Test .deb package installation on multiple Ubuntu versions
  test-deb-package:
    needs:
      - build-linux-packages
    strategy:
      fail-fast: false
      matrix:
        include:
          - distro: ubuntu:22.04
            name: Ubuntu 22.04
          - distro: ubuntu:24.04
            name: Ubuntu 24.04
          - distro: debian:12
            name: Debian 12
    runs-on: ubuntu-22.04
    container:
      image: ${{ matrix.distro }}
    env:
      DEBIAN_FRONTEND: noninteractive
    steps:
      - name: Download .deb package
        uses: actions/download-artifact@v7
        with:
          name: artifacts-linux-packages-amd64
          path: packages/

      - name: Install .deb package
        run: |
          # Ubuntu's minimal docker images ship /etc/dpkg/dpkg.cfg.d/excludes
          # which path-excludes /usr/share/man and /usr/share/doc, so man pages
          # never land on disk even though they're in the package. Remove it so
          # the man-page verification below reflects a real install.
          rm -f /etc/dpkg/dpkg.cfg.d/excludes
          apt-get update
          apt-get install -y ./packages/*.deb

      - name: Verify installation
        run: |
          echo "=== Testing on ${{ matrix.name }} ==="

          echo ""
          echo "=== Checking installed files ==="
          dpkg -L fresh-editor

          echo ""
          echo "=== Verifying binary location ==="
          ls -la /usr/bin/fresh
          test -x /usr/bin/fresh || { echo "Binary not executable"; exit 1; }
          test -f /usr/bin/fresh || { echo "/usr/bin/fresh is not a regular file"; exit 1; }
          test ! -L /usr/bin/fresh || { echo "/usr/bin/fresh should not be a symlink"; exit 1; }

          echo ""
          echo ""
          echo "=== Verifying the installed binary resolves its provenance ==="
          # Assert what the editor actually concludes, not merely that a file is
          # on disk. A `test -f` + `grep` of the receipt's contents passed for
          # every release in which the receipt was shipped to a directory the
          # resolver never searched, so apt installs silently resolved to
          # channel `unknown` and were offered no update path at all.
          paths=$(/usr/bin/fresh --cmd config paths) || { echo "config paths failed"; exit 1; }
          echo "$paths"
          echo "$paths" | grep -qE '^install channel: +.*\(apt\)$' \
            || { echo "installed binary did not resolve channel apt"; exit 1; }
          echo "$paths" | grep -qE '^provenance: +Authoritative$' \
            || { echo "provenance is not Authoritative — the receipt was not found"; exit 1; }

          echo ""
          echo "=== Verifying nothing else shipped under /usr/share/fresh-editor ==="
          # Only the receipt belongs here — plugins/themes are embedded in the binary.
          leaked=$(find /usr/share/fresh-editor -type f ! -name install-receipt.toml || true)
          test -z "$leaked" || { echo "unexpected files under /usr/share/fresh-editor: $leaked"; exit 1; }

          echo ""
          echo "=== Testing binary ==="
          fresh --version
          /usr/bin/fresh --version

          echo ""
          echo "=== Verifying man page installed ==="
          ls -la /usr/share/man/man1/fresh.1.gz || { echo "Man page missing"; exit 1; }

          echo ""
          echo "=== Verifying desktop entry + icons ==="
          ls -la /usr/share/applications/fresh.desktop
          ls /usr/share/icons/hicolor/*/apps/fresh.png | head

          echo ""
          echo "=== Testing uninstall ==="
          apt-get remove -y fresh-editor

          echo ""
          echo "=== Verifying binary removed ==="
          test ! -e /usr/bin/fresh || { echo "Binary not removed after uninstall"; exit 1; }

          echo ""
          echo "deb package test PASSED on ${{ matrix.name }}"

  # Test .rpm package installation on multiple Fedora/RHEL versions
  test-rpm-package:
    needs:
      - build-linux-packages
    strategy:
      fail-fast: false
      matrix:
        include:
          - distro: fedora:39
            name: Fedora 39
          - distro: fedora:40
            name: Fedora 40
          - distro: rockylinux:9
            name: Rocky Linux 9
    runs-on: ubuntu-22.04
    container:
      image: ${{ matrix.distro }}
    steps:
      - name: Download .rpm package
        uses: actions/download-artifact@v7
        with:
          name: artifacts-linux-packages-amd64
          path: packages/

      - name: Install .rpm package
        run: |
          # Fedora/Rocky minimal images ship neither the `file` utility (used by
          # the ELF check below) nor, by default, man pages — dnf.conf sets
          # tsflags=nodocs. Install `file` and override tsflags so docs install
          # and the verification below reflects a real install.
          dnf install -y file
          dnf install -y --setopt=tsflags='' packages/*.rpm

      - name: Verify installation
        run: |
          echo "=== Testing on ${{ matrix.name }} ==="

          echo ""
          echo "=== Checking installed files ==="
          rpm -ql fresh-editor

          echo ""
          echo "=== Verifying binary location ==="
          ls -la /usr/bin/fresh
          test -x /usr/bin/fresh || { echo "Binary not executable"; exit 1; }
          test -f /usr/bin/fresh || { echo "/usr/bin/fresh is not a regular file"; exit 1; }
          test ! -L /usr/bin/fresh || { echo "/usr/bin/fresh should not be a symlink"; exit 1; }
          # First bytes are the ELF magic, not "#!/bin/sh" — confirms binary, not wrapper.
          file /usr/bin/fresh | grep -q ELF || { echo "/usr/bin/fresh is not an ELF binary"; exit 1; }

          echo ""
          echo ""
          echo "=== Verifying the installed binary resolves its provenance ==="
          # See the .deb job: assert the resolved channel, not just that the
          # receipt file exists — the latter stays green when the resolver
          # cannot find it.
          paths=$(/usr/bin/fresh --cmd config paths) || { echo "config paths failed"; exit 1; }
          echo "$paths"
          echo "$paths" | grep -qE '^install channel: +.*\(dnf\)$' \
            || { echo "installed binary did not resolve channel dnf"; exit 1; }
          echo "$paths" | grep -qE '^provenance: +Authoritative$' \
            || { echo "provenance is not Authoritative — the receipt was not found"; exit 1; }

          echo ""
          echo "=== Verifying nothing else shipped under /usr/share/fresh-editor ==="
          leaked=$(find /usr/share/fresh-editor -type f ! -name install-receipt.toml || true)
          test -z "$leaked" || { echo "unexpected files under /usr/share/fresh-editor: $leaked"; exit 1; }

          echo ""
          echo "=== Testing binary ==="
          fresh --version
          /usr/bin/fresh --version

          echo ""
          echo "=== Verifying man page installed ==="
          ls -la /usr/share/man/man1/fresh.1* || { echo "Man page missing"; exit 1; }

          echo ""
          echo "=== Verifying desktop entry + icons ==="
          ls -la /usr/share/applications/fresh.desktop
          ls /usr/share/icons/hicolor/*/apps/fresh.png | head

          echo ""
          echo "=== Testing uninstall ==="
          dnf remove -y fresh-editor

          echo ""
          echo "=== Verifying binary removed ==="
          test ! -e /usr/bin/fresh || { echo "Binary not removed after uninstall"; exit 1; }

          echo ""
          echo "rpm package test PASSED on ${{ matrix.name }}"

  # Test .deb package upgrade from previous version
  test-deb-upgrade:
    needs:
      - build-linux-packages
    runs-on: ubuntu-22.04
    container:
      image: ubuntu:22.04
    env:
      DEBIAN_FRONTEND: noninteractive
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v6

      - name: Get version info
        id: version
        run: |
          # Get current version from Cargo.toml
          CURRENT_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          echo "current=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
          echo "Current version: $CURRENT_VERSION"

      - name: Install dependencies
        run: |
          apt-get update
          apt-get install -y curl jq

      - name: Find and install previous version
        id: prev
        run: |
          # Get previous release version from GitHub
          PREV_VERSION=$(curl -s "https://api.github.com/repos/sinelaw/fresh/releases" | \
            jq -r '[.[] | select(.prerelease == false and .tag_name != "v${{ steps.version.outputs.current }}")] | .[0].tag_name' | \
            sed 's/^v//')

          if [ -z "$PREV_VERSION" ] || [ "$PREV_VERSION" = "null" ]; then
            echo "No previous release found, skipping upgrade test"
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          echo "Previous version: $PREV_VERSION"
          echo "version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
          echo "skip=false" >> "$GITHUB_OUTPUT"

          # Download and install previous version .deb package
          DEB_URL="https://github.com/sinelaw/fresh/releases/download/v${PREV_VERSION}/fresh-editor_${PREV_VERSION}-1_amd64.deb"
          echo "Downloading: $DEB_URL"
          curl -L -o /tmp/fresh-prev.deb "$DEB_URL" || {
            echo "Failed to download previous .deb, skipping"
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          }

          apt-get install -y /tmp/fresh-prev.deb

          # Verify previous version is installed
          INSTALLED=$(fresh --version)
          echo "Installed version: $INSTALLED"
          echo "$INSTALLED" | grep -q "$PREV_VERSION" || {
            echo "Warning: version mismatch, got $INSTALLED expected $PREV_VERSION"
          }

      - name: Download new .deb package
        if: steps.prev.outputs.skip != 'true'
        uses: actions/download-artifact@v7
        with:
          name: artifacts-linux-packages-amd64
          path: packages/

      - name: Upgrade to new version
        if: steps.prev.outputs.skip != 'true'
        run: |
          echo "=== Upgrading from v${{ steps.prev.outputs.version }} to v${{ steps.version.outputs.current }} ==="

          # Install (upgrade) to new version
          apt-get install -y ./packages/*.deb

          # Verify upgrade
          INSTALLED=$(fresh --version)
          echo "Installed version after upgrade: $INSTALLED"

          # Check version matches expected
          echo "$INSTALLED" | grep -q "${{ steps.version.outputs.current }}" || {
            echo "ERROR: Version mismatch after upgrade!"
            echo "Expected: ${{ steps.version.outputs.current }}"
            echo "Got: $INSTALLED"
            exit 1
          }

          echo ""
          echo "=== deb upgrade test PASSED ==="

  # Test .rpm package upgrade from previous version
  test-rpm-upgrade:
    needs:
      - build-linux-packages
    runs-on: ubuntu-22.04
    container:
      image: fedora:40
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@v6

      - name: Get version info
        id: version
        run: |
          # Get current version from Cargo.toml
          CURRENT_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          echo "current=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
          echo "Current version: $CURRENT_VERSION"

      - name: Install dependencies
        run: |
          dnf install -y curl jq

      - name: Find and install previous version
        id: prev
        run: |
          # Get previous release version from GitHub
          PREV_VERSION=$(curl -s "https://api.github.com/repos/sinelaw/fresh/releases" | \
            jq -r '[.[] | select(.prerelease == false and .tag_name != "v${{ steps.version.outputs.current }}")] | .[0].tag_name' | \
            sed 's/^v//')

          if [ -z "$PREV_VERSION" ] || [ "$PREV_VERSION" = "null" ]; then
            echo "No previous release found, skipping upgrade test"
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          echo "Previous version: $PREV_VERSION"
          echo "version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
          echo "skip=false" >> "$GITHUB_OUTPUT"

          # Download and install previous version .rpm package
          RPM_URL="https://github.com/sinelaw/fresh/releases/download/v${PREV_VERSION}/fresh-editor-${PREV_VERSION}-1.x86_64.rpm"
          echo "Downloading: $RPM_URL"
          curl -L -o /tmp/fresh-prev.rpm "$RPM_URL" || {
            echo "Failed to download previous .rpm, skipping"
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          }

          dnf install -y /tmp/fresh-prev.rpm

          # Verify previous version is installed
          INSTALLED=$(fresh --version)
          echo "Installed version: $INSTALLED"
          echo "$INSTALLED" | grep -q "$PREV_VERSION" || {
            echo "Warning: version mismatch, got $INSTALLED expected $PREV_VERSION"
          }

      - name: Download new .rpm package
        if: steps.prev.outputs.skip != 'true'
        uses: actions/download-artifact@v7
        with:
          name: artifacts-linux-packages-amd64
          path: packages/

      - name: Upgrade to new version
        if: steps.prev.outputs.skip != 'true'
        run: |
          echo "=== Upgrading from v${{ steps.prev.outputs.version }} to v${{ steps.version.outputs.current }} ==="

          # Upgrade to new version using rpm -U
          rpm -U ./packages/*.rpm

          # Verify upgrade
          INSTALLED=$(fresh --version)
          echo "Installed version after upgrade: $INSTALLED"

          # Check version matches expected
          echo "$INSTALLED" | grep -q "${{ steps.version.outputs.current }}" || {
            echo "ERROR: Version mismatch after upgrade!"
            echo "Expected: ${{ steps.version.outputs.current }}"
            echo "Got: $INSTALLED"
            exit 1
          }

          echo ""
          echo "=== rpm upgrade test PASSED ==="

  # Build the Debian source package (.dsc + .orig.tar.gz + .debian.tar.xz +
  # _source.changes). Uses the same debian/ tree as the binary build, but
  # ships cargo-vendored deps inside the orig tarball so the source package
  # is self-contained and rebuilds offline (what mentors.debian.net / Debian
  # buildds require).
  build-deb-source:
    runs-on: ubuntu-latest
    container:
      image: ubuntu:22.04
    env:
      DEBIAN_FRONTEND: noninteractive
    steps:
      - name: Install system dependencies
        run: |
          apt-get update
          apt-get install -y --no-install-recommends \
            ca-certificates \
            curl \
            git \
            build-essential \
            pkg-config \
            libclang-dev \
            debhelper \
            dpkg-dev \
            devscripts \
            fakeroot \
            lintian \
            rsync \
            xz-utils

      - uses: actions/checkout@v6
        with:
          persist-credentials: false
          submodules: recursive

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Sync debian/changelog with Cargo.toml version
        run: ./scripts/update-debian-changelog.sh

      - name: Vendor cargo dependencies
        run: |
          mkdir -p .cargo
          # cargo vendor prints a [source] block to stdout; merge into .cargo/config.toml
          # so the source package rebuilds with --offline against vendor/.
          cargo vendor vendor >> .cargo/config.toml
          echo "vendored $(find vendor -maxdepth 1 -mindepth 1 -type d | wc -l) crates"

      - name: Build source package
        run: |
          VERSION="$(awk -F'"' '
            /^\[workspace\.package\]/ { in_wp = 1; next }
            /^\[/                     { in_wp = 0 }
            in_wp && /^version[[:space:]]*=/ { print $2; exit }
          ' Cargo.toml)"
          test -n "$VERSION"

          # dpkg-source expects a directory named <pkg>-<upstream-ver>/ with
          # a sibling <pkg>_<upstream-ver>.orig.tar.gz. Stage a copy.
          STAGE=/tmp/build
          DIR="fresh-editor-${VERSION}"
          mkdir -p "$STAGE"
          # Copy everything except the obvious junk and the staging dir itself.
          # (cp -a inside the source tree to a target outside; .git is huge and unneeded.)
          rsync -a --delete \
            --exclude='.git/' \
            --exclude='target/' \
            --exclude='node_modules/' \
            --exclude='.worktrees/' \
            --exclude='perf.data*' \
            --exclude='flamegraph.svg' \
            ./ "$STAGE/$DIR/"

          # Build the orig tarball from the staged tree (excluding debian/,
          # which lives in the .debian.tar.xz component).
          tar --create --gzip \
              --owner=0 --group=0 \
              --exclude="$DIR/debian" \
              --file="$STAGE/fresh-editor_${VERSION}.orig.tar.gz" \
              -C "$STAGE" "$DIR"

          # Build the source package from inside the staged dir.
          cd "$STAGE/$DIR"
          dpkg-buildpackage -S -d -us -uc

          echo "=== source-package artifacts ==="
          ls -la "$STAGE/"

      - name: Verify source package
        run: |
          VERSION="$(awk -F'"' '
            /^\[workspace\.package\]/ { in_wp = 1; next }
            /^\[/                     { in_wp = 0 }
            in_wp && /^version[[:space:]]*=/ { print $2; exit }
          ' Cargo.toml)"

          DSC="/tmp/build/fresh-editor_${VERSION}-1.dsc"
          test -f "$DSC" || { echo "missing .dsc"; ls /tmp/build; exit 1; }

          # Round-trip: extract and ensure the package metadata parses.
          mkdir -p /tmp/verify
          dpkg-source -x "$DSC" /tmp/verify/extracted
          dpkg-parsechangelog -l /tmp/verify/extracted/debian/changelog

      - name: Collect source package artifacts
        run: |
          mkdir -p source-package
          VERSION="$(awk -F'"' '
            /^\[workspace\.package\]/ { in_wp = 1; next }
            /^\[/                     { in_wp = 0 }
            in_wp && /^version[[:space:]]*=/ { print $2; exit }
          ' Cargo.toml)"
          for ext in dsc orig.tar.gz debian.tar.xz; do
            find /tmp/build -maxdepth 1 -name "fresh-editor_${VERSION}*.${ext}" -exec cp {} source-package/ \;
          done
          find /tmp/build -maxdepth 1 -name "fresh-editor_${VERSION}*_source.changes" -exec cp {} source-package/ \;
          find /tmp/build -maxdepth 1 -name "fresh-editor_${VERSION}*_source.buildinfo" -exec cp {} source-package/ \;
          ls -la source-package/

      - name: Upload source package
        uses: actions/upload-artifact@v6
        with:
          name: artifacts-deb-source
          path: source-package/*

  # Final job that depends on all tests to ensure failures propagate to caller
  # This job will fail if any dependent job fails, causing the reusable workflow to fail
  linux-packages-complete:
    needs: [build-linux-packages, build-deb-source, test-deb-package, test-rpm-package, test-deb-upgrade, test-rpm-upgrade]
    if: ${{ !cancelled() }}
    runs-on: ubuntu-22.04
    steps:
      - name: Check all jobs succeeded
        if: ${{ failure() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
        run: |
          echo "One or more jobs failed:"
          echo "  build-linux-packages: ${{ needs.build-linux-packages.result }}"
          echo "  build-deb-source: ${{ needs.build-deb-source.result }}"
          echo "  test-deb-package: ${{ needs.test-deb-package.result }}"
          echo "  test-rpm-package: ${{ needs.test-rpm-package.result }}"
          echo "  test-deb-upgrade: ${{ needs.test-deb-upgrade.result }}"
          echo "  test-rpm-upgrade: ${{ needs.test-rpm-upgrade.result }}"
          exit 1
      - name: All jobs succeeded
        run: echo "All linux package jobs succeeded!"
