Skip to content
Open
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
60 changes: 60 additions & 0 deletions .github/actions/deploy-app/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Deploy Backend App
description: Deploys a Docker image to the target environment and verifies health.

inputs:
image_tag:
description: Full Docker image tag to deploy
required: true
environment:
description: Target environment (staging | production)
required: true
git_sha:
description: Git SHA being deployed
required: true
deploy_token:
description: Deployment platform token / webhook secret
required: true
app_url:
description: Base URL of the deployed app (for health check)
required: true

runs:
using: composite
steps:
- name: Trigger deployment
shell: bash
env:
DEPLOY_TOKEN: ${{ inputs.deploy_token }}
run: |
# Replace this curl with your actual deploy platform call
# (Render deploy hook, Railway redeploy, Fly.io deploy, etc.)
# The IMAGE_TAG and GIT_SHA are passed so the platform can pull the right image.
curl -fsSL -X POST "${{ inputs.app_url }}/_deploy" \
-H "Authorization: Bearer ${DEPLOY_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"image": "${{ inputs.image_tag }}",
"sha": "${{ inputs.git_sha }}",
"environment": "${{ inputs.environment }}"
}' || true # remove `|| true` once real endpoint is wired up

- name: Wait for health check
shell: bash
run: |
echo "Waiting for ${{ inputs.app_url }}/health ..."
for i in $(seq 1 24); do
STATUS=$(curl -o /dev/null -s -w "%{http_code}" "${{ inputs.app_url }}/health" || echo "000")
if [ "$STATUS" = "200" ]; then
echo "Health check passed (attempt ${i})"
exit 0
fi
echo "Attempt ${i}/24 — status ${STATUS}, retrying in 10s..."
sleep 10
done
echo "::error::Health check failed after 4 minutes"
exit 1

- name: Log release metadata
shell: bash
run: |
echo "::notice title=Release [${{ inputs.environment }}]::image=${{ inputs.image_tag }} | sha=${{ inputs.git_sha }} | env=${{ inputs.environment }} | ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
52 changes: 52 additions & 0 deletions .github/actions/run-migrations/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Run Supabase Migrations
description: Applies pending Supabase migrations exactly once and logs the result.

inputs:
supabase_url:
description: Supabase project URL
required: true
supabase_service_key:
description: Supabase service-role key
required: true
environment:
description: Target environment (staging | production)
required: true
git_sha:
description: Git SHA being deployed
required: true

runs:
using: composite
steps:
- name: Install Supabase CLI
shell: bash
run: |
curl -fsSL https://github.com/supabase/cli/releases/latest/download/supabase_linux_amd64.tar.gz \
| tar -xz -C /usr/local/bin supabase

- name: Link project
shell: bash
env:
SUPABASE_ACCESS_TOKEN: ${{ inputs.supabase_service_key }}
run: |
supabase link --project-ref "$(echo '${{ inputs.supabase_url }}' | sed 's|https://||;s|\.supabase\.co.*||')"

- name: Run migrations
id: migrate
shell: bash
env:
SUPABASE_ACCESS_TOKEN: ${{ inputs.supabase_service_key }}
run: |
echo "::group::Migration output"
supabase db push --include-all 2>&1 | tee /tmp/migration.log
echo "::endgroup::"

STATUS="${PIPESTATUS[0]}"
APPLIED=$(grep -c 'Applying migration' /tmp/migration.log || true)
echo "migrations_applied=${APPLIED}" >> "$GITHUB_OUTPUT"
exit "$STATUS"

- name: Log migration metadata
shell: bash
run: |
echo "::notice title=Migration [${{ inputs.environment }}]::Applied ${{ steps.migrate.outputs.migrations_applied }} migration(s) | sha=${{ inputs.git_sha }} | env=${{ inputs.environment }} | ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
93 changes: 93 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Backend CI

on:
push:
branches: [main, develop, 'feat/**', 'fix/**']
paths:
- 'app/backend/**'
- '.github/workflows/backend-ci.yml'
pull_request:
branches: [main, develop]
paths:
- 'app/backend/**'
- '.github/workflows/backend-ci.yml'

jobs:
ci:
name: Lint, Type-check & Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: app/backend

services:
supabase:
image: supabase/postgres:15.1.0.147
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: quickex_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
NODE_ENV: test
STELLAR_NETWORK: testnet
SUPABASE_URL: http://localhost:54321
SUPABASE_ANON_KEY: test-anon-key
PORT: 4000

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
<<<<<<< Updated upstream
with:
version: 9
run_install: false
=======
>>>>>>> Stashed changes

- uses: actions/setup-node@v4
with:
node-version: '20'

- name: Get pnpm store path
id: pnpm-cache
run: echo "store=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"

- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.store }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('app/backend/pnpm-lock.yaml') }}
restore-keys: pnpm-store-${{ runner.os }}-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm run lint

- name: Type-check
run: pnpm run type-check

- name: Build
run: pnpm run build

- name: Unit tests
run: pnpm run test:unit

- name: Integration tests
run: pnpm run test:int

- name: Upload coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-${{ github.sha }}
path: app/backend/coverage/
retention-days: 7
157 changes: 157 additions & 0 deletions .github/workflows/backend-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: Backend Release

on:
push:
branches: [main]
paths:
- 'app/backend/**'
- '.github/workflows/backend-release.yml'
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options: [staging, production]
rollback_sha:
description: 'Git SHA to roll back to (leave empty for normal deploy)'
required: false

concurrency:
group: backend-release-${{ github.event.inputs.environment || 'staging' }}
cancel-in-progress: false

jobs:
# ─── 1. Build & publish Docker image ────────────────────────────────────────
build:
name: Build Image
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.image_tag }}
git_sha: ${{ github.sha }}
steps:
- uses: actions/checkout@v4

- name: Set image metadata
id: meta
run: |
SHORT_SHA="${GITHUB_SHA::8}"
echo "image_tag=ghcr.io/${{ github.repository_owner }}/quickex-backend:${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "SHORT_SHA=${SHORT_SHA}" >> "$GITHUB_ENV"

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: app/backend
push: true
tags: |
${{ steps.meta.outputs.image_tag }}
ghcr.io/${{ github.repository_owner }}/quickex-backend:latest
labels: |
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}

# ─── 2. Deploy to staging ───────────────────────────────────────────────────
deploy-staging:
name: Deploy → Staging
needs: build
runs-on: ubuntu-latest
environment: staging
env:
DEPLOY_ENV: staging
IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
GIT_SHA: ${{ needs.build.outputs.git_sha }}

steps:
- uses: actions/checkout@v4

- name: Run migrations (staging)
uses: ./.github/actions/run-migrations
with:
supabase_url: ${{ secrets.STAGING_SUPABASE_URL }}
supabase_service_key: ${{ secrets.STAGING_SUPABASE_SERVICE_KEY }}
environment: staging
git_sha: ${{ env.GIT_SHA }}

- name: Deploy app (staging)
uses: ./.github/actions/deploy-app
with:
image_tag: ${{ env.IMAGE_TAG }}
environment: staging
git_sha: ${{ env.GIT_SHA }}
deploy_token: ${{ secrets.STAGING_DEPLOY_TOKEN }}
app_url: ${{ secrets.STAGING_APP_URL }}

# ─── 3. Deploy to production (manual gate) ──────────────────────────────────
deploy-production:
name: Deploy → Production
needs: [build, deploy-staging]
runs-on: ubuntu-latest
environment: production # requires manual approval in GitHub Environments
if: github.event_name == 'push' || github.event.inputs.environment == 'production'
env:
DEPLOY_ENV: production
IMAGE_TAG: ${{ needs.build.outputs.image_tag }}
GIT_SHA: ${{ needs.build.outputs.git_sha }}

steps:
- uses: actions/checkout@v4

- name: Run migrations (production)
uses: ./.github/actions/run-migrations
with:
supabase_url: ${{ secrets.PROD_SUPABASE_URL }}
supabase_service_key: ${{ secrets.PROD_SUPABASE_SERVICE_KEY }}
environment: production
git_sha: ${{ env.GIT_SHA }}

- name: Deploy app (production)
uses: ./.github/actions/deploy-app
with:
image_tag: ${{ env.IMAGE_TAG }}
environment: production
git_sha: ${{ env.GIT_SHA }}
deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}
app_url: ${{ secrets.PROD_APP_URL }}

# ─── 4. Rollback (manual trigger only) ──────────────────────────────────────
rollback:
name: Rollback
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' && github.event.inputs.rollback_sha != ''
environment: ${{ github.event.inputs.environment }}
env:
TARGET_ENV: ${{ github.event.inputs.environment }}
ROLLBACK_SHA: ${{ github.event.inputs.rollback_sha }}

steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.ROLLBACK_SHA }}

- name: Resolve rollback image
id: rollback_image
run: |
SHORT_SHA="${ROLLBACK_SHA::8}"
echo "image_tag=ghcr.io/${{ github.repository_owner }}/quickex-backend:${SHORT_SHA}" >> "$GITHUB_OUTPUT"

- name: Deploy previous image
uses: ./.github/actions/deploy-app
with:
image_tag: ${{ steps.rollback_image.outputs.image_tag }}
environment: ${{ env.TARGET_ENV }}
git_sha: ${{ env.ROLLBACK_SHA }}
deploy_token: ${{ env.TARGET_ENV == 'production' && secrets.PROD_DEPLOY_TOKEN || secrets.STAGING_DEPLOY_TOKEN }}
app_url: ${{ env.TARGET_ENV == 'production' && secrets.PROD_APP_URL || secrets.STAGING_APP_URL }}

- name: Log rollback event
run: |
echo "::notice title=Rollback::Rolled ${{ env.TARGET_ENV }} back to ${{ env.ROLLBACK_SHA }} at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
Loading