Skip to content

Release

Release #34

Workflow file for this run

name: Release
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Release version, with or without the leading v (example: 0.2.2)"
required: true
type: string
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
test:
name: Test Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install
run: |
python -m pip install -U pip
python -m pip install --no-cache-dir -e ".[dev]"
- name: Pytest
env:
ABSTRACT_E2E_GATEWAY_SPLIT: "0"
ABSTRACT_E2E_LMSTUDIO: "0"
ABSTRACTGATEWAY_EMBEDDING_PROVIDER: "disabled"
run: python -m pytest
docs:
name: Build docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install
run: |
python -m pip install -U pip
python -m pip install --no-cache-dir -e ".[docs]"
- name: MkDocs build
run: mkdocs build -q
build:
name: Build distributions
runs-on: ubuntu-latest
needs: [test, docs]
outputs:
version: ${{ steps.release-meta.outputs.version }}
tag: ${{ steps.release-meta.outputs.tag }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install build tools
run: |
python -m pip install -U pip
python -m pip install build twine
- name: Validate tag, version, and changelog
id: release-meta
env:
RELEASE_INPUT_VERSION: ${{ github.event.inputs.version || '' }}
run: |
python - <<'PY'
import os
import re
import sys
from pathlib import Path
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
version_input = os.environ.get("RELEASE_INPUT_VERSION", "").strip()
ref_name = os.environ["GITHUB_REF_NAME"].strip()
raw_version = version_input or ref_name
version = raw_version[1:] if raw_version.startswith("v") else raw_version
if not re.fullmatch(r"\d+\.\d+\.\d+(?:[a-zA-Z0-9.-]+)?", version):
raise SystemExit(f"Invalid release version: {raw_version!r}")
tag = f"v{version}"
pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
package_version = pyproject["project"]["version"]
if version != package_version:
raise SystemExit(
f"Tag version {version!r} does not match pyproject version {package_version!r}"
)
init_text = Path("src/abstractgateway/__init__.py").read_text(encoding="utf-8")
init_match = re.search(r'__version__\s*=\s*"([^"]+)"', init_text)
if not init_match:
raise SystemExit("Could not find __version__ in src/abstractgateway/__init__.py")
if version != init_match.group(1):
raise SystemExit(
f"Tag version {version!r} does not match __version__ {init_match.group(1)!r}"
)
app_text = Path("src/abstractgateway/app.py").read_text(encoding="utf-8")
app_match = re.search(r'version\s*=\s*"([^"]+)"', app_text)
if not app_match:
raise SystemExit("Could not find FastAPI app version in src/abstractgateway/app.py")
if version != app_match.group(1):
raise SystemExit(
f"Tag version {version!r} does not match FastAPI app version {app_match.group(1)!r}"
)
changelog = Path("CHANGELOG.md").read_text(encoding="utf-8")
header = re.search(rf"^## \[{re.escape(version)}\] - .+$", changelog, re.MULTILINE)
if not header:
raise SystemExit(f"CHANGELOG.md has no entry for {version}")
rest = changelog[header.end():]
next_header = re.search(r"^## \[", rest, re.MULTILINE)
notes = rest[: next_header.start() if next_header else len(rest)].strip()
if not notes:
raise SystemExit(f"CHANGELOG.md entry for {version} is empty")
Path("release-notes.md").write_text(notes + "\n", encoding="utf-8")
github_output = Path(os.environ["GITHUB_OUTPUT"])
with github_output.open("a", encoding="utf-8") as fh:
print(f"version={version}", file=fh)
print(f"tag={tag}", file=fh)
PY
- name: Build
run: python -m build
- name: Check distributions
run: python -m twine check dist/*
- uses: actions/upload-artifact@v4
with:
name: release-dist
path: dist/*
- uses: actions/upload-artifact@v4
with:
name: release-notes
path: release-notes.md
ensure-tag:
name: Ensure release tag
runs-on: ubuntu-latest
needs: build
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create annotated tag for manual releases
if: github.event_name == 'workflow_dispatch'
run: |
tag="${{ needs.build.outputs.tag }}"
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
echo "Tag ${tag} already exists on origin."
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag -a "${tag}" -m "AbstractGateway ${tag}"
git push origin "${tag}"
- name: Tag already supplied by push trigger
if: github.event_name != 'workflow_dispatch'
run: echo "Using existing tag ${{ needs.build.outputs.tag }}"
publish-pypi:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: [build, ensure-tag]
environment:
name: pypi
url: https://pypi.org/project/abstractgateway/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: release-dist
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
publish-ghcr:
name: Publish GHCR Gateway images
runs-on: ubuntu-latest
needs: [build, ensure-tag, publish-pypi]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Wait for PyPI release
env:
VERSION: ${{ needs.build.outputs.version }}
run: |
python - <<'PY'
import json
import os
import time
import urllib.request
version = os.environ["VERSION"]
url = f"https://pypi.org/pypi/abstractgateway/{version}/json"
for attempt in range(1, 31):
try:
with urllib.request.urlopen(url, timeout=10) as response:
if response.status == 200:
json.loads(response.read().decode("utf-8"))
print(f"abstractgateway {version} is available on PyPI")
raise SystemExit(0)
except Exception as exc:
print(f"PyPI not ready yet (attempt {attempt}/30): {exc}")
time.sleep(10)
raise SystemExit(f"Timed out waiting for abstractgateway {version} on PyPI")
PY
for attempt in $(seq 1 30); do
if python -m pip install --dry-run --no-deps --no-cache-dir --index-url https://pypi.org/simple "abstractgateway==${VERSION}" > /tmp/abstractgateway-pip-visible.log 2>&1; then
cat /tmp/abstractgateway-pip-visible.log
echo "abstractgateway ${VERSION} is installable from the PyPI simple index"
exit 0
fi
cat /tmp/abstractgateway-pip-visible.log
echo "PyPI simple index not ready for pip yet (attempt ${attempt}/30)."
sleep 10
done
exit 1
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
file: docker/abstractgateway-server/Dockerfile
platforms: linux/amd64,linux/arm64
provenance: false
push: true
build-args: |
ABSTRACTGATEWAY_INSTALL_MODE=pypi
ABSTRACTGATEWAY_VERSION=${{ needs.build.outputs.version }}
tags: |
ghcr.io/${{ github.repository_owner }}/abstractgateway:${{ needs.build.outputs.version }}
ghcr.io/${{ github.repository_owner }}/abstractgateway:latest
ghcr.io/${{ github.repository_owner }}/abstractgateway-server:${{ needs.build.outputs.version }}
ghcr.io/${{ github.repository_owner }}/abstractgateway-server:latest
- name: Attempt experimental NVIDIA full server image
uses: docker/build-push-action@v6
continue-on-error: true
with:
context: .
file: docker/abstractgateway-server/Dockerfile.nvidia
platforms: linux/amd64
provenance: false
push: true
build-args: |
ABSTRACTGATEWAY_INSTALL_MODE=pypi
ABSTRACTGATEWAY_VERSION=${{ needs.build.outputs.version }}
ABSTRACTGATEWAY_EXTRAS=gpu
tags: |
ghcr.io/${{ github.repository_owner }}/abstractgateway:${{ needs.build.outputs.version }}-gpu
ghcr.io/${{ github.repository_owner }}/abstractgateway:gpu-latest
ghcr.io/${{ github.repository_owner }}/abstractgateway-server-nvidia:${{ needs.build.outputs.version }}
ghcr.io/${{ github.repository_owner }}/abstractgateway-server-nvidia:latest
github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [build, ensure-tag, publish-pypi, publish-ghcr]
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: release-dist
path: dist
- uses: actions/download-artifact@v4
with:
name: release-notes
path: .
- uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.build.outputs.tag }}
name: AbstractGateway ${{ needs.build.outputs.tag }}
body_path: release-notes.md
files: dist/*
fail_on_unmatched_files: true
deploy-docs:
name: Deploy docs to GitHub Pages
runs-on: ubuntu-latest
needs: [build, github-release]
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install docs dependencies
run: |
python -m pip install -U pip
python -m pip install -e ".[docs]"
- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Deploy MkDocs site
run: mkdocs gh-deploy --force --clean --message "Deploy docs for ${{ needs.build.outputs.tag }}"