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"