Skip to content

v1.8.0

v1.8.0 #19

Workflow file for this run

name: Release Pipeline
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 1.0.0)'
required: true
type: string
permissions:
contents: read
packages: write
pages: write
concurrency:
group: production-release-${{ github.event.release.tag_name || inputs.version }}
cancel-in-progress: false
jobs:
config:
name: Get Configuration
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
python-versions: ${{ steps.config.outputs.python-versions }}
default-python-version: ${{ steps.config.outputs.default-python-version }}
container-registry: ${{ steps.config.outputs.container-registry }}
container-image: ${{ steps.config.outputs.container-image }}
package-version: ${{ steps.config.outputs.package-version }}
is-release: ${{ steps.config.outputs.is-release }}
pypi-name: ${{ steps.config.outputs.pypi-name }}
build-platforms: ${{ steps.config.outputs.build-platforms }}
short-name: ${{ steps.config.outputs.short-name }}
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Get project configuration
id: config
uses: ./.github/actions/get-config
# Build the wheel and UI bundle exactly once so prod-containers, prod-pypi,
# and github-assets all publish the identical artefact.
build-wheel:
name: Build Release Wheel
needs: config
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Python and UV
uses: ./.github/actions/setup-uv-cached
with:
cache-key: prod-wheel-${{ needs.config.outputs.package-version }}
fail-on-cache-miss: false
- name: Setup Bun (for UI static bundle)
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.2.15" # pinned; update intentionally to avoid surprise breakage
# The SPA bundle is built automatically by the setuptools build hook during
# `make build` (python -m build). No separate step needed.
- name: Build package wheel
run: make build
- name: Upload wheel artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: release-wheel-${{ needs.config.outputs.package-version }}
path: dist/
retention-days: 1
prod-containers:
name: Build Production Containers
needs: [config, build-wheel]
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix:
python-version: ${{ fromJSON(needs.config.outputs.python-versions) }}
env:
SCAN_LEVEL: ${{ matrix.python-version == needs.config.outputs.default-python-version && 'full' || 'basic' }}
PUSH_TO_REGISTRY: true
permissions:
contents: read
packages: write
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
with:
platforms: ${{ needs.config.outputs.build-platforms }}
- name: Log in to Container Registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ needs.config.outputs.container-registry }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download release wheel
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-wheel-${{ needs.config.outputs.package-version }}
path: dist/
- name: Build and push container image
env:
REGISTRY: ${{ needs.config.outputs.container-registry }}
IMAGE_NAME: ${{ needs.config.outputs.container-image }}
VERSION: ${{ needs.config.outputs.package-version }}
PYTHON_VERSION: ${{ matrix.python-version }}
BUILD_PLATFORMS: ${{ needs.config.outputs.build-platforms }}
PACKAGE_NAME_SHORT: ${{ needs.config.outputs.short-name }}
run: |
docker buildx build \
--platform "$BUILD_PLATFORMS" \
--build-arg "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
--build-arg "VERSION=$VERSION" \
--build-arg "VCS_REF=$(git rev-parse --short HEAD)" \
--build-arg "PYTHON_VERSION=$PYTHON_VERSION" \
--build-arg "PACKAGE_NAME_SHORT=$PACKAGE_NAME_SHORT" \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--push \
-t "$REGISTRY/$IMAGE_NAME:$VERSION-python$PYTHON_VERSION" \
.
prod-manifests:
name: Create Production Manifests
needs: [config, prod-containers]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Log in to Container Registry
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ needs.config.outputs.container-registry }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create production tag manifests
shell: bash
env:
REGISTRY: ${{ needs.config.outputs.container-registry }}
IMAGE: ${{ needs.config.outputs.container-image }}
VERSION: ${{ needs.config.outputs.package-version }}
DEFAULT_PYTHON: ${{ needs.config.outputs.default-python-version }}
IS_RELEASE: "true"
run: |
# Parse Makefile container tag output into shell variables in the same step.
# GITHUB_ENV writes are only visible to SUBSEQUENT steps, so we assign
# directly into the current shell environment instead.
# get-container-tags-env emits "export KEY=VALUE" lines; strip the prefix and
# validate the KEY=VALUE format before assigning (rejects subcommands etc.).
while IFS= read -r line; do
# Strip leading "export " if present
kv="${line#export }"
# Validate KEY=VALUE format before assigning (reject lines with subcommands etc.)
if [[ "$kv" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
declare "${BASH_REMATCH[1]}=${BASH_REMATCH[2]}"
fi
done < <(make get-container-tags-env)
# Create primary tags (latest, version)
if [[ -n "${primary_tags:-}" ]]; then
IFS=',' read -ra TAGS <<< "$primary_tags"
for tag in "${TAGS[@]}"; do
echo "Creating primary tag: $tag -> $VERSION-python$DEFAULT_PYTHON"
docker buildx imagetools create \
-t "$REGISTRY/$IMAGE:$tag" \
"$REGISTRY/$IMAGE:$VERSION-python$DEFAULT_PYTHON"
done
fi
# Create Python variant tags
if [[ -n "${python_tags:-}" ]]; then
IFS=',' read -ra TAGS <<< "$python_tags"
for tag in "${TAGS[@]}"; do
py_ver=${tag#python}
echo "Creating Python variant tag: $tag -> $VERSION-python$py_ver"
docker buildx imagetools create \
-t "$REGISTRY/$IMAGE:$tag" \
"$REGISTRY/$IMAGE:$VERSION-python$py_ver"
done
fi
prod-pypi:
name: Publish to PyPI
needs: [config, build-wheel]
runs-on: ubuntu-latest
timeout-minutes: 30
environment: pypi
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Python and UV
uses: ./.github/actions/setup-uv-cached
with:
cache-key: prod-pypi-${{ needs.config.outputs.package-version }}
fail-on-cache-miss: false
- name: Download release wheel
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-wheel-${{ needs.config.outputs.package-version }}
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
with:
print-hash: true
verbose: true
attestations: true
prod-docs:
name: Deploy Production Documentation
needs: config
runs-on: ubuntu-latest
timeout-minutes: 20
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
contents: write
pages: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Setup Python and UV
uses: ./.github/actions/setup-uv-cached
with:
cache-key: prod-docs-${{ needs.config.outputs.package-version }}
fail-on-cache-miss: false
- name: Compute minor version
id: version
run: |
VERSION_MINOR=$(echo "${{ needs.config.outputs.package-version }}" | cut -d. -f1,2)
echo "minor=${VERSION_MINOR}" >> "$GITHUB_OUTPUT"
- name: Deploy versioned docs via mike
run: bash dev-tools/docs/mike_deploy.sh release "${{ steps.version.outputs.minor }}" --push
- name: Upload Pages artifact
id: upload
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: docs/site
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
- name: Smoke-check deployed site
run: bash dev-tools/docs/smoke_check_pages.sh "${{ steps.deployment.outputs.page_url }}"
container-cleanup:
name: Cleanup Old Container Images
needs: [config, prod-manifests]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
packages: write
steps:
- name: Delete untagged container images
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5
with:
package-name: ${{ github.event.repository.name }}
package-type: container
min-versions-to-keep: 0
delete-only-untagged-versions: true
github-assets:
name: Upload GitHub Release Assets
needs: [config, build-wheel, prod-containers, prod-pypi]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Python and UV
uses: ./.github/actions/setup-uv-cached
with:
cache-key: prod-assets-${{ needs.config.outputs.package-version }}
fail-on-cache-miss: false
- name: Download release wheel
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-wheel-${{ needs.config.outputs.package-version }}
path: dist/
- name: Generate SBOM
run: make sbom-generate
- name: Upload release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.config.outputs.package-version }}"
RELEASE_TAG="v$VERSION"
gh release upload "$RELEASE_TAG" dist/*.whl dist/*.tar.gz --clobber
for f in python-sbom-cyclonedx.json python-sbom.json; do
if [[ -f "$f" ]]; then
gh release upload "$RELEASE_TAG" "$f" --clobber
fi
done