Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
@@ -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 }}
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<short>` — 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

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<short>`), then triggers the Coolify deploy of the `afp` app

**Repository automation (maintainer-only):**

Expand Down
29 changes: 3 additions & 26 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 16 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<short>`, 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 \
Expand All @@ -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
```

Expand All @@ -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
```

Expand All @@ -220,24 +223,15 @@ 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):

- `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`.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand Down
Loading