ci #351
This file contains 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: ci | |
on: | |
pull_request: | |
push: | |
branches: [develop, main] | |
tags: ["[0-9]+.[0-9]+.[0-9]+*"] | |
workflow_dispatch: | |
inputs: | |
environment: | |
description: GitHub Actions deployment environment | |
required: false | |
type: environment | |
env: | |
DOCKER_BUILDKIT: "1" | |
HATCH_ENV: "ci" | |
HATCH_VERSION: "1.12.0" | |
PIPX_VERSION: "1.7.1" | |
jobs: | |
setup: | |
runs-on: ubuntu-latest | |
outputs: | |
environment-name: ${{ steps.set-env.outputs.environment-name }} | |
environment-url: ${{ steps.set-env.outputs.environment-url }} | |
repo-name: ${{ steps.set-env.outputs.repo-name }} | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Set GitHub Actions deployment environment | |
id: set-env | |
run: | | |
repo_name=${GITHUB_REPOSITORY##*/} | |
if ${{ github.event_name == 'workflow_dispatch' }}; then | |
environment_name=${{ inputs.environment }} | |
elif ${{ github.ref_type == 'tag' }}; then | |
environment_name="PyPI" | |
else | |
environment_name="" | |
fi | |
if [ "$environment_name" = "PyPI" ]; then | |
url="https://pypi.org/project/$repo_name/" | |
environment_url="$url$GITHUB_REF_NAME/" | |
else | |
timestamp="$(date -Iseconds)" | |
url="https://api.github.com/repos/$GITHUB_REPOSITORY/deployments" | |
environment_url="$url?timestamp=$timestamp" | |
fi | |
echo "environment-name=$environment_name" >>"$GITHUB_OUTPUT" | |
echo "environment-url=$environment_url" >>"$GITHUB_OUTPUT" | |
echo "repo-name=$repo_name" >>"$GITHUB_OUTPUT" | |
- name: Create annotation for deployment environment | |
if: steps.set-env.outputs.environment-name != '' | |
run: echo "::notice::Deployment environment ${{ steps.set-env.outputs.environment-name }}" | |
python: | |
runs-on: ubuntu-latest | |
needs: [setup] | |
strategy: | |
matrix: | |
python-version: ["3.9", "3.10", "3.11", "3.12"] | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/setup-python@v5 | |
with: | |
python-version: ${{ matrix.python-version }} | |
- name: Set up pip cache | |
if: runner.os == 'Linux' | |
uses: actions/cache@v4 | |
with: | |
path: ~/.cache/pip | |
key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }} | |
restore-keys: ${{ runner.os }}-pip- | |
- name: Install pipx for Python ${{ matrix.python-version }} | |
run: python -m pip install "pipx==$PIPX_VERSION" | |
- name: Install Hatch | |
run: pipx install "hatch==$HATCH_VERSION" | |
- name: Test Hatch version | |
run: | | |
HATCH_VERSION_INSTALLED=$(hatch --version) | |
echo "The HATCH_VERSION environment variable is set to $HATCH_VERSION." | |
echo "The installed Hatch version is ${HATCH_VERSION_INSTALLED##Hatch, version }." | |
case $HATCH_VERSION_INSTALLED in | |
*$HATCH_VERSION) echo "Hatch version correct." ;; | |
*) echo "Hatch version incorrect." && exit 1 ;; | |
esac | |
- name: Install dependencies | |
run: hatch env create ${{ env.HATCH_ENV }} | |
- name: Test virtualenv location | |
run: | | |
EXPECTED_VIRTUALENV_PATH=$GITHUB_WORKSPACE/.venv | |
INSTALLED_VIRTUALENV_PATH=$(hatch env find) | |
echo "The virtualenv should be at $EXPECTED_VIRTUALENV_PATH." | |
echo "Hatch is using a virtualenv at $INSTALLED_VIRTUALENV_PATH." | |
case "$INSTALLED_VIRTUALENV_PATH" in | |
"$EXPECTED_VIRTUALENV_PATH") echo "Correct Hatch virtualenv." ;; | |
*) echo "Incorrect Hatch virtualenv." && exit 1 ;; | |
esac | |
- name: Test that Git tag version and Python package version match | |
if: github.ref_type == 'tag' && matrix.python-version == '3.12' | |
run: | | |
GIT_TAG_VERSION=$GITHUB_REF_NAME | |
PACKAGE_VERSION=$(hatch version) | |
echo "The Python package version is $PACKAGE_VERSION." | |
echo "The Git tag version is $GIT_TAG_VERSION." | |
if [ "$PACKAGE_VERSION" = "$GIT_TAG_VERSION" ]; then | |
echo "Versions match." | |
else | |
echo "Versions do not match." && exit 1 | |
fi | |
- name: Run Hatch script for code quality checks | |
run: hatch run ${{ env.HATCH_ENV }}:check | |
- name: Run tests | |
run: | | |
export COVERAGE_PROCESS_START="$PWD/pyproject.toml" | |
hatch run ${{ env.HATCH_ENV }}:coverage run | |
timeout-minutes: 5 | |
- name: Enforce test coverage | |
run: | | |
hatch run ${{ env.HATCH_ENV }}:coverage combine -q | |
hatch run ${{ env.HATCH_ENV }}:coverage report | |
- name: Build Python package | |
run: hatch build | |
- name: Upload Python package artifacts | |
if: > | |
github.ref_type == 'tag' && | |
matrix.python-version == '3.12' && | |
needs.setup.outputs.environment-name == 'PyPI' | |
uses: actions/upload-artifact@v4 | |
with: | |
if-no-files-found: error | |
name: ${{ needs.setup.outputs.repo-name }}-${{ github.ref_name }} | |
path: dist | |
docker: | |
runs-on: ubuntu-latest | |
needs: [setup, python] | |
strategy: | |
fail-fast: false | |
matrix: | |
linux-version: ["alpine", "bookworm", "slim-bookworm"] | |
python-version: ["3.9", "3.10", "3.11", "3.12"] | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/setup-python@v5 | |
with: | |
python-version: ${{ matrix.python-version }} | |
- run: python3 -m pip install 'httpie>=3,<4' 'urllib3>=1,<2' | |
- name: Set up versions and Docker tags for Python and Alpine Linux | |
id: setup | |
run: | | |
LINUX_VERSION=${{ matrix.linux-version }} | |
linux_version_without_debian_release_name="${LINUX_VERSION/bookworm/}" | |
linux_tag="${linux_version_without_debian_release_name%-}" | |
LINUX_TAG="${linux_tag:+-$linux_tag}" | |
PYTHON_VERSION=${{ matrix.python-version }} | |
PYTHON_TAG="-python$PYTHON_VERSION" | |
echo "LINUX_VERSION=$LINUX_VERSION" >> $GITHUB_ENV | |
echo "LINUX_TAG=$LINUX_TAG" >> $GITHUB_ENV | |
echo "PYTHON_VERSION=$PYTHON_VERSION" >> $GITHUB_ENV | |
echo "PYTHON_TAG=$PYTHON_TAG" >> $GITHUB_ENV | |
- name: Build Docker images | |
run: | | |
docker build . --rm --target base \ | |
--build-arg BUILDKIT_INLINE_CACHE=1 \ | |
--build-arg HATCH_VERSION="$HATCH_VERSION" \ | |
--build-arg LINUX_VERSION="$LINUX_VERSION" \ | |
--build-arg PIPX_VERSION="$PIPX_VERSION" \ | |
--build-arg PYTHON_VERSION="$PYTHON_VERSION" \ | |
--cache-from ghcr.io/br3ndonland/inboard \ | |
-t ghcr.io/br3ndonland/inboard:base"$LINUX_TAG" | |
docker build . --rm --target starlette \ | |
--build-arg BUILDKIT_INLINE_CACHE=1 \ | |
--build-arg HATCH_VERSION="$HATCH_VERSION" \ | |
--build-arg LINUX_VERSION="$LINUX_VERSION" \ | |
--build-arg PIPX_VERSION="$PIPX_VERSION" \ | |
--build-arg PYTHON_VERSION="$PYTHON_VERSION" \ | |
-t ghcr.io/br3ndonland/inboard:starlette"$LINUX_TAG" | |
docker build . --rm --target fastapi \ | |
--build-arg BUILDKIT_INLINE_CACHE=1 \ | |
--build-arg HATCH_VERSION="$HATCH_VERSION" \ | |
--build-arg LINUX_VERSION="$LINUX_VERSION" \ | |
--build-arg PIPX_VERSION="$PIPX_VERSION" \ | |
--build-arg PYTHON_VERSION="$PYTHON_VERSION" \ | |
-t ghcr.io/br3ndonland/inboard:fastapi"$LINUX_TAG" | |
- name: Run Docker containers for testing | |
run: | | |
docker run -d -p 80:80 --name inboard-base \ | |
-e "BASIC_AUTH_USERNAME=test_user" \ | |
-e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \ | |
ghcr.io/br3ndonland/inboard:base"$LINUX_TAG" | |
docker run -d -p 81:80 --name inboard-starlette \ | |
-e "BASIC_AUTH_USERNAME=test_user" \ | |
-e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \ | |
ghcr.io/br3ndonland/inboard:starlette"$LINUX_TAG" | |
docker run -d -p 82:80 --name inboard-fastapi \ | |
-e "BASIC_AUTH_USERNAME=test_user" \ | |
-e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \ | |
ghcr.io/br3ndonland/inboard:fastapi"$LINUX_TAG" | |
- name: Test Hatch version in Docker containers | |
run: | | |
test_hatch_version_in_docker() { | |
echo "The HATCH_VERSION environment variable is set to $HATCH_VERSION." | |
local hatch_version_in_docker hatch_version_in_docker_full | |
for container_name in "$@"; do | |
hatch_version_in_docker_full=$(docker exec "$container_name" hatch --version) | |
hatch_version_in_docker="${hatch_version_in_docker_full##Hatch, version }" | |
if [ -n "$hatch_version_in_docker" ]; then | |
echo "Docker container $container_name has $hatch_version_in_docker." | |
fi | |
case $hatch_version_in_docker in | |
*$HATCH_VERSION) echo "Hatch versions match for $container_name." ;; | |
*) echo "Hatch version test failed for $container_name." && return 1 ;; | |
esac | |
done | |
} | |
test_hatch_version_in_docker inboard-base inboard-starlette inboard-fastapi | |
- name: Test virtualenv location in Docker containers | |
run: | | |
test_virtualenv_location_in_docker() { | |
local docker_virtualenv docker_python expected_virtualenv expected_python | |
expected_virtualenv="/app/.venv" | |
expected_python="$expected_virtualenv/bin/python" | |
echo "The Hatch virtualenv should be at $expected_virtualenv." | |
echo "The Python executable should be at $expected_python." | |
for container_name in "$@"; do | |
docker_virtualenv=$(docker exec "$container_name" hatch env find) | |
docker_python=$(docker exec "$container_name" which python) | |
case "$docker_virtualenv" in | |
"$expected_virtualenv") echo "Correct Hatch virtualenv $docker_virtualenv for $container_name." ;; | |
*) echo "Incorrect Hatch virtualenv $docker_virtualenv for $container_name." && return 1 ;; | |
esac | |
case "$docker_python" in | |
"$expected_python") echo "Correct Python $docker_python for $container_name." ;; | |
*) echo "Incorrect Python $docker_python for $container_name." && return 1 ;; | |
esac | |
done | |
} | |
test_virtualenv_location_in_docker inboard-base inboard-starlette inboard-fastapi | |
- name: Smoke test Docker containers | |
run: | | |
handle_error_code() { | |
case "$1" in | |
2) : 'Request timed out!' ;; | |
3) : 'Unexpected HTTP 3xx Redirection!' ;; | |
4) : 'HTTP 4xx Client Error!' ;; | |
5) : 'HTTP 5xx Server Error!' ;; | |
6) : 'Exceeded --max-redirects=<n> redirects!' ;; | |
*) : 'Other Error!' ;; | |
esac | |
echo "$_" | |
return "$1" | |
} | |
smoke_test() { | |
if http --check-status --ignore-stdin -q --timeout=5 "$@"; then | |
echo 'Smoke test passed. OK!' | |
else | |
handle_error_code "$?" | |
fi | |
} | |
smoke_test_xfail() { | |
if http --check-status --ignore-stdin -q --timeout=5 "$@" &>/dev/null; then | |
echo 'Smoke test should have failed!' | |
return 1 | |
else | |
echo 'Smoke test expected to fail. OK!' | |
fi | |
} | |
smoke_test :80 | |
smoke_test :81 | |
smoke_test :82 | |
smoke_test -a test_user:r4ndom_bUt_memorable :81/status | |
smoke_test -a test_user:r4ndom_bUt_memorable :82/status | |
smoke_test_xfail -a test_user:incorrect_password :81/status | |
smoke_test_xfail -a test_user:incorrect_password :82/status | |
smoke_test_xfail :81/status | |
smoke_test_xfail :82/status | |
- name: Log in to Docker registry | |
if: > | |
github.ref_type == 'tag' || | |
github.ref == 'refs/heads/develop' || | |
github.ref == 'refs/heads/main' | |
run: | | |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io \ | |
-u ${{ github.actor }} --password-stdin | |
- name: Tag and push Docker images with latest tags | |
if: > | |
matrix.python-version == '3.12' && | |
( | |
github.ref_type == 'tag' || | |
github.ref == 'refs/heads/develop' || | |
github.ref == 'refs/heads/main' | |
) | |
run: | | |
docker push ghcr.io/br3ndonland/inboard:base"$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:starlette"$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:fastapi"$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:fastapi"$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:latest"$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:latest"$LINUX_TAG" | |
- name: Tag and push Docker images with Python version | |
if: github.ref_type == 'tag' || github.ref == 'refs/heads/main' | |
run: | | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:base"$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:base"$PYTHON_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:starlette"$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:starlette"$PYTHON_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:fastapi"$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:fastapi"$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:base"$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:starlette"$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:fastapi"$PYTHON_TAG$LINUX_TAG" | |
- name: Tag and push Docker images with Git tag | |
if: github.ref_type == 'tag' | |
run: | | |
GIT_TAG_FULL=${{ github.ref_name }} | |
GIT_TAG_MAJOR_MINOR=$(echo "$GIT_TAG_FULL" | cut -d '.' -f 1-2) | |
for GIT_TAG in "$GIT_TAG_FULL" "$GIT_TAG_MAJOR_MINOR"; do | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"base$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"base-$GIT_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"starlette$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"starlette-$GIT_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"fastapi$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"fastapi-$GIT_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"base$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"base-$GIT_TAG$PYTHON_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"starlette$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"starlette-$GIT_TAG$PYTHON_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"fastapi$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"fastapi-$GIT_TAG$PYTHON_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"base$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"$GIT_TAG-base$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"starlette$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"$GIT_TAG-starlette$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"fastapi$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"$GIT_TAG-fastapi$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"base$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"$GIT_TAG-base$PYTHON_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"starlette$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"$GIT_TAG-starlette$PYTHON_TAG$LINUX_TAG" | |
docker tag \ | |
ghcr.io/br3ndonland/inboard:"fastapi$LINUX_TAG" \ | |
ghcr.io/br3ndonland/inboard:"$GIT_TAG-fastapi$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"base-$GIT_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"starlette-$GIT_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"fastapi-$GIT_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"base-$GIT_TAG$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"starlette-$GIT_TAG$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"fastapi-$GIT_TAG$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"$GIT_TAG-base$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"$GIT_TAG-starlette$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"$GIT_TAG-fastapi$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"$GIT_TAG-base$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"$GIT_TAG-starlette$PYTHON_TAG$LINUX_TAG" | |
docker push ghcr.io/br3ndonland/inboard:"$GIT_TAG-fastapi$PYTHON_TAG$LINUX_TAG" | |
done | |
pypi: | |
environment: | |
name: ${{ needs.setup.outputs.environment-name }} | |
url: ${{ needs.setup.outputs.environment-url }} | |
if: github.ref_type == 'tag' && needs.setup.outputs.environment-name == 'PyPI' | |
needs: [setup, python, docker] | |
permissions: | |
id-token: write | |
runs-on: ubuntu-latest | |
steps: | |
- name: Download Python package artifacts | |
uses: actions/download-artifact@v4 | |
with: | |
merge-multiple: true | |
name: ${{ needs.setup.outputs.repo-name }}-${{ github.ref_name }} | |
path: dist | |
- name: Publish Python package to PyPI | |
uses: pypa/gh-action-pypi-publish@release/v1.8 | |
changelog: | |
if: github.ref_type == 'tag' | |
needs: [setup, python, docker, pypi] | |
permissions: | |
contents: write | |
pull-requests: write | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
ref: develop | |
- name: Generate changelog from Git tags | |
run: | | |
echo '# Changelog | |
' >CHANGELOG.md | |
echo '# Changelog | |
[View on GitHub](https://github.com/${{github.repository}}/blob/HEAD/CHANGELOG.md) | |
' >docs/changelog.md | |
GIT_LOG_FORMAT='## %(subject) - %(taggerdate:short) | |
%(contents:body) | |
Tagger: %(taggername) %(taggeremail) | |
Date: %(taggerdate:iso) | |
```text | |
%(contents:signature)``` | |
' | |
git tag -l --sort=-taggerdate:iso --format="$GIT_LOG_FORMAT" >>CHANGELOG.md | |
git tag -l --sort=-taggerdate:iso --format="$GIT_LOG_FORMAT" >>docs/changelog.md | |
# shellcheck disable=SC2016 | |
ESCAPE_DUNDERS='s:([^`])(__)([a-z]+)(__)([^`]):\1\\_\\_\3\\_\\_\5:g' | |
sed -Ei "$ESCAPE_DUNDERS" CHANGELOG.md | |
sed -Ei "$ESCAPE_DUNDERS" docs/changelog.md | |
- name: Format changelog with Prettier | |
run: npx -s -y prettier@'^2' --write CHANGELOG.md docs/changelog.md | |
- name: Create pull request with updated changelog | |
uses: peter-evans/create-pull-request@v6 | |
with: | |
add-paths: | | |
CHANGELOG.md | |
docs/changelog.md | |
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> | |
branch: create-pull-request/${{ github.ref_name }} | |
commit-message: Update changelog for version ${{ github.ref_name }} | |
title: Update changelog for version ${{ github.ref_name }} |