Skip to content

Build, test, and publish packages #11

Build, test, and publish packages

Build, test, and publish packages #11

Workflow file for this run

# =============================================================================
# Unified PyPI/NPM Release Workflow
# =============================================================================
#
# This workflow builds, tests, and publishes all llama-stack packages:
# - llama-stack (PyPI)
# - llama-stack-api (PyPI)
# - llama-stack-client-python (PyPI, from external repo)
# - llama-stack-client-typescript (npm, from external repo)
#
# =============================================================================
# REQUIRED SECRETS
# =============================================================================
#
# 1. NPM_TOKEN - npm Access Token
# Purpose: Publish llama-stack-client package to npmjs.org (production only)
#
# How to create:
# - Log in to https://www.npmjs.com/
# - Profile > Access Tokens > Generate New Token > Classic Token
# - Select type: Automation
#
# Add to repo: Settings > Secrets and variables > Actions > New repository secret
# Name: NPM_TOKEN
#
# Note: Token owner must have publish access to llama-stack-client on npm
#
# 2. DOCKERHUB_USERNAME - DockerHub username
# Purpose: Authenticate with DockerHub to push distribution images
#
# 3. DOCKERHUB_TOKEN - DockerHub access token
# Purpose: Authenticate with DockerHub to push distribution images
#
# How to create:
# - Log in to https://hub.docker.com/
# - Account Settings > Security > New Access Token
# - Select access permission: Read & Write
#
# Add to repo: Settings > Secrets and variables > Actions > New repository secret
# Names: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN
#
# =============================================================================
# WORKFLOW INPUTS (workflow_dispatch)
# =============================================================================
#
# dry_run:
# - test-pypi: Publish to test.pypi.org, npm pack only (default)
# - build-only: Build and validate only, no publishing
# - off: Production publish to pypi.org and npmjs.org
#
# version: Optional version override (e.g., "0.2.0rc1")
#
# packages:
# - all: Build all packages
# - llama-stack-only: Build only llama-stack and llama-stack-api
# - clients-only: Build only client packages
#
# client_ref: Git ref for client repos. Auto-detected from branch:
# - On release-X.Y.x branches or vX.Y.Z tags: uses matching release-X.Y.x
# - Otherwise: defaults to main
# - Explicit value overrides auto-detection
#
# docker_only: Skip package build/test/publish; only build and push Docker
# images. Use this to retry Docker publishing after a run where packages
# published successfully but image builds failed. Requires "version" to be
# set to the already-published version.
#
# =============================================================================
name: Build, test, and publish packages
on:
push:
branches:
- main
- "release-**"
tags:
- "v*"
pull_request:
branches:
- main
- "release-**"
release:
types:
- published
schedule:
# Nightly at midnight UTC - for test.pypi.org publishing
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run mode'
required: false
type: choice
options:
- 'test-pypi' # Publish to test.pypi.org (default for testing)
- 'build-only' # Build and validate only, no publishing anywhere
- 'off' # Production publish (use with caution!)
default: 'test-pypi'
version:
description: 'Version override (e.g., "0.2.0rc1"). Leave empty for auto-detection.'
required: false
type: string
packages:
description: 'Which packages to build/publish'
required: false
type: choice
options:
- all
- llama-stack-only
- clients-only
default: 'all'
client_ref:
description: 'Git ref for client repos (branch/tag/sha). Default: main'
required: false
type: string
default: 'main'
docker_only:
description: 'Skip package build/publish; only build and push Docker images (requires version)'
required: false
type: boolean
default: false
env:
LC_ALL: en_US.UTF-8
defaults:
run:
shell: bash
permissions:
contents: read
jobs:
# Compute version once, shared by all build jobs.
# This ensures external packages (client-python, client-typescript) get the
# same version as local packages during nightly/manual runs.
compute-version:
name: Compute version
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout local repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Compute package version
id: version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
elif [ "${{ github.event_name }}" == "release" ]; then
VERSION="${GITHUB_REF#refs/tags/v}"
else
# Use git describe to get version from nearest tag (e.g., v0.5.1-dev)
# Falls back to fallback_version in pyproject.toml if no tags are reachable
RAW=$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || echo "")
if [ -n "$RAW" ]; then
BASE="${RAW#v}" # strip 'v' prefix
BASE="${BASE%-dev}" # strip any -dev suffix from the tag
else
# Fallback: read from pyproject.toml (e.g., source tarballs without git history)
BASE=$(python3 -c "
import tomllib, pathlib
p = tomllib.loads(pathlib.Path('pyproject.toml').read_text())
v = p.get('tool', {}).get('setuptools_scm', {}).get('fallback_version', '0.0.0.dev0')
print(v.split('.dev')[0])
")
fi
DATE=$(date -u +%Y%m%d)
VERSION="${BASE}.dev${DATE}"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Computed version: ${VERSION}"
# Build and validate release artifacts
build-package:
name: Build ${{ matrix.package }}
if: inputs.docker_only != true
needs: compute-version
permissions:
contents: read
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# Local packages (in this repo)
- package: llama-stack-api
path: src/llama_stack_api
type: local
registry: pypi
- package: llama-stack
path: .
type: local
registry: pypi
# External packages (client SDKs from other repos)
- package: llama-stack-client-python
repo: llamastack/llama-stack-client-python
type: external
registry: pypi
- package: llama-stack-client-typescript
repo: llamastack/llama-stack-client-typescript
type: external
registry: npm
steps:
# Skip check for package selection
- name: Check if package should be built
id: should-build
run: |
PACKAGES="${{ inputs.packages || 'all' }}"
PACKAGE="${{ matrix.package }}"
TYPE="${{ matrix.type }}"
if [ "$PACKAGES" == "all" ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
elif [ "$PACKAGES" == "llama-stack-only" ] && [ "$TYPE" == "local" ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
elif [ "$PACKAGES" == "clients-only" ] && [ "$TYPE" == "external" ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
else
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping $PACKAGE (packages=$PACKAGES)"
fi
# === LOCAL PACKAGE STEPS ===
- name: Checkout local repo
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'local'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0 # for setuptools-scm
- name: Install dependent PRs if needed
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'local'
uses: depends-on/depends-on-action@826c144163ac67bf08347590a5f81afd45da63ca # main
with:
token: ${{ secrets.GITHUB_TOKEN }}
# === DETERMINE CLIENT REF ===
# Use release branch from client repos when running on a release branch,
# unless client_ref is explicitly provided via workflow_dispatch.
- name: Determine client ref
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'external'
id: client-ref
run: |
if [ -n "${{ inputs.client_ref }}" ] && [ "${{ inputs.client_ref }}" != "main" ]; then
# Explicit override from workflow_dispatch
echo "ref=${{ inputs.client_ref }}" >> "$GITHUB_OUTPUT"
echo "Using explicit client_ref: ${{ inputs.client_ref }}"
elif [[ "$GITHUB_REF" == refs/heads/release-* ]]; then
# Running on a release branch — use the same branch name in client repos
BRANCH="${GITHUB_REF#refs/heads/}"
echo "ref=$BRANCH" >> "$GITHUB_OUTPUT"
echo "Using release branch: $BRANCH"
elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then
# Running from a version tag — derive release branch (e.g., v0.5.1 -> release-0.5.x)
TAG="${GITHUB_REF#refs/tags/v}"
MAJOR_MINOR="${TAG%.*}"
BRANCH="release-${MAJOR_MINOR}.x"
echo "ref=$BRANCH" >> "$GITHUB_OUTPUT"
echo "Using derived release branch from tag: $BRANCH"
else
echo "ref=main" >> "$GITHUB_OUTPUT"
echo "Using default: main"
fi
# === EXTERNAL PACKAGE STEPS ===
- name: Checkout external repo
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'external'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ matrix.repo }}
ref: ${{ steps.client-ref.outputs.ref }}
path: external-repo
fetch-depth: 0
# === PYTHON SETUP (for all Python packages) ===
- name: Set up Python
if: steps.should-build.outputs.skip != 'true' && matrix.registry == 'pypi'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Install uv
if: steps.should-build.outputs.skip != 'true' && matrix.registry == 'pypi'
uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0
- name: Install build dependencies
if: steps.should-build.outputs.skip != 'true' && matrix.registry == 'pypi'
run: uv pip install --system setuptools setuptools-scm wheel build
# === NODE SETUP (for npm packages) ===
- name: Set up Node.js
if: steps.should-build.outputs.skip != 'true' && matrix.registry == 'npm'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
# === LOCAL PYTHON PACKAGE BUILD ===
- name: Check for missing package entries (llama-stack-api)
if: steps.should-build.outputs.skip != 'true' && matrix.package == 'llama-stack-api'
working-directory: src/llama_stack_api
run: |
for f in *.py; do
[[ "$f" == "__init__.py" ]] && continue
grep -q "llama_stack_api.${f%.py}" pyproject.toml || echo "::warning::Missing from py-modules: ${f%.py}"
done
for d in */; do
[[ "$d" =~ ^(__pycache__|dist|build|.*egg-info|\..*)/$ ]] && continue
grep -q "llama_stack_api.${d%/}" pyproject.toml || echo "::warning::Missing from packages: ${d%/}"
done
- name: Build local Python package
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'local' && matrix.registry == 'pypi'
run: uv build --out-dir dist --no-build-isolation
working-directory: ${{ matrix.path }}
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ needs.compute-version.outputs.version }}
# === EXTERNAL PYTHON PACKAGE BUILD ===
- name: Build external Python package
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'external' && matrix.registry == 'pypi'
working-directory: external-repo
run: |
VERSION="${{ needs.compute-version.outputs.version }}"
echo "Setting version to $VERSION"
sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
# Also update __version__ in _version.py if it exists
VERSION_FILE=$(find . -name "_version.py" -path "*/llama_stack_client/*" | head -1)
if [ -n "$VERSION_FILE" ]; then
sed -i "s/__version__ = .*/__version__ = \"$VERSION\"/" "$VERSION_FILE"
fi
# Use python -m build for better compatibility
uv pip install --system build
python -m build --outdir dist
# === NPM PACKAGE BUILD ===
- name: Build TypeScript package
if: steps.should-build.outputs.skip != 'true' && matrix.registry == 'npm'
working-directory: external-repo
run: |
VERSION="${{ needs.compute-version.outputs.version }}"
# Convert PEP 440 dev version to valid semver for npm
# e.g. 0.4.5.dev20260202 -> 0.4.5-dev.20260202
NPM_VERSION="${VERSION/.dev/-dev.}"
echo "Setting version to $NPM_VERSION (from $VERSION)"
npm version "$NPM_VERSION" --no-git-tag-version
npm install
npm run build
# === PYTHON PACKAGE VALIDATION ===
- name: Install validation tools
if: steps.should-build.outputs.skip != 'true' && matrix.registry == 'pypi'
run: uv pip install --system twine check-wheel-contents
- name: Check wheel contents (local)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'local' && matrix.registry == 'pypi'
run: check-wheel-contents --ignore W002,W004 ${{ matrix.path }}/dist/*.whl
- name: Check wheel contents (external)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'external' && matrix.registry == 'pypi'
run: check-wheel-contents --ignore W002,W004 external-repo/dist/*.whl
- name: Validate package with twine (local)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'local' && matrix.registry == 'pypi'
run: twine check ${{ matrix.path }}/dist/*
- name: Validate package with twine (external)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'external' && matrix.registry == 'pypi'
run: twine check external-repo/dist/*
# === LIST AND UPLOAD ARTIFACTS ===
- name: List dist contents (local)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'local'
run: ls -la ${{ matrix.path }}/dist/
- name: List dist contents (external Python)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'external' && matrix.registry == 'pypi'
run: ls -la external-repo/dist/
- name: List package contents (external npm)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'external' && matrix.registry == 'npm'
working-directory: external-repo
run: |
npm pack --dry-run
mkdir -p dist
npm pack --pack-destination dist
ls -la dist/
- name: Upload artifacts (local)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'local'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Packages-${{ matrix.package }}
path: ${{ matrix.path }}/dist/*
- name: Upload artifacts (external)
if: steps.should-build.outputs.skip != 'true' && matrix.type == 'external'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Packages-${{ matrix.package }}
path: external-repo/dist/*
# Functional tests - install and verify packages work
test-package:
name: Test packages (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
needs: build-package
strategy:
matrix:
python-version: ['3.12', '3.13']
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0
with:
python-version: ${{ matrix.python-version }}
- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20'
- name: Download llama-stack-api artifacts
id: download-api
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: Packages-llama-stack-api
path: dist-api
continue-on-error: true
- name: Download llama-stack artifacts
id: download-stack
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: Packages-llama-stack
path: dist-stack
continue-on-error: true
- name: Download llama-stack-client-python artifacts
id: download-client-python
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: Packages-llama-stack-client-python
path: dist-client-python
continue-on-error: true
- name: Download llama-stack-client-typescript artifacts
id: download-client-ts
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: Packages-llama-stack-client-typescript
path: dist-client-ts
continue-on-error: true
- name: Create venv and install Python packages
run: |
uv venv .venv
source .venv/bin/activate
# Install available Python packages
if [ -d "dist-api" ] && ls dist-api/*.whl 1>/dev/null 2>&1; then
echo "Installing llama-stack-api..."
uv pip install dist-api/*.whl
fi
if [ -d "dist-client-python" ] && ls dist-client-python/*.whl 1>/dev/null 2>&1; then
echo "Installing llama-stack-client..."
uv pip install dist-client-python/*.whl
fi
if [ -d "dist-stack" ] && ls dist-stack/*.whl 1>/dev/null 2>&1; then
echo "Installing llama-stack..."
uv pip install dist-stack/*.whl
fi
- name: List Wheel Contents (llama-stack-api)
if: steps.download-api.outcome == 'success'
run: |
source .venv/bin/activate
python -m zipfile -l dist-api/*.whl
- name: Verify Llama Stack package
if: steps.download-stack.outcome == 'success'
run: |
source .venv/bin/activate
uv pip list
uv pip show llama-stack
command -v llama
llama stack list-apis
llama stack list-providers inference
llama stack list-deps starter
- name: Verify packages are importable
run: |
source .venv/bin/activate
if [ -d "dist-stack" ] && ls dist-stack/*.whl 1>/dev/null 2>&1; then
python -c "import llama_stack; print(f'llama_stack imported successfully from {llama_stack.__file__}')"
fi
if [ -d "dist-api" ] && ls dist-api/*.whl 1>/dev/null 2>&1; then
python -c "import llama_stack_api; print(f'llama_stack_api imported successfully from {llama_stack_api.__file__}')"
fi
if [ -d "dist-client-python" ] && ls dist-client-python/*.whl 1>/dev/null 2>&1; then
python -c "import llama_stack_client; print(f'llama_stack_client imported successfully from {llama_stack_client.__file__}')"
fi
- name: Verify TypeScript package
if: steps.download-client-ts.outcome == 'success'
run: |
if [ -d "dist-client-ts" ] && ls dist-client-ts/*.tgz 1>/dev/null 2>&1; then
echo "TypeScript package tarball found:"
ls -la dist-client-ts/*.tgz
# Create a test directory and install the package
mkdir -p ts-test
cd ts-test
npm init -y
npm install ../dist-client-ts/*.tgz
echo "TypeScript package installed successfully"
# Verify the package is importable
node -e "const pkg = require('llama-stack-client'); console.log('llama-stack-client package loaded successfully');" || echo "Package may use ES modules, skipping require test"
fi
# Publish packages to PyPI/npm
# Order: llama-stack-client-python, llama-stack-client-typescript, llama-stack-api, llama-stack
publish-packages:
name: Publish ${{ matrix.package }}
if: |
github.repository_owner == 'llamastack' &&
(inputs.dry_run || 'test-pypi') != 'build-only' && (
github.event_name == 'workflow_dispatch' ||
github.event.action == 'published' ||
github.event_name == 'schedule'
)
permissions:
contents: write # for gh release upload
id-token: write # for PyPI trusted publishing
runs-on: ubuntu-latest
needs: test-package
strategy:
max-parallel: 1
matrix:
include:
# Order matters! Dependencies are published first
- package: llama-stack-client-python
registry: pypi
type: external
- package: llama-stack-client-typescript
registry: npm
type: external
- package: llama-stack-api
registry: pypi
type: local
- package: llama-stack
registry: pypi
type: local
steps:
# Skip check for package selection
- name: Check if package should be published
id: should-publish
run: |
PACKAGES="${{ inputs.packages || 'all' }}"
PACKAGE="${{ matrix.package }}"
TYPE="${{ matrix.type }}"
if [ "$PACKAGES" == "all" ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
elif [ "$PACKAGES" == "llama-stack-only" ] && [ "$TYPE" == "local" ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
elif [ "$PACKAGES" == "clients-only" ] && [ "$TYPE" == "external" ]; then
echo "skip=false" >> "$GITHUB_OUTPUT"
else
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping publish for $PACKAGE (packages=$PACKAGES)"
fi
- name: Download build artifacts
if: steps.should-publish.outputs.skip != 'true'
id: download
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: Packages-${{ matrix.package }}
path: dist
continue-on-error: true
- name: Check if artifacts exist
if: steps.should-publish.outputs.skip != 'true'
id: check-artifacts
run: |
if [ -d "dist" ] && [ "$(ls -A dist 2>/dev/null)" ]; then
echo "has_artifacts=true" >> "$GITHUB_OUTPUT"
echo "Artifacts found:"
ls -la dist/
else
echo "has_artifacts=false" >> "$GITHUB_OUTPUT"
echo "::warning::No artifacts found for ${{ matrix.package }}. Skipping publish."
fi
# === PYPI PUBLISHING ===
- name: Determine PyPI target
if: steps.should-publish.outputs.skip != 'true' && matrix.registry == 'pypi' && steps.check-artifacts.outputs.has_artifacts == 'true'
id: pypi-target
run: |
DRY_RUN="${{ inputs.dry_run }}"
EVENT="${{ github.event_name }}"
# Determine target registry
if [ "$EVENT" == "release" ]; then
{
echo "url=https://upload.pypi.org/legacy/"
echo "target=pypi.org"
echo "is_production=true"
} >> "$GITHUB_OUTPUT"
elif [ "$DRY_RUN" == "off" ]; then
{
echo "url=https://upload.pypi.org/legacy/"
echo "target=pypi.org"
echo "is_production=true"
} >> "$GITHUB_OUTPUT"
else
{
echo "url=https://test.pypi.org/legacy/"
echo "target=test.pypi.org"
echo "is_production=false"
} >> "$GITHUB_OUTPUT"
fi
- name: Sign artifacts with Sigstore (production only)
if: |
steps.should-publish.outputs.skip != 'true' &&
matrix.registry == 'pypi' &&
steps.check-artifacts.outputs.has_artifacts == 'true' &&
steps.pypi-target.outputs.is_production == 'true'
uses: sigstore/gh-action-sigstore-python@a5caf349bc536fbef3668a10ed7f5cd309a4b53d # v3.2.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
release-signing-artifacts: false
- name: Upload artifacts to GitHub release (production only)
if: |
steps.should-publish.outputs.skip != 'true' &&
matrix.registry == 'pypi' &&
steps.check-artifacts.outputs.has_artifacts == 'true' &&
steps.pypi-target.outputs.is_production == 'true' &&
github.event_name == 'release'
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
gh release upload '${{ github.ref_name }}' dist/* --repo '${{ github.repository }}' || true
- name: Remove sigstore signatures before PyPI upload
if: |
steps.should-publish.outputs.skip != 'true' &&
matrix.registry == 'pypi' &&
steps.check-artifacts.outputs.has_artifacts == 'true' &&
steps.pypi-target.outputs.is_production == 'true'
run: rm -f ./dist/*.sigstore.json
- name: Upload to PyPI
if: steps.should-publish.outputs.skip != 'true' && matrix.registry == 'pypi' && steps.check-artifacts.outputs.has_artifacts == 'true'
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
repository-url: ${{ steps.pypi-target.outputs.url }}
verbose: true
# === NPM PUBLISHING ===
- name: Set up Node.js
if: steps.should-publish.outputs.skip != 'true' && matrix.registry == 'npm' && steps.check-artifacts.outputs.has_artifacts == 'true'
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Publish to npm
if: steps.should-publish.outputs.skip != 'true' && matrix.registry == 'npm' && steps.check-artifacts.outputs.has_artifacts == 'true'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
DRY_RUN="${{ inputs.dry_run }}"
EVENT="${{ github.event_name }}"
echo "Event: $EVENT"
echo "Dry run: $DRY_RUN"
# Check if we should do production publish
if [ "$EVENT" == "release" ]; then
IS_PRODUCTION=true
elif [ "$DRY_RUN" == "off" ]; then
IS_PRODUCTION=true
else
IS_PRODUCTION=false
fi
if [ "$IS_PRODUCTION" == "true" ]; then
echo "PRODUCTION: Publishing to npm"
if [ -z "$NODE_AUTH_TOKEN" ]; then
echo "::error::NPM_TOKEN secret not configured. Cannot publish to npm."
exit 1
fi
# Extract and publish from the tarball
TARBALL=$(find ./dist -name '*.tgz' | head -1)
npm publish "$TARBALL" --access public
else
echo "DRY RUN: Verifying npm package (not publishing)"
echo "Package tarball:"
find dist -name '*.tgz' -exec ls -la {} +
# Verify the tarball is valid
TARBALL=$(find ./dist -name '*.tgz' | head -1)
tar -tzf "$TARBALL" 2>/dev/null | head -20 || true
echo "... (truncated)"
fi
# ==========================================================================
# Docker Image Publishing
# ==========================================================================
# Builds and pushes distribution Docker images to DockerHub after packages
# are published to PyPI. CPU distros are built for linux/amd64 and
# linux/arm64. GPU distros are built for linux/amd64 only.
#
# Images are pushed to: llamastack/distribution-<distro>:<tag>
# - Production (release or dry_run=off): tagged with VERSION and "latest"
# - Test (test-pypi or schedule): tagged with "test-VERSION"
publish-docker-images:
name: Publish Docker ${{ matrix.distro }}
if: |
always() &&
needs.compute-version.result == 'success' &&
(needs.publish-packages.result == 'success' || needs.publish-packages.result == 'skipped') &&
github.repository_owner == 'llamastack' &&
(inputs.dry_run || 'test-pypi') != 'build-only' &&
(inputs.packages || 'all') != 'clients-only' && (
github.event_name == 'workflow_dispatch' ||
github.event.action == 'published' ||
github.event_name == 'schedule'
)
permissions:
contents: read
runs-on: ubuntu-latest
needs: [publish-packages, compute-version]
strategy:
fail-fast: false
matrix:
include:
# CPU distributions — multi-arch (amd64 + arm64)
- distro: starter
platforms: linux/amd64,linux/arm64
- distro: postgres-demo
platforms: linux/amd64,linux/arm64
- distro: dell
platforms: linux/amd64,linux/arm64
# GPU distributions — amd64 only
- distro: starter-gpu
platforms: linux/amd64
steps:
- name: Free disk space
run: |
echo "Disk space before cleanup:"
df -h
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
docker system prune -af --volumes
echo "Disk space after cleanup:"
df -h
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU for multi-arch builds
if: contains(matrix.platforms, 'arm64')
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
- name: Log in to DockerHub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine image tags and build args
id: meta
run: |
VERSION="${{ needs.compute-version.outputs.version }}"
DISTRO="${{ matrix.distro }}"
IMAGE="llamastack/distribution-${DISTRO}"
DRY_RUN="${{ inputs.dry_run }}"
EVENT="${{ github.event_name }}"
if [ "$EVENT" == "release" ] || [ "$DRY_RUN" == "off" ]; then
{
echo "install_mode=pypi"
echo "tags=${IMAGE}:${VERSION},${IMAGE}:latest"
echo "version_arg=PYPI_VERSION=${VERSION}"
} >> "$GITHUB_OUTPUT"
echo "Publishing production image: ${IMAGE}:${VERSION} + latest"
else
{
echo "install_mode=test-pypi"
echo "tags=${IMAGE}:test-${VERSION}"
echo "version_arg=TEST_PYPI_VERSION=${VERSION}"
} >> "$GITHUB_OUTPUT"
echo "Publishing test image: ${IMAGE}:test-${VERSION}"
fi
- name: Wait for package on test PyPI
if: steps.meta.outputs.install_mode == 'test-pypi'
run: |
VERSION="${{ needs.compute-version.outputs.version }}"
URL="https://test.pypi.org/pypi/llama-stack/${VERSION}/json"
echo "Polling ${URL} ..."
for i in $(seq 1 20); do
if curl -sf "$URL" -o /dev/null; then
echo "llama-stack==${VERSION} is available on test PyPI"
exit 0
fi
echo "Attempt ${i}/20: not yet available, waiting 30s..."
sleep 30
done
echo "::error::llama-stack==${VERSION} did not appear on test PyPI after 10 minutes"
exit 1
- name: Build and push Docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
file: containers/Containerfile
platforms: ${{ matrix.platforms }}
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
DISTRO_NAME=${{ matrix.distro }}
INSTALL_MODE=${{ steps.meta.outputs.install_mode }}
${{ steps.meta.outputs.version_arg }}