bssh 2.1.2

Parallel SSH command execution tool for cluster management
Documentation
# .github/workflows/launchpad_ppa.yml
name: Upload to Launchpad PPA

on:
  workflow_dispatch:
    inputs:
      release_tag:
        description: 'Release tag to upload to PPA (e.g. v0.5.0). Empty = latest release'
        required: false
      distributions:
        description: 'Target distributions (comma-separated: jammy,noble,resolute)'
        required: false
        default: 'jammy,noble,resolute'

permissions:
  contents: read

jobs:
  upload-ppa:
    name: Upload to PPA (${{ matrix.distro }})
    runs-on: ubuntu-latest
    environment: packaging
    strategy:
      fail-fast: false
      matrix:
        include:
          - distro: jammy
            vendor_rust: "1.92.0"
            toolchain_source: "Requires backend-ai PPA dependency on ~lablup/+archive/ubuntu/rustc-release"
          - distro: noble
            vendor_rust: "1.92.0"
            toolchain_source: "Requires backend-ai PPA dependency on ~lablup/+archive/ubuntu/rustc-release"
          - distro: resolute
            vendor_rust: "1.92.0"
            toolchain_source: "Uses the official Ubuntu 26.04 archive Rust 1.92+ toolchain"

    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Check distribution
      id: check_distro
      run: |
        DISTRIBUTIONS="${{ github.event.inputs.distributions }}"
        if [[ ",$DISTRIBUTIONS," == *",${{ matrix.distro }},"* ]]; then
          echo "should_run=true" >> "$GITHUB_OUTPUT"
        else
          echo "should_run=false" >> "$GITHUB_OUTPUT"
          echo "Skipping ${{ matrix.distro }} - not in requested distributions"
        fi

    - name: Get release tag
      if: steps.check_distro.outputs.should_run == 'true'
      id: get_tag
      run: |
        if [ -n "${{ github.event.inputs.release_tag }}" ]; then
          echo "tag=${{ github.event.inputs.release_tag }}" >> "$GITHUB_OUTPUT"
        else
          TAG=$(gh release list --limit 1 --json tagName -q '.[0].tagName')
          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
        fi
      env:
        GH_TOKEN: ${{ github.token }}

    - name: Preserve current packaging overlay
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        rm -rf /tmp/bssh-packaging
        mkdir -p /tmp/bssh-packaging
        cp -a debian /tmp/bssh-packaging/debian

    - name: Check out release tag
      if: steps.check_distro.outputs.should_run == 'true'
      uses: actions/checkout@v4
      with:
        ref: ${{ steps.get_tag.outputs.tag }}
        fetch-depth: 0

    - name: Restore current packaging overlay
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        rm -rf debian
        cp -a /tmp/bssh-packaging/debian debian

    - name: Describe Rust toolchain source
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        echo "Target distro: ${{ matrix.distro }}"
        echo "Vendoring toolchain: Rust ${{ matrix.vendor_rust }}"
        echo "Launchpad build toolchain: ${{ matrix.toolchain_source }}"

    - name: Update debian/changelog from release
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        sudo apt-get update && sudo apt-get install -y curl jq
        bash ./debian/update-changelog.sh \
          -d ${{ matrix.distro }} \
          -p lablup/backend-ai \
          --auto-increment \
          ${{ steps.get_tag.outputs.tag }}
      env:
        GH_TOKEN: ${{ github.token }}

    - name: Install build dependencies
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        sudo apt update
        sudo apt install -y \
          devscripts \
          debhelper \
          dh-make \
          fakeroot \
          dput \
          gpg \
          dpkg-dev \
          build-essential

    - name: Install Rust ${{ matrix.vendor_rust }} for vendoring
      if: steps.check_distro.outputs.should_run == 'true'
      uses: dtolnay/rust-toolchain@master
      with:
        toolchain: ${{ matrix.vendor_rust }}

    - name: Validate lockfile with Rust ${{ matrix.vendor_rust }}
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        cargo --version
        rustc --version
        cargo metadata --format-version 1 --locked --no-deps > /dev/null
        awk '/^version = /{print; found=1} END{ if(!found) exit 1 }' Cargo.lock

    - name: Vendor Rust dependencies for offline Launchpad build
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        rm -rf vendor .cargo
        mkdir -p .cargo
        cargo vendor --locked vendor > .cargo/config.toml
        python3 debian/sanitize-vendor.py
        python3 - <<'PY'
        import json
        from pathlib import Path, PurePosixPath

        offenders = []
        for checksum_file in Path("vendor").rglob(".cargo-checksum.json"):
            data = json.loads(checksum_file.read_text())
            for rel_path in data.get("files", {}):
                if rel_path.endswith(".orig") or any(
                    part.startswith(".") for part in PurePosixPath(rel_path).parts
                ):
                    offenders.append(f"{checksum_file}: {rel_path}")

        if offenders:
            raise SystemExit(
                "vendor checksum still references hidden/orig files:\n"
                + "\n".join(offenders[:50])
            )
        PY
        grep -q 'replace-with = "vendored-sources"' .cargo/config.toml
        grep -q '\[source.vendored-sources\]' .cargo/config.toml
        grep -q '^directory = "vendor"$' .cargo/config.toml
        test -d vendor
        ! find vendor -name '*.orig' -print | grep -q .

    - name: Prepare package version
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        VERSION="${{ steps.get_tag.outputs.tag }}"
        VERSION="${VERSION#v}"
        echo "PACKAGE_VERSION=${VERSION}" >> "$GITHUB_ENV"
        CHANGELOG_VERSION=$(head -n 1 debian/changelog | awk '{print $2}' | tr -d '()')
        echo "FULL_PACKAGE_VERSION=${CHANGELOG_VERSION}" >> "$GITHUB_ENV"
        echo "Package version: ${CHANGELOG_VERSION}"
        head -n 1 debian/changelog | grep -q "${VERSION}-" || {
          echo "Error: Changelog doesn't contain expected version ${VERSION}"
          exit 1
        }

    - name: Import GPG key
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import
        gpg --list-secret-keys --keyid-format LONG
        FINGERPRINT=$(gpg --list-secret-keys --with-colons | grep '^fpr:' | head -1 | cut -d: -f10)
        echo "GPG_FINGERPRINT=$FINGERPRINT" >> "$GITHUB_ENV"
        echo "${FINGERPRINT}:6:" | gpg --import-ownertrust

    - name: Configure GPG
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        export GNUPGHOME=/home/runner/.gnupg
        mkdir -p $GNUPGHOME
        chmod 700 $GNUPGHOME
        cat > $GNUPGHOME/gpg-agent.conf << 'EOF'
        allow-loopback-pinentry
        default-cache-ttl 300
        max-cache-ttl 3600
        EOF
        cat > $GNUPGHOME/gpg.conf << 'EOF'
        use-agent
        pinentry-mode loopback
        batch
        no-tty
        EOF
        if [ -n "${{ secrets.GPG_PASSPHRASE }}" ]; then
          echo "${{ secrets.GPG_PASSPHRASE }}" > $GNUPGHOME/passphrase
          chmod 600 $GNUPGHOME/passphrase
        fi
        gpgconf --kill gpg-agent || true

    - name: Build source package
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        rm -f bssh
        rm -rf target/
        test -d vendor
        test -f .cargo/config.toml
        sed -i 's/Architecture: .*/Architecture: any/' debian/control
        chmod +x debian/rules
        export GNUPGHOME=/home/runner/.gnupg

        if [ -n "${{ secrets.GPG_PASSPHRASE }}" ]; then
          echo "Building and signing source package with passphrase..."
          cat > /tmp/gpg-sign.sh << 'EOSCRIPT'
        #!/bin/bash
        echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 --pinentry-mode loopback "$@"
        EOSCRIPT
          chmod +x /tmp/gpg-sign.sh
          export GPG_PASSPHRASE="${{ secrets.GPG_PASSPHRASE }}"
          dpkg-buildpackage -S -sa -k"$GPG_FINGERPRINT" -d --sign-command="/tmp/gpg-sign.sh"
        else
          echo "Building and signing source package without passphrase..."
          dpkg-buildpackage -S -sa -k"$GPG_FINGERPRINT" -d
        fi
        echo "Source package built and signed successfully"

    - name: Upload to Ubuntu PPA
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        cat > ~/.dput.cf << 'EOF'
        [backend-ai-ppa]
        fqdn = ppa.launchpad.net
        method = ftp
        incoming = ~lablup/ubuntu/backend-ai/
        login = anonymous
        allow_unsigned_uploads = 0
        EOF
        CHANGES_FILE=$(ls ../*_source.changes 2>/dev/null | head -1)
        if [ -z "$CHANGES_FILE" ]; then
          echo "Error: No source .changes file found"
          exit 1
        fi
        echo "Uploading $CHANGES_FILE to PPA lablup/backend-ai"
        dput backend-ai-ppa "$CHANGES_FILE"
        echo "✅ Successfully uploaded to PPA"
        echo "📦 Version: $FULL_PACKAGE_VERSION"
        echo "📦 Package will be available at: https://launchpad.net/~lablup/+archive/ubuntu/backend-ai"