|
| 1 | +name: Deploy Backend |
| 2 | + |
| 3 | +on: |
| 4 | + workflow_dispatch: |
| 5 | + push: |
| 6 | + branches: [main] |
| 7 | + paths: |
| 8 | + - "backend/**" |
| 9 | + - "src/fall_in/core/**" |
| 10 | + - "src/fall_in/ai/**" |
| 11 | + - "src/fall_in/net/**" |
| 12 | + - "src/fall_in/multiplayer/models.py" |
| 13 | + |
| 14 | +# Only one deployment at a time. |
| 15 | +concurrency: |
| 16 | + group: deploy-backend |
| 17 | + cancel-in-progress: true |
| 18 | + |
| 19 | +jobs: |
| 20 | + test: |
| 21 | + name: Pre-deploy Test |
| 22 | + runs-on: ubuntu-latest |
| 23 | + defaults: |
| 24 | + run: |
| 25 | + shell: bash |
| 26 | + working-directory: backend |
| 27 | + |
| 28 | + steps: |
| 29 | + - uses: actions/checkout@v6 |
| 30 | + |
| 31 | + - name: Set up Python 3.12 |
| 32 | + uses: actions/setup-python@v6 |
| 33 | + with: |
| 34 | + python-version: "3.12" |
| 35 | + |
| 36 | + - name: Install uv |
| 37 | + uses: astral-sh/setup-uv@v7 |
| 38 | + |
| 39 | + - name: Install dependencies |
| 40 | + run: uv sync --extra dev |
| 41 | + |
| 42 | + - name: Lint |
| 43 | + run: uv run ruff check app/ tests/ |
| 44 | + |
| 45 | + - name: Test |
| 46 | + run: uv run pytest -x -q |
| 47 | + |
| 48 | + deploy: |
| 49 | + name: Deploy to OCI |
| 50 | + needs: test |
| 51 | + runs-on: ubuntu-latest |
| 52 | + |
| 53 | + steps: |
| 54 | + - uses: actions/checkout@v6 |
| 55 | + |
| 56 | + - name: Set up SSH |
| 57 | + run: | |
| 58 | + mkdir -p ~/.ssh |
| 59 | + echo "${{ secrets.OCI_SSH_KEY }}" > ~/.ssh/id_ed25519 |
| 60 | + chmod 600 ~/.ssh/id_ed25519 |
| 61 | + ssh-keyscan -H "${{ secrets.OCI_HOST }}" >> ~/.ssh/known_hosts |
| 62 | +
|
| 63 | + - name: Sync source to server |
| 64 | + run: | |
| 65 | + rsync -azP --delete \ |
| 66 | + --include='backend/***' \ |
| 67 | + --include='src/fall_in/core/***' \ |
| 68 | + --include='src/fall_in/ai/***' \ |
| 69 | + --include='src/fall_in/net/***' \ |
| 70 | + --include='src/fall_in/multiplayer/models.py' \ |
| 71 | + --include='src/fall_in/__init__.py' \ |
| 72 | + --include='src/fall_in/multiplayer/__init__.py' \ |
| 73 | + --include='src/' \ |
| 74 | + --include='src/fall_in/' \ |
| 75 | + --include='src/fall_in/multiplayer/' \ |
| 76 | + --include='pyproject.toml' \ |
| 77 | + --include='uv.lock' \ |
| 78 | + --include='data/***' \ |
| 79 | + --exclude='*' \ |
| 80 | + ./ "${{ secrets.OCI_USER }}@${{ secrets.OCI_HOST }}:~/fall-in/" |
| 81 | +
|
| 82 | + - name: Build & restart on server |
| 83 | + run: | |
| 84 | + ssh "${{ secrets.OCI_USER }}@${{ secrets.OCI_HOST }}" << 'DEPLOY_SCRIPT' |
| 85 | + set -e |
| 86 | + cd ~/fall-in |
| 87 | +
|
| 88 | + # Build Docker image (ARM-native on Ampere A1). |
| 89 | + docker build -t fall-in-backend -f backend/Dockerfile . |
| 90 | +
|
| 91 | + # Stop existing container (if any) and start fresh. |
| 92 | + docker stop fall-in-backend 2>/dev/null || true |
| 93 | + docker rm fall-in-backend 2>/dev/null || true |
| 94 | +
|
| 95 | + docker run -d \ |
| 96 | + --name fall-in-backend \ |
| 97 | + --restart unless-stopped \ |
| 98 | + --env-file ~/fall-in/backend/.env \ |
| 99 | + --network host \ |
| 100 | + fall-in-backend |
| 101 | +
|
| 102 | + # Wait for health check. |
| 103 | + echo "Waiting for health check..." |
| 104 | + for i in $(seq 1 15); do |
| 105 | + if curl -sf http://localhost:8000/healthz > /dev/null 2>&1; then |
| 106 | + echo "Health check passed." |
| 107 | + exit 0 |
| 108 | + fi |
| 109 | + sleep 2 |
| 110 | + done |
| 111 | + echo "Health check failed after 30s" |
| 112 | + docker logs fall-in-backend --tail 30 |
| 113 | + exit 1 |
| 114 | + DEPLOY_SCRIPT |
| 115 | +
|
| 116 | + - name: Clean up old Docker images |
| 117 | + if: success() |
| 118 | + run: | |
| 119 | + ssh "${{ secrets.OCI_USER }}@${{ secrets.OCI_HOST }}" \ |
| 120 | + 'docker image prune -f' |
0 commit comments