bssh 2.0.1

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,oracular)'
        required: false
        default: 'jammy,noble'

permissions:
  contents: read

jobs:
  upload-ppa:
    name: Upload to PPA (${{ matrix.distro }})
    runs-on: ubuntu-latest
    environment: packaging
    strategy:
      fail-fast: false
      matrix:
        distro: [jammy, noble, oracular]

    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: Update debian/changelog from release
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        sudo apt-get update && sudo apt-get install -y curl
        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

    # ───────────────────────────────────────────────────────────────
    # NEW Step 5.5: Install Rust toolchain 1.75 (to produce lockfile v3)
    # ───────────────────────────────────────────────────────────────
    - name: Install Rust 1.75 (for lockfile v3 & vendor)
      if: steps.check_distro.outputs.should_run == 'true'
      uses: dtolnay/rust-toolchain@1.75.0

    # ───────────────────────────────────────────────────────────────
    # NEW Step 5.6: Generate Cargo.lock (v3) with Cargo 1.75
    # ───────────────────────────────────────────────────────────────
    - name: Generate lockfile v3 (Cargo 1.75)
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        # Ensure we create a lockfile compatible with jammy's cargo (v3)
        rm -f Cargo.lock
        cargo generate-lockfile
        # Safety check: lockfile must be version = 3 (1.75 writes v3)
        awk '/^version = /{print; found=1} END{ if(!found) exit 1 }' Cargo.lock
        if ! grep -q '^version = 3' Cargo.lock; then
          echo "ERROR: Cargo.lock is not v3 (jammy-compatible)."
          exit 1
        fi

    # ───────────────────────────────────────────────────────────────
    # NEW Step 5.7: Vendor dependencies & write .cargo/config.toml
    # ───────────────────────────────────────────────────────────────
    - name: Vendor Rust dependencies (offline build on Launchpad)
      if: steps.check_distro.outputs.should_run == 'true'
      run: |
        mkdir -p .cargo
        # Create vendor directory and config
        cargo vendor vendor > .cargo/config.toml
        # Ensure config has replace-with vendored-sources
        grep -q 'replace-with = "vendored-sources"' .cargo/config.toml
        grep -q '\[source.vendored-sources\]' .cargo/config.toml
        grep -q '^directory = "vendor"$' .cargo/config.toml
        # Quick sanity: no network is needed for build now
        echo "Vendoring done. Files:"
        ls -lah vendor | head -n 50

    - 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: |
        # Clean any build artifacts
        rm -f bssh
        rm -rf target/
        # Ensure Architecture field is 'any'
        sed -i 's/Architecture: .*/Architecture: any/' debian/control
        chmod +x debian/rules
        export GNUPGHOME=/home/runner/.gnupg

        # IMPORTANT: vendor/ and .cargo/config.toml are already present in the tree
        # dpkg-buildpackage -S -sa will include them into the source tarball

        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"