Skip to content

release: v2.2.7

release: v2.2.7 #291

Workflow file for this run

name: Test
on:
pull_request:
branches: [main, develop]
workflow_call:
inputs:
test_path:
description: Optional path passed to pytest (file, directory, or expression).
required: false
type: string
default: ""
env:
CONTAINER_ROOT_DIR: /home/app/
DB_CONNECTION_TEST_MAX_ATTEMPTS: 10
DB_CONNECTION_TEST_SLEEP_INTERVAL: 5
ENV: ci_test
APP_VERSION: 'test'
FILE_UPLOAD_ENABLED: true
TMP_UPLOADED_FILES: /tmp/ci-uploaded/
METADATA_SESSION_DIR: /tmp/ci-metadata-sessions/
MEDIA_DIR: /tmp/ci-media/
LIBRARIES_DIR: /tmp/ci-media/libraries/
DB_IS_NEEDED: true
DB_DATA_DIR: /var/lib/postgresql/data
DB_SUPERUSER_NAME: postgres
DB_PORT: '5432'
AFP_PORT: '3002'
AFP_POOL_DIR_EXTERNAL: /tmp/pool/
AFP_POST_ENDPOINT: fingerprint-audio
TMTA_USERNAME: tmta
SUPERADMIN_USERNAME: ci_superadmin
SUPERADMIN_PASSWORD: ci_superadmin_test
SUPERADMIN_EMAIL: ci_superadmin@example.com
DEMO_USERNAME: ci_demo
DEMO_PASSWORD: ci_demo_test
DEMO_EMAIL: ci_demo@example.com
jobs:
pre-commit:
name: Pre-commit
runs-on: ubuntu-latest
# Dummy identity + flags only. Inherits workflow paths for FILE_UPLOAD (models need LIBRARIES_DIR at import).
# No GitHub Environment or secrets: DB and external integrations off for mypy-only Django load.
env:
APP_NAME: htmt_api_mypy_ci
APP_TITLE: HTMT API (mypy CI)
DEBUG: "true"
APP_IS_EXPOSED: "false"
DB_IS_NEEDED: "false"
AFP_ENABLED: "false"
MUSICBRAINZ_LOOKUP_ENABLED: "false"
SPOTIFY_ENABLED: "false"
GOOGLE_OAUTH_ENABLED: "false"
steps:
- name: Checkout code
uses: actions/checkout@v5.0.1
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-pre-commit-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Set up filesystem for Django (mypy)
run: bash scripts/setup-filesystem.sh
- name: Install actionlint
run: bash scripts/install-actionlint.sh
- name: Run pre-commit
run: pre-commit run --all-files --verbose --show-diff-on-failure
check-vars-and-secrets:
name: Check vars
runs-on: ubuntu-latest
environment:
name: ci_test
steps:
- uses: actions/checkout@v5.0.1
with:
fetch-depth: 0
- name: Check required vars (no app DB secrets for pytest)
env:
API_DIR_NAME: ${{ vars.API_DIR_NAME }}
APP_TITLE: ${{ vars.APP_TITLE }}
APP_NAME: ${{ vars.HTMT_API_APP_NAME }}
DB_APP_NAME_SUFFIX: ${{ vars.DB_APP_NAME_SUFFIX }}
GHCR_IMAGE_NAMESPACE: ${{ vars.GHCR_IMAGE_NAMESPACE }}
DB_CONTAINER_NAME: ${{ format('{0}{1}', vars.HTMT_API_APP_NAME, vars.DB_APP_NAME_SUFFIX) }}
AFP_CONTAINER_NAME: ${{ vars.AFP_APP_NAME }}
AFP_IMAGE_REPO: ${{ vars.AFP_IMAGE_REPO }}
AFP_VERSION: ${{ vars.AFP_VERSION }}
run: bash scripts/check-workflow-env.sh API_DIR_NAME APP_TITLE APP_NAME DB_APP_NAME_SUFFIX GHCR_IMAGE_NAMESPACE DB_CONTAINER_NAME AFP_CONTAINER_NAME AFP_IMAGE_REPO AFP_VERSION
pytest:
name: Pytest
needs: [check-vars-and-secrets]
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
checks: write
pull-requests: write
environment:
name: ci_test
env:
ENV: ci_test
API_DIR_NAME: ${{ vars.API_DIR_NAME }}
APP_TITLE: ${{ vars.APP_TITLE }}
APP_NAME: ${{ vars.HTMT_API_APP_NAME }}
DB_CONTAINER_NAME: db
AFP_CONTAINER_NAME: afp
AFP_IMAGE_REPO: ${{ vars.AFP_IMAGE_REPO }}
AFP_VERSION: ${{ vars.AFP_VERSION }}
DEBUG: true
DB_IS_NEEDED: true
APP_IS_EXPOSED: false
AFP_ENABLED: true
SPOTIFY_ENABLED: true
SPOTIFY_CLIENT_ID: test
SPOTIFY_CLIENT_SECRET: test
SPOTIFY_REDIRECT_URI: http://test/callback
SPOTIFY_SCOPES: test
GOOGLE_OAUTH_ENABLED: true
GOOGLE_CLIENT_ID: test
GOOGLE_CLIENT_SECRET: test
GOOGLE_REDIRECT_URI: http://test/callback
MUSICBRAINZ_LOOKUP_ENABLED: true
ACOUSTID_API_KEY: ci-fake-acoustid-key
# Ephemeral CI DB: same defaults as docker-compose.yml (not staging/prod secrets).
DB_SUPERUSER_PASSWORD: postgres
DB_APP_DB_NAME: htmt_api
DB_APP_USERNAME: htmt_api_user
DB_APP_USER_PASSWORD: htmt_api_password
DJANGO_SECRET_KEY: dev-only-secret-key
DB_URL: db
AFP_URL: afp
steps:
- name: Checkout code
uses: actions/checkout@v5.0.1
- name: GHCR image namespace for Compose (lowercase)
shell: bash
env:
GHCR_NS: ${{ vars.GHCR_IMAGE_NAMESPACE }}
run: |
set -euo pipefail
ns_lc="${GHCR_NS,,}"
if [ -z "$ns_lc" ]; then
echo "ERROR: GHCR_IMAGE_NAMESPACE GitHub Variable is empty."
exit 1
fi
echo "GHCR_IMAGE_NAMESPACE=${ns_lc}" >> "${GITHUB_ENV}"
- name: Log in to ghcr.io for afp pull
shell: bash
run: |
set -euo pipefail
docker logout ghcr.io 2>/dev/null || true
owner_lc="${GITHUB_REPOSITORY_OWNER,,}"
if [ -n "${GHCR_READ_PACKAGES_TOKEN:-}" ]; then
if [ -z "${GHCR_READ_PACKAGES_USERNAME:-}" ]; then
echo "ERROR: GHCR_READ_PACKAGES_TOKEN is set but GHCR_READ_PACKAGES_USERNAME is empty."
exit 1
fi
echo "${GHCR_READ_PACKAGES_TOKEN}" | docker login ghcr.io -u "${GHCR_READ_PACKAGES_USERNAME}" --password-stdin
else
echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "${owner_lc}" --password-stdin
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GHCR_READ_PACKAGES_TOKEN: ${{ secrets.GHCR_READ_PACKAGES_TOKEN }}
GHCR_READ_PACKAGES_USERNAME: ${{ secrets.GHCR_READ_PACKAGES_USERNAME }}
- name: Build api image and start db and afp
run: |
set -euo pipefail
compose=(docker compose -f docker-compose.yml)
"${compose[@]}" build api
"${compose[@]}" pull db
"${compose[@]}" pull afp
"${compose[@]}" up -d --wait db afp
- name: Run pytest in api container
id: pytest_in_container
env:
PYTEST_PATH_INPUT: ${{ inputs.test_path }}
run: |
set -euo pipefail
compose=(docker compose -f docker-compose.yml)
if [ -n "${PYTEST_PATH_INPUT:-}" ]; then
pytest_target=${PYTEST_PATH_INPUT}
else
pytest_target="${API_DIR_NAME}/test/"
fi
pytest_target_q=$(printf '%q' "$pytest_target")
inner_script=$(
printf '%s\n' \
'set -euo pipefail' \
'bash scripts/setup-filesystem.sh' \
"bash scripts/wait-for-postgres-db.sh db ${DB_PORT} ${DB_CONNECTION_TEST_MAX_ATTEMPTS} ${DB_CONNECTION_TEST_SLEEP_INTERVAL}" \
"bash scripts/wait-for-afp.sh afp ${AFP_PORT} 12 5" \
'bash scripts/init-django-data.sh' \
"pytest -v --tb=short -x --junitxml=test-results.xml ${pytest_target_q}"
)
# Mount workspace so --junitxml=test-results.xml is visible on the runner (base compose has no ./ bind mount).
# CI_STARTUP_TRACE: verbose Django/pytest startup diagnostics (see docs/workflows.md).
# PYTHONFAULTHANDLER: on hang, SIGQUIT dumps Python stacks (docker kill -s QUIT <pytest_pid>).
# PYTEST_PLUGINS: load startup plugin when bind-mount hides image-local *.egg-info entry points.
# The thin entrypoint (exec "$@") passes the command straight through; CMD is overridden here with the
# inner_script so the container runs pytest instead of the default start-server.sh.
"${compose[@]}" run --rm -T \
-e CI_STARTUP_TRACE=1 \
-e PYTHONFAULTHANDLER=1 \
-e PYTEST_PLUGINS=api.ci_pytest_startup_plugin \
-v "${GITHUB_WORKSPACE}:/home/app" api bash -lc "$inner_script"
- name: Show Compose service logs on failure
if: failure() && steps.pytest_in_container.outcome == 'failure'
run: |
docker compose -f docker-compose.yml logs --no-color --tail=200 db afp || true
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2.23.0
# Fork PRs: GITHUB_TOKEN is read-only; this action needs Checks API (and optional PR comments).
if: >-
always() &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
with:
files: test-results.xml
- name: Tear down Compose
if: always()
run: docker compose -f docker-compose.yml down -v || true