diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 0000000..167f79c --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,72 @@ +# Builds the image on a GitHub-hosted runner (this repo is public: free minutes, off the VPS CPU), +# pushes it to GHCR, then triggers the Coolify deploy of the afp app, which pulls the moving +# :staging (develop) / :prod (main) tag. Requires org-level vars COOLIFY_API_SUBDOMAIN, DOMAIN_NAME, +# SERVER_HOST and org-level secret COOLIFY_API_TOKEN. The health check passes health_check_origin_ip +# (SERVER_HOST) to curl --resolve directly to the origin, bypassing Cloudflare's Bot Fight Mode. +name: Build and deploy + +on: + push: + branches: [develop, main] + +permissions: + contents: read + +concurrency: + group: build-and-deploy-${{ github.ref }} + cancel-in-progress: false + +env: + IMAGE: ghcr.io/behindthemusictree/afp + +jobs: + build: + name: Build and push image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7.0.1 + + - uses: docker/setup-buildx-action@v4.4.1 + + - uses: docker/login-action@v4.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: meta + uses: docker/metadata-action@v6.2.0 + with: + images: ${{ env.IMAGE }} + tags: | + type=raw,value=staging,enable=${{ github.ref == 'refs/heads/develop' }} + type=raw,value=prod,enable=${{ github.ref == 'refs/heads/main' }} + type=sha,prefix=sha- + + - uses: docker/build-push-action@v7.4.0 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + name: Deploy (${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}) + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Trigger afp deploy + uses: BehindTheMusicTree/github-workflows/.github/actions/trigger-coolify-deploy@v4.3.4 + with: + app_name: afp + coolify_environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} + coolify_subdomain: ${{ vars.COOLIFY_API_SUBDOMAIN }} + domain: ${{ vars.DOMAIN_NAME }} + coolify_api_token: ${{ secrets.COOLIFY_API_TOKEN }} + health_check_path: /health + health_check_origin_ip: ${{ vars.SERVER_HOST }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 327091a..1fe05f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,11 +42,18 @@ All contributors (including maintainers) should update `CHANGELOG.md` when creat ## [Unreleased] +## [1.4.5] - 2026-09-28 + ### CI +- **Build and deploy**: New `.github/workflows/build-and-deploy.yml` builds the Docker image on `ubuntu-latest` (off the VPS) and pushes it to `ghcr.io/behindthemusictree/afp` — `:staging` on `develop`, `:prod` on `main`, plus `:sha-` — then triggers the Coolify deploy of the `afp` app (staging/production) via `trigger-coolify-deploy`, health-checking `/health`. - **Tests**: CI now runs `pytest` with coverage instead of `python -m unittest discover`, matching the pytest config already in `pyproject.toml`. Added unit tests for `audio_fingerprinter.py`, `env_var_loader.py`, `errors.py`, and `utils.py`. The coverage gate (`fail_under = 100` in `pyproject.toml`) is scoped to the `audio_fingerprinter` package only — `run.py`/`settings.py` are excluded since their incidental coverage depends on the runner's `fpcalc`/`ffmpeg` behavior (e.g. short test-audio files fail fingerprinting locally on macOS but succeed on CI's Linux runner), which made gating on them non-deterministic across environments. - **Tests**: The `Tests` workflow now also triggers on pull requests and pushes targeting `develop` (previously only `main`), and on direct pushes to either branch, so PRs into `develop` actually run CI and a direct push bypassing review still gets checked. -- **Publish**: Removed **`.github/workflows/publish.yaml`** and **`scripts/check-publish-env.sh`** — Coolify now builds and deploys the image directly from this git repository instead of the legacy GHCR tag-push flow. **`GHCR_IMAGE_NAMESPACE`** / **`AFP_IMAGE_REPO`** are no longer used. +- **Publish**: Removed **`.github/workflows/publish.yaml`** and **`scripts/check-publish-env.sh`** (legacy GHCR tag-push flow), superseded by **Build and deploy** above. **`GHCR_IMAGE_NAMESPACE`** / **`AFP_IMAGE_REPO`** are no longer used. + +### Changed + +- **Docker image**: The `Dockerfile` no longer takes build arguments, so one prebuilt image serves every environment. `FLASK_LOG_*_FILENAME` / `GUNICORN_LOG_*_FILENAME` are now runtime env vars (already validated at startup), and `FPCALC` is baked in as `/app/bin/fpcalc`, where the image places the binary — `FPCALC_INTERNAL_PATH` is no longer a build input (the Tests workflow still uses it). ### Documentation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7b37a24..3f7211a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,7 @@ _Note: Contributors can submit fixes for critical issues via feature branches. M - **Tests** (`.github/workflows/tests.yaml`): Runs on pull requests and pushes targeting `main` or `develop` - **Tests**: Setup Python 3.14, install system dependencies via `scripts/install-dependencies.sh`, run `scripts/setup-filesystem.sh`, then `python -m pytest --cov` (enforces the minimum coverage threshold in `pyproject.toml`) -- **Deploy**: Coolify builds and deploys the image directly from this git repository — there is no GitHub Actions publish workflow or GHCR image. +- **Build and deploy** (`.github/workflows/build-and-deploy.yml`): On pushes to `develop` / `main`, builds the image and pushes it to `ghcr.io/behindthemusictree/afp` (`:staging` / `:prod`, plus `:sha-`), then triggers the Coolify deploy of the `afp` app **Repository automation (maintainer-only):** diff --git a/Dockerfile b/Dockerfile index 1d24584..727415e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,35 +2,12 @@ # If you relied on Ubuntu 22.04 + pinned ffmpeg for byte-identical fingerprints, re-validate after this change. FROM python:3.14-slim-bookworm -ARG FPCALC_INTERNAL_PATH -ARG FLASK_LOG_APP_FILENAME -ARG FLASK_LOG_ERROR_FILENAME -ARG FLASK_LOG_REQUESTS_FILENAME -ARG GUNICORN_LOG_ERROR_FILENAME -ARG GUNICORN_LOG_ACCESS_FILENAME - -RUN for var in \ - FPCALC_INTERNAL_PATH \ - FLASK_LOG_APP_FILENAME \ - FLASK_LOG_ERROR_FILENAME \ - FLASK_LOG_REQUESTS_FILENAME \ - GUNICORN_LOG_ERROR_FILENAME \ - GUNICORN_LOG_ACCESS_FILENAME; do \ - if [ -z "$(eval echo \$$var)" ]; then \ - echo "The $var argument is not provided" >&2; \ - exit 1; \ - fi; \ -done - +# Log filenames are required at runtime (settings.py / scripts/setup-filesystem.sh fail fast), so one +# prebuilt image serves every environment. FPCALC is baked in: the image itself places the binary there. ENV APP_IS_DOCKERIZED=true \ APP_IS_EXPOSED=true \ ENV=TEST \ - FPCALC=$FPCALC_INTERNAL_PATH \ - FLASK_LOG_APP_FILENAME=$FLASK_LOG_APP_FILENAME \ - FLASK_LOG_ERROR_FILENAME=$FLASK_LOG_ERROR_FILENAME \ - FLASK_LOG_REQUESTS_FILENAME=$FLASK_LOG_REQUESTS_FILENAME \ - GUNICORN_LOG_ERROR_FILENAME=$GUNICORN_LOG_ERROR_FILENAME \ - GUNICORN_LOG_ACCESS_FILENAME=$GUNICORN_LOG_ACCESS_FILENAME + FPCALC=/app/bin/fpcalc WORKDIR /app diff --git a/README.md b/README.md index 5534790..67c7b97 100644 --- a/README.md +++ b/README.md @@ -154,26 +154,19 @@ The service will start on `0.0.0.0:PORT` (configured via `APP_PORT` environment ## Docker Deployment -**CI:** Coolify builds and deploys the image directly from this git repository — there is no GitHub Actions publish workflow or GHCR image. +**CI:** `.github/workflows/build-and-deploy.yml` builds the image on a GitHub-hosted runner and pushes it to `ghcr.io/behindthemusictree/afp` on every push to `develop` (`:staging`) and `main` (`:prod`), plus `:sha-`, then triggers the Coolify deploy of the `afp` app in the matching environment. ### Build -Build the Docker image with required build arguments (path vars are not build args; they are required at runtime): +The image takes no build arguments — one image serves every environment: ```bash -docker build \ - --build-arg FPCALC_INTERNAL_PATH=/app/bin/fpcalc \ - --build-arg FLASK_LOG_APP_FILENAME=app.log \ - --build-arg FLASK_LOG_ERROR_FILENAME=error.log \ - --build-arg FLASK_LOG_REQUESTS_FILENAME=requests.log \ - --build-arg GUNICORN_LOG_ERROR_FILENAME=error.log \ - --build-arg GUNICORN_LOG_ACCESS_FILENAME=access.log \ - -t audio-fingerprinter:latest . +docker build -t audio-fingerprinter:latest . ``` ### Run -Path variables are **required at runtime** (not baked into the image). Pass them with `-e` in every environment: +Path and log filename variables are **required at runtime** (not baked into the image). Pass them with `-e` in every environment: ```bash docker run -d \ @@ -185,6 +178,11 @@ docker run -d \ -e APP_PORT=5000 \ -e GUNICORN_LOG_DIR=/var/log/audio-fingerprinter-gunicorn \ -e FLASK_LOG_DIR_EXTERNAL=/var/log/audio-fingerprinter-flask \ + -e FLASK_LOG_APP_FILENAME=app.log \ + -e FLASK_LOG_ERROR_FILENAME=error.log \ + -e FLASK_LOG_REQUESTS_FILENAME=requests.log \ + -e GUNICORN_LOG_ERROR_FILENAME=error.log \ + -e GUNICORN_LOG_ACCESS_FILENAME=access.log \ audio-fingerprinter:latest ``` @@ -201,6 +199,11 @@ docker run -d \ -e APP_PORT=3002 \ -e GUNICORN_LOG_DIR=/app/log/gunicorn/ \ -e FLASK_LOG_DIR_EXTERNAL=/app/log/flask \ + -e FLASK_LOG_APP_FILENAME=app.log \ + -e FLASK_LOG_ERROR_FILENAME=error.log \ + -e FLASK_LOG_REQUESTS_FILENAME=requests.log \ + -e GUNICORN_LOG_ERROR_FILENAME=error.log \ + -e GUNICORN_LOG_ACCESS_FILENAME=access.log \ audio-fingerprinter:latest ``` @@ -220,17 +223,6 @@ These environment variables are needed when running the app in development: - `FLASK_LOG_ERROR_FILENAME` - `FLASK_LOG_REQUESTS_FILENAME` -### Build - -These environment variables are needed when building the container (path dirs are not build args): - -- `FPCALC_INTERNAL_PATH` -- `FLASK_LOG_APP_FILENAME` -- `FLASK_LOG_ERROR_FILENAME` -- `FLASK_LOG_REQUESTS_FILENAME` -- `GUNICORN_LOG_ERROR_FILENAME` -- `GUNICORN_LOG_ACCESS_FILENAME` - ### Runtime (required) These must be set when running the container (fail fast if missing): @@ -238,6 +230,8 @@ These must be set when running the container (fail fast if missing): - `POOL_DIR_EXTERNAL` or `POOL_DIR_INTERNAL` – pool directory path inside the container - `APP_PORT` – port the app binds to - `GUNICORN_LOG_DIR` – when `APP_IS_EXPOSED=true` (default in image) +- `FLASK_LOG_APP_FILENAME`, `FLASK_LOG_ERROR_FILENAME`, `FLASK_LOG_REQUESTS_FILENAME` – Flask log filenames +- `GUNICORN_LOG_ERROR_FILENAME`, `GUNICORN_LOG_ACCESS_FILENAME` – when `APP_IS_EXPOSED=true` - `FLASK_LOG_DIR_EXTERNAL` or `FLASK_LOG_DIR_INTERNAL` – Flask log directory When running with `--user` (non-root), use writable paths: `GUNICORN_LOG_DIR=/app/log/gunicorn/`, `FLASK_LOG_DIR_EXTERNAL=/app/log/flask`. diff --git a/pyproject.toml b/pyproject.toml index 96cca20..ee61506 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "audio-fingerprinter" -version = "1.4.4" +version = "1.4.5" description = "Flask REST API for Chromaprint-based audio fingerprints" readme = "README.md" requires-python = ">=3.14" @@ -40,7 +40,7 @@ show_missing = true exclude_lines = ["if __name__ == .__main__.:"] [tool.bumpversion] -current_version = "1.4.4" +current_version = "1.4.5" commit = false tag = false