Skip to content

Bump version from 0.94.0 to 0.95.0 #773

Bump version from 0.94.0 to 0.95.0

Bump version from 0.94.0 to 0.95.0 #773

Workflow file for this run

name: ci
on:
pull_request:
push:
branches: [develop, main]
tags: ["[0-9]+.[0-9]+.[0-9]+*"]
workflow_dispatch:
inputs:
generate_changelog:
description: Run the changelog generation job
type: boolean
default: false
publish_to_pypi:
description: Publish Python package to PyPI
type: boolean
default: false
permissions:
contents: read
env:
DOCKER_BUILDKIT: "1"
HATCH_ENV_TYPE_VIRTUAL_PATH: ".venv"
HATCH_ENV: "ci"
HATCH_VERSION: ">=1.17,<1.18"
PIPX_VERSION: ">=1.16,<1.17"
REPO_NAME: inboard
jobs:
python:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Set up pip cache
if: runner.os == 'Linux'
uses: actions/cache@v5
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: Install dependencies
run: hatch env create ${{ env.HATCH_ENV }}
- name: Test virtualenv location
run: |
EXPECTED_VIRTUALENV_PATH="$GITHUB_WORKSPACE/$HATCH_ENV_TYPE_VIRTUAL_PATH"
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.13'
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: |
hatch run ${{ env.HATCH_ENV }}:coverage run
coverage_files_after_run=$(find . -name '.coverage*' | wc -l)
sleep_time=10
sleep "$sleep_time"
coverage_files_after_sleep=$(find . -name '.coverage*' | wc -l)
echo "[INFO] Verifying that coverage.py has stopped generating .coverage files.
[INFO] Number of coverage files after coverage run: $coverage_files_after_run
[INFO] Number of coverage files after $sleep_time second sleep: $coverage_files_after_sleep
"
if [ "$coverage_files_after_sleep" -gt "$coverage_files_after_run" ]; then
echo "[ERROR] Unexpected .coverage files detected."
exit 1
fi
timeout-minutes: 8
- 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.13'
uses: actions/upload-artifact@v7
with:
if-no-files-found: error
name: ${{ env.REPO_NAME }}-${{ github.ref_name }}
path: dist
docker:
runs-on: ubuntu-latest
needs: [python]
permissions:
packages: write
strategy:
fail-fast: false
matrix:
linux-version: ["alpine", "trixie", "slim-trixie"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- 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/trixie/}"
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: 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 number of redirects" ;;
*) : "Other error code $1" ;;
esac
echo "[ERROR] error code: $_" >&2
return "$1"
}
smoke_test() {
echo "[INFO] Running smoke test of $*."
if curl --fail --max-time 5 --no-keepalive --no-progress-meter "$@"; then
echo
echo "[INFO] Smoke test of $* passed."
else
local error_code="$?"
echo
echo "[ERROR] Smoke test of $* failed." >&2
handle_error_code "$error_code"
fi
}
smoke_test_xfail() {
echo "[INFO] Running smoke test of $* that is expected to fail."
if ! curl --fail --max-time 5 --no-keepalive --no-progress-meter "$@"; then
echo
echo "[INFO] Smoke test of $* expected to fail."
else
echo
echo "[ERROR] Smoke test of $* should have failed!" >&2
return 1
fi
}
test_gunicorn_in_docker() {
local container_name=$1 expected_message
local -i worker_count
echo "[INFO] Docker container logs from $container_name:"
docker logs "$container_name"
if [[ $container_name == *asgi ]]; then
expected_message="Using worker: asgi"
else
expected_message="Using worker:"
fi
grep -q "$expected_message" <(docker logs "$container_name")
echo "[INFO] Gunicorn control interface (gunicornc) stats from $container_name:"
docker exec "$container_name" gunicornc -c "show stats"
worker_count=$(docker exec "$container_name" gunicornc -c "show stats" -j | jq -r '.workers_current')
if [ $worker_count -lt 1 ]; then
echo "[ERROR] Gunicorn control interface (gunicornc) reports $worker_count workers spawned." >&2
return 1
fi
}
test_virtualenv_location_in_docker() {
local container_name=$1 docker_virtualenv docker_python expected_virtualenv expected_python
expected_virtualenv="/app/$HATCH_ENV_TYPE_VIRTUAL_PATH"
expected_python="$expected_virtualenv/bin/python"
echo "[INFO] The Hatch virtualenv should be at $expected_virtualenv."
echo "[INFO] The Python executable should be at $expected_python."
docker_virtualenv=$(docker exec "$container_name" hatch env find)
docker_python=$(docker exec "$container_name" which python)
case "$docker_virtualenv" in
"$expected_virtualenv") echo "[INFO] Correct Hatch virtualenv $docker_virtualenv for $container_name." ;;
*) echo "[ERROR] Incorrect Hatch virtualenv $docker_virtualenv for $container_name." >&2 && return 1 ;;
esac
case "$docker_python" in
"$expected_python") echo "[INFO] Correct Python $docker_python for $container_name." ;;
*) echo "[ERROR] Incorrect Python $docker_python for $container_name." >&2 && return 1 ;;
esac
}
main() {
local container_name=$1 container_tag worker_class
local -i host_port=10101
echo "[INFO] Testing $container_name."
echo
container_tag=$(echo "$container_name" | cut -d - -f 2)
[[ $container_name == *asgi ]] && worker_class=asgi
docker run -d -p $host_port:80 --name "$container_name" \
-e "BASIC_AUTH_USERNAME=test_user" \
-e "BASIC_AUTH_PASSWORD=r4ndom_bUt_memorable" \
-e "LOG_FORMAT=gunicorn" \
-e "LOG_LEVEL=debug" \
${worker_class:+-e "WORKER_CLASS=asgi"} \
ghcr.io/br3ndonland/inboard:"$container_tag$LINUX_TAG"
sleep 5
test_virtualenv_location_in_docker "$container_name"
test_gunicorn_in_docker "$container_name"
smoke_test http://localhost:$host_port
if [[ $container_name == *fastapi* || $container_name == *starlette* ]]; then
smoke_test --user test_user:r4ndom_bUt_memorable http://localhost:$host_port/status
smoke_test_xfail --user test_user:incorrect_password http://localhost:$host_port/status
smoke_test_xfail http://localhost:$host_port/status
fi
docker stop "$container_name"
}
container_names=(
inboard-base
inboard-starlette
inboard-fastapi
inboard-base-gunicorn-asgi
inboard-starlette-gunicorn-asgi
inboard-fastapi-gunicorn-asgi
)
for container_name in "${container_names[@]}"; do
main "$container_name"
done
- 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.13' &&
(
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
docker-cleanup:
environment:
name: Production
deployment: false
if: github.ref_type == 'tag' || github.ref == 'refs/heads/main'
needs: [python, docker]
runs-on: ubuntu-latest
steps:
- uses: dataaxiom/ghcr-cleanup-action@v1
with:
delete-untagged: true
token: ${{ secrets.GH_PAT }}
pypi:
environment:
name: PyPI
url: https://pypi.org/project/${{ env.REPO_NAME }}/${{ github.ref_name }}
if: >-
(github.event_name == 'push' && github.ref_type == 'tag') ||
(github.event_name == 'workflow_dispatch' && inputs.publish_to_pypi == true)
needs: [python, docker]
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- name: Download Python package artifacts
uses: actions/download-artifact@v8
with:
merge-multiple: true
name: ${{ env.REPO_NAME }}-${{ github.ref_name }}
path: dist
- name: Publish Python package to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
changelog:
environment:
name: Production
deployment: false
if: >-
(github.event_name == 'push' && github.ref_type == 'tag') ||
(github.event_name == 'workflow_dispatch' && inputs.generate_changelog == true)
needs: [python, docker]
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: develop
- name: Generate changelog from Git tags
run: |
echo '---
icon: lucide/clipboard-clock
---
# Changelog
' >docs/changelog.md
GIT_LOG_FORMAT='## %(subject) - %(taggerdate:short)
%(contents:body)'
git tag -l --sort=-taggerdate:iso --format="$GIT_LOG_FORMAT" >>docs/changelog.md
- name: Fix changelog
run: |
# Fix changelog
#
# The previous step in the GitHub Actions job generates a changelog
# from Git tag messages and writes the output to Markdown documents.
# https://github.com/br3ndonland/inboard/commit/b15efff7fc45759983743411fcbbd52415eb5455
#
# Some post-processing of these Git tag messages may be necessary in order
# for them to render properly from the Markdown documents. One reason for
# this post-processing is to handle "dunders" (double underscores).
#
# Dunder names like `__init__` have special meaning in Python.
# https://docs.python.org/3/reference/datamodel.html#specialnames
# https://docs.python.org/3/reference/lexical_analysis.html
#
# Dunders also make text bold in Markdown unless escaped with backslashes
# or enclosed in backticks. When the document is transformed to HTML, the
# backticks are transformed to HTML code elements.
#
# Some early changelog entries in this project did not enclose dunders in
# backticks. When the text from these entries is formatted as Markdown and
# rendered, text within dunders like `__init__` shows up as "init" in bold
# because Markdown parsers read it as equivalent to `**init**`. To avoid
# having these dunder names converted to bold text, the GitHub Actions job
# runs a `sed` command to escape the dunders, like \_\_init\_\_.
#
# The changelog entry for the update to Gunicorn 23.0.0 mentions a
# security vulnerability that was fixed, but this vulnerability was
# actually fixed in Gunicorn 22.0.0 and described in the changelog
# for inboard 0.68.0. This step removes the duplicate info.
# https://github.com/br3ndonland/inboard/pull/117
ESCAPE_DUNDERS='s:(\s|\/)(__)(all|init|token)(__)(\s|\.py|\(\)){0,1}:\1\\_\\_\3\\_\\_\5:g'
REPLACEMENT='There are several breaking changes noted in the\n[Gunicorn changelog](https://gunicorn.org/news/).'
sed -Ei "$ESCAPE_DUNDERS" "docs/changelog.md"
sed -Ei \
-e "s|\[Changes\]\(https://docs.gunicorn.org/en/latest/news.html\) include|$REPLACEMENT|g" \
-e "s|a fix for a high-severity security vulnerability \(CVE-2024-1135,||g" \
-e "s|\[GHSA-w3h3-4rj7-4ph4\]\(https://github.com/advisories/GHSA-w3h3-4rj7-4ph4\)\)\.||g" \
-e "s|There are several breaking changes noted in the Gunicorn changelog\.||g" \
"docs/changelog.md"
sed -Ei -z "s|\n\n\n||g" "docs/changelog.md"
sed -Ei "s|because that the|because the|g" "docs/changelog.md"
sed -Ei "s|Update to FastAPI 0.132$|**Update to FastAPI 0.132** (78d137c30911901aaa6faf50d522a11a3b3988d7)|g" "docs/changelog.md"
- name: Format changelog with Prettier
run: npx -s -y prettier@'^3' --write docs/changelog.md
- name: Set PR and commit title
id: set-title
run: |
if [ "$GITHUB_REF_TYPE" = 'tag' ]; then
title="Update changelog for version $GITHUB_REF_NAME"
else
most_recent_tag=$(git describe --abbrev=0 --tags)
title="Update changelog for version $most_recent_tag"
fi
echo "title=$title" >>"$GITHUB_OUTPUT"
- name: Create pull request with updated changelog
uses: peter-evans/create-pull-request@v8
with:
add-paths: |
docs/changelog.md
body: |
Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action
[skip ci]
branch: create-pull-request/${{ github.ref_name }}
commit-message: |
${{ steps.set-title.outputs.title }}
[skip ci]
sign-commits: true
title: ${{ steps.set-title.outputs.title }}