Release #34
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
| 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 }}" |