Skip to content

Merge pull request #217 from Open-Finance-Lab/fingpt_backend_prod #46

Merge pull request #217 from Open-Finance-Lab/fingpt_backend_prod

Merge pull request #217 from Open-Finance-Lab/fingpt_backend_prod #46

name: Backend CI and Deploy
on:
push:
branches:
- main
workflow_dispatch:
env:
BACKEND_DIR: Main/backend
PYTHON_VERSION: "3.12"
REGISTRY: ghcr.io
DROPLET_HOST: agenticfinsearch.org
DROPLET_USER: deploy
SYSTEMD_UNIT: fingpt-api
RUN_TESTS: "false" # Toggle to "true" to re-enable backend tests
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.meta.outputs.image }}
sha_tag: ${{ steps.meta.outputs.sha_tag }}
main_tag: ${{ steps.meta.outputs.main_tag }}
latest_tag: ${{ steps.meta.outputs.latest_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
- name: Install Python ${{ env.PYTHON_VERSION }}
run: uv python install ${{ env.PYTHON_VERSION }}
- name: Sync backend dependencies
working-directory: ${{ env.BACKEND_DIR }}
run: uv sync --frozen --python ${{ env.PYTHON_VERSION }}
- name: Run Django system checks
working-directory: ${{ env.BACKEND_DIR }}
run: uv run python manage.py check
- name: Verify deployment readiness
working-directory: ${{ env.BACKEND_DIR }}
run: uv run python verify_deployment.py
- name: Compute image tags
id: meta
run: |
REPO_SLUG=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
IMAGE_ID="${{ env.REGISTRY }}/${REPO_SLUG}-backend"
echo "image=$IMAGE_ID" >> "$GITHUB_OUTPUT"
echo "sha_tag=${IMAGE_ID}:${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "main_tag=${IMAGE_ID}:main" >> "$GITHUB_OUTPUT"
echo "latest_tag=${IMAGE_ID}:latest" >> "$GITHUB_OUTPUT"
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build backend image
run: |
docker build \
--file ${{ env.BACKEND_DIR }}/Dockerfile \
--tag ${{ steps.meta.outputs.sha_tag }} \
--tag ${{ steps.meta.outputs.main_tag }} \
--tag ${{ steps.meta.outputs.latest_tag }} \
${{ env.BACKEND_DIR }}
- name: Push backend image
run: |
docker push ${{ steps.meta.outputs.sha_tag }}
docker push ${{ steps.meta.outputs.main_tag }}
docker push ${{ steps.meta.outputs.latest_tag }}
test:
runs-on: ubuntu-latest
needs: build
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Tests temporarily disabled
if: ${{ env.RUN_TESTS != 'true' }}
run: echo "::warning::Backend test suite is currently skipped. Set RUN_TESTS to true when ready to re-enable it."
- name: Set up uv
if: ${{ env.RUN_TESTS == 'true' }}
uses: astral-sh/setup-uv@v3
- name: Install Python ${{ env.PYTHON_VERSION }}
if: ${{ env.RUN_TESTS == 'true' }}
run: uv python install ${{ env.PYTHON_VERSION }}
- name: Sync backend dependencies
if: ${{ env.RUN_TESTS == 'true' }}
working-directory: ${{ env.BACKEND_DIR }}
run: uv sync --frozen --python ${{ env.PYTHON_VERSION }}
- name: Run backend tests
if: ${{ env.RUN_TESTS == 'true' }}
working-directory: ${{ env.BACKEND_DIR }}
run: uv run python manage.py test
deploy:
runs-on: ubuntu-latest
needs:
- build
- test
permissions:
contents: read
steps:
- name: Determine deploy eligibility
id: deploy-gate
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
GHCR_READ_TOKEN: ${{ secrets.GHCR_READ_TOKEN }}
run: |
set -euo pipefail
if [[ -n "${DEPLOY_SSH_KEY:-}" && -n "${GHCR_READ_TOKEN:-}" ]]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi
- name: Deploy to Fedora droplet
if: ${{ steps.deploy-gate.outputs.enabled == 'true' }}
uses: appleboy/ssh-action@v0.1.10
env:
REMOTE_IMAGE: ${{ needs.build.outputs.main_tag }}
GHCR_USERNAME: ${{ github.repository_owner }}
GHCR_TOKEN: ${{ secrets.GHCR_READ_TOKEN }}
with:
host: ${{ env.DROPLET_HOST }}
username: ${{ env.DROPLET_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: 22
envs: REMOTE_IMAGE,GHCR_USERNAME,GHCR_TOKEN,SYSTEMD_UNIT
script: |
set -euo pipefail
export XDG_RUNTIME_DIR=/run/user/$(id -u)
podman login ${{ env.REGISTRY }} -u "$GHCR_USERNAME" -p "$GHCR_TOKEN"
podman pull "$REMOTE_IMAGE"
systemctl --user daemon-reload
systemctl --user restart "$SYSTEMD_UNIT"
podman image prune -f
- name: Verify deployment health
if: ${{ steps.deploy-gate.outputs.enabled == 'true' }}
uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ env.DROPLET_HOST }}
username: ${{ env.DROPLET_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
port: 22
envs: SYSTEMD_UNIT
script: |
set -euo pipefail
export XDG_RUNTIME_DIR=/run/user/$(id -u)
echo "Waiting for service to start..."
sleep 5
echo "Checking service status..."
systemctl --user is-active "$SYSTEMD_UNIT" || {
echo "ERROR: Service is not active"
systemctl --user status "$SYSTEMD_UNIT" || true
exit 1
}
echo "Checking health endpoint..."
curl -f -m 10 http://localhost:8000/health/ || {
echo "ERROR: Health check failed"
exit 1
}
echo "Deployment verification successful!"