Skip to content

ci

ci #350

Workflow file for this run

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 }}