Build, test, and publish packages #11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================= | |
| # 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 }} |