| 1 | name: Test & Deploy
|
| 2 |
|
| 3 | on:
|
| 4 | push:
|
| 5 | branches:
|
| 6 | - master
|
| 7 | - production
|
| 8 | paths:
|
| 9 | - 'api/**'
|
| 10 | - '.github/workflows/deploy.yml'
|
| 11 | pull_request:
|
| 12 | paths:
|
| 13 | - 'api/**'
|
| 14 | - '.github/workflows/deploy.yml'
|
| 15 |
|
| 16 | env:
|
| 17 | REGISTRY: ghcr.io
|
| 18 | IMAGE_NAME: ${{ github.repository }}
|
| 19 |
|
| 20 | jobs:
|
| 21 | test:
|
| 22 | name: Run test suite
|
| 23 | runs-on: ubuntu-latest
|
| 24 |
|
| 25 | services:
|
| 26 | postgres:
|
| 27 | image: postgres:17-alpine
|
| 28 | env:
|
| 29 | POSTGRES_USER: user
|
| 30 | POSTGRES_PASSWORD: password
|
| 31 | POSTGRES_DB: postgres
|
| 32 | options: >-
|
| 33 | --health-cmd "pg_isready -U user"
|
| 34 | --health-interval 10s
|
| 35 | --health-timeout 5s
|
| 36 | --health-retries 5
|
| 37 | ports:
|
| 38 | - 5432:5432
|
| 39 |
|
| 40 | defaults:
|
| 41 | run:
|
| 42 | working-directory: api
|
| 43 |
|
| 44 | env:
|
| 45 | DATABASE_URL: postgres://user:password@localhost:5432/orbit_test
|
| 46 | NODE_ENV: test
|
| 47 | JWT_SECRET: test-secret-key
|
| 48 | FRONTEND_URL: http://localhost:8081
|
| 49 |
|
| 50 | steps:
|
| 51 | - uses: actions/checkout@v4
|
| 52 |
|
| 53 | - uses: actions/setup-node@v4
|
| 54 | with:
|
| 55 | node-version: '24'
|
| 56 | cache: 'npm'
|
| 57 | cache-dependency-path: api/package-lock.json
|
| 58 |
|
| 59 | - name: Install dependencies
|
| 60 | run: npm ci
|
| 61 |
|
| 62 | - name: Set up test database
|
| 63 | run: |
|
| 64 | node scripts/create-test-db.mjs
|
| 65 | npx prisma generate
|
| 66 | npx prisma db push
|
| 67 |
|
| 68 | - name: Run tests
|
| 69 | run: npx vitest run
|
| 70 |
|
| 71 | build-and-push:
|
| 72 | name: Build & push image
|
| 73 | needs: test
|
| 74 | if: github.event_name == 'push'
|
| 75 | runs-on: ubuntu-latest
|
| 76 | permissions:
|
| 77 | contents: read
|
| 78 | packages: write
|
| 79 |
|
| 80 | outputs:
|
| 81 | image_tag: ${{ steps.meta.outputs.version }}
|
| 82 |
|
| 83 | steps:
|
| 84 | - name: Checkout
|
| 85 | uses: actions/checkout@v4
|
| 86 |
|
| 87 | - name: Log in to GHCR
|
| 88 | uses: docker/login-action@v3
|
| 89 | with:
|
| 90 | registry: ${{ env.REGISTRY }}
|
| 91 | username: ${{ github.actor }}
|
| 92 | password: ${{ secrets.GHCR_TOKEN }}
|
| 93 |
|
| 94 | - name: Docker metadata
|
| 95 | id: meta
|
| 96 | uses: docker/metadata-action@v5
|
| 97 | with:
|
| 98 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
| 99 | tags: |
|
| 100 | # main branch => tag "latest"
|
| 101 | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
|
| 102 | # production branch => tag "production"
|
| 103 | # type=raw,value=production,enable=${{ github.ref == 'refs/heads/production' }}
|
| 104 | # always tag with short SHA for traceability
|
| 105 | type=sha,prefix=,format=short
|
| 106 |
|
| 107 | - name: Build and push
|
| 108 | uses: docker/build-push-action@v5
|
| 109 | with:
|
| 110 | context: ./api
|
| 111 | file: ./api/Dockerfile.production
|
| 112 | push: true
|
| 113 | tags: ${{ steps.meta.outputs.tags }}
|
| 114 | labels: ${{ steps.meta.outputs.labels }}
|
| 115 |
|
| 116 | deploy-production:
|
| 117 | name: Deploy to production
|
| 118 | needs: build-and-push
|
| 119 | runs-on: ubuntu-latest
|
| 120 | if: github.ref == 'refs/heads/master'
|
| 121 | environment: production
|
| 122 |
|
| 123 | steps:
|
| 124 | - name: Deploy via SSH
|
| 125 | uses: appleboy/ssh-action@v1
|
| 126 | with:
|
| 127 | host: ${{ secrets.VPS_HOST }}
|
| 128 | username: ${{ secrets.VPS_USER }}
|
| 129 | key: ${{ secrets.VPS_SSH_KEY }}
|
| 130 | script: |
|
| 131 | set -e
|
| 132 | cd /srv/app/production
|
| 133 |
|
| 134 | docker pull ghcr.io/${{ github.repository }}:latest
|
| 135 |
|
| 136 | sed -i "s/^IMAGE_TAG=.*/IMAGE_TAG=latest/" .env
|
| 137 |
|
| 138 | docker compose up -d --no-deps --force-recreate api
|
| 139 |
|
| 140 | # old image layers pile up across deploys; keep roughly the last week
|
| 141 | docker image prune -af --filter "until=168h" >/dev/null 2>&1 || true
|
| 142 |
|
| 143 | sleep 5
|
| 144 | docker compose logs --tail=50 api
|