From da4bbb23e8377deb5dd736dec15becb9fa64a49f Mon Sep 17 00:00:00 2001 From: Uwe Winter Date: Mon, 23 Feb 2026 15:26:57 +1100 Subject: [PATCH 1/3] ci: add health indicator, support migration on entrypoint.sh --- .dockerignore | 16 ++++++++++++++++ README.md | 1 + app/main.py | 6 ++++++ scripts/entrypoint.sh | 11 +++++++++++ 4 files changed, 34 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c00807e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.github +.venv +.pytest_cache +__pycache__ +*.pyc +.coverage +htmlcov +.env +.env.* +Dockerfile* +docker-compose*.yml +README.md +LICENSE +tests +docs diff --git a/README.md b/README.md index 0c79fe7..66bc37a 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,7 @@ The system implements role-based access control with the following roles: - Apply migrations: `uv run alembic upgrade head` - Create a new revision: `uv run alembic revision --autogenerate -m "description"` - Docker Compose runs migrations automatically on container start via `scripts/entrypoint.sh`. + - `scripts/entrypoint.sh` supports `APP_MODE=serve` (default) and `APP_MODE=migrate` (run migrations then exit). ### Environment configuration diff --git a/app/main.py b/app/main.py index 4ee8f7b..b176215 100644 --- a/app/main.py +++ b/app/main.py @@ -104,6 +104,12 @@ def root(): } +@app.get("/health") +def health(): + """ALB health-check endpoint.""" + return {"status": "ok"} + + if __name__ == "__main__": import uvicorn diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index bad94dd..cb78dc6 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail +MODE="${APP_MODE:-serve}" DB_URL="${DATABASE_URI:-}" if [ -z "${DB_URL}" ]; then echo "DATABASE_URI is not set; cannot run migrations." @@ -26,6 +27,16 @@ done echo "Running database migrations..." uv run alembic upgrade head +if [ "${MODE}" = "migrate" ]; then + echo "Migration-only mode complete." + exit 0 +fi + +if [ "${MODE}" != "serve" ]; then + echo "Unknown APP_MODE '${MODE}'. Expected 'serve' or 'migrate'." + exit 1 +fi + if [ "${ENVIRONMENT:-prod}" = "dev" ]; then echo "Starting application (dev mode with reload)..." exec uv run --frozen uvicorn app.main:app \ From 52085e5d66c26fa7960101d1e934278d6ad67a69 Mon Sep 17 00:00:00 2001 From: Uwe Winter Date: Mon, 23 Feb 2026 18:31:22 +1100 Subject: [PATCH 2/3] ci: add auto deployment functions --- .dockerignore | 3 - .github/workflows/build-and-deploy-dev.yml | 79 ++++++++++++++++++++++ .github/workflows/build-publish.yml | 49 ++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/build-and-deploy-dev.yml create mode 100644 .github/workflows/build-publish.yml diff --git a/.dockerignore b/.dockerignore index c00807e..0cd84e0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,9 +8,6 @@ __pycache__ htmlcov .env .env.* -Dockerfile* -docker-compose*.yml -README.md LICENSE tests docs diff --git a/.github/workflows/build-and-deploy-dev.yml b/.github/workflows/build-and-deploy-dev.yml new file mode 100644 index 0000000..fbdbb4b --- /dev/null +++ b/.github/workflows/build-and-deploy-dev.yml @@ -0,0 +1,79 @@ +name: build-and-deploy-dev + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + id-token: write + +env: + AWS_REGION: ap-southeast-2 + IMAGE_REPO: 868550873346.dkr.ecr.ap-southeast-2.amazonaws.com/atol-backend-dev + DEPLOY_FUNCTION_NAME: AtolBackendDevDeploymentFunction + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ECR_PUSH }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to ECR + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build and push dev image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.IMAGE_REPO }}:latest + ${{ env.IMAGE_REPO }}:${{ github.sha }} + provenance: false + sbom: false + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Deploy backend via Lambda + env: + IMAGE_TAG: latest + run: | + set -euo pipefail + PAYLOAD=$(jq -n --arg tag "${IMAGE_TAG}" '{tag: $tag}') + RESPONSE_FILE=$(mktemp) + + INVOKE_METADATA=$(aws lambda invoke \ + --function-name "${DEPLOY_FUNCTION_NAME}" \ + --payload "${PAYLOAD}" \ + --cli-binary-format raw-in-base64-out \ + --cli-read-timeout 0 \ + "${RESPONSE_FILE}") + + echo "${INVOKE_METADATA}" + cat "${RESPONSE_FILE}" + + FUNCTION_ERROR=$(echo "${INVOKE_METADATA}" | jq -r '.FunctionError // empty') + if [ -n "${FUNCTION_ERROR}" ]; then + echo "Deployment lambda reported an error: ${FUNCTION_ERROR}" >&2 + exit 1 + fi + + STATUS=$(jq -r '.status // empty' "${RESPONSE_FILE}") + if [ "${STATUS}" != "SUCCESS" ]; then + echo "Deployment lambda returned unexpected status: ${STATUS}" >&2 + exit 1 + fi diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml new file mode 100644 index 0000000..3a11a4b --- /dev/null +++ b/.github/workflows/build-publish.yml @@ -0,0 +1,49 @@ +name: build-and-push-release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: read + id-token: write + +env: + AWS_REGION: ap-southeast-2 + IMAGE_REPO: 868550873346.dkr.ecr.ap-southeast-2.amazonaws.com/atol-backend-dev + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ECR_PUSH }} + aws-region: ${{ env.AWS_REGION }} + + - name: Login to ECR + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build and push release image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.IMAGE_REPO }}:${{ github.ref_name }} + ${{ env.IMAGE_REPO }}:latest + provenance: false + sbom: false + cache-from: type=gha + cache-to: type=gha,mode=max From a45650f58bbb3b36cb122b8b6fde9e5d62b4c45a Mon Sep 17 00:00:00 2001 From: Uwe Winter Date: Mon, 23 Feb 2026 18:38:15 +1100 Subject: [PATCH 3/3] feat: add version endpoint --- .github/workflows/build-and-deploy-dev.yml | 2 ++ .github/workflows/build-publish.yml | 2 ++ Dockerfile | 2 ++ app/core/settings.py | 1 + app/main.py | 6 ++++++ 5 files changed, 13 insertions(+) diff --git a/.github/workflows/build-and-deploy-dev.yml b/.github/workflows/build-and-deploy-dev.yml index fbdbb4b..f82a295 100644 --- a/.github/workflows/build-and-deploy-dev.yml +++ b/.github/workflows/build-and-deploy-dev.yml @@ -39,6 +39,8 @@ jobs: context: . file: ./Dockerfile platforms: linux/amd64 + build-args: | + APP_VERSION=dev-${{ github.sha }} push: true tags: | ${{ env.IMAGE_REPO }}:latest diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml index 3a11a4b..33ce0ba 100644 --- a/.github/workflows/build-publish.yml +++ b/.github/workflows/build-publish.yml @@ -39,6 +39,8 @@ jobs: context: . file: ./Dockerfile platforms: linux/amd64 + build-args: | + APP_VERSION=${{ github.ref_name }} push: true tags: | ${{ env.IMAGE_REPO }}:${{ github.ref_name }} diff --git a/Dockerfile b/Dockerfile index 3e7c5bf..ef9ec6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim WORKDIR /app +ARG APP_VERSION=dev # Ensure uv installs into a local virtual environment for the project ENV UV_PROJECT_ENVIRONMENT=/app/.venv @@ -18,6 +19,7 @@ RUN chmod +x scripts/entrypoint.sh ENV PATH="/app/.venv/bin:${PATH}" ENV PYTHONPATH=/app ENV PORT=8000 +ENV APP_VERSION=${APP_VERSION} EXPOSE 8000 diff --git a/app/core/settings.py b/app/core/settings.py index 984e3b8..c878e50 100644 --- a/app/core/settings.py +++ b/app/core/settings.py @@ -35,6 +35,7 @@ class Settings(BaseSettings): # Environment ENVIRONMENT: Optional[str] = None # Options: "dev", "prod" + APP_VERSION: str = "dev" # Model config model_config = SettingsConfigDict( diff --git a/app/main.py b/app/main.py index b176215..238855d 100644 --- a/app/main.py +++ b/app/main.py @@ -110,6 +110,12 @@ def health(): return {"status": "ok"} +@app.get("/version") +def version(): + """Application version endpoint.""" + return {"version": settings.APP_VERSION} + + if __name__ == "__main__": import uvicorn