Skip to content
Merged
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
263 changes: 263 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
name: Blue-Green Deployment

on:
push:
branches: [main]

workflow_dispatch:
inputs:
rollback:
description: 'Rollback to previous environment'
required: false
type: boolean
default: false

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
SMOKE_TEST_URL_BLUE: https://blue.stellarpay.io/health
SMOKE_TEST_URL_GREEN: https://green.stellarpay.io/health
PRODUCTION_URL: https://api.stellarpay.io/health
CANARY_PERCENTAGE: 10

jobs:
# ---------------------------------------------------------------------------
# Build & Test
# ---------------------------------------------------------------------------
build:
name: Build and Test
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.image_tag.outputs.tag }}
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10

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

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

- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm typecheck

- name: Build
run: pnpm build

- name: Test
run: pnpm test -- --coverage

- name: Generate image tag
id: image_tag
run: echo "tag=$(echo ${{ github.sha }} | cut -c1-7)" >> "$GITHUB_OUTPUT"

- name: Build Docker image
run: docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image_tag.outputs.tag }} -f apps/api/Dockerfile .

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Push Docker image
run: docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image_tag.outputs.tag }}

# ---------------------------------------------------------------------------
# Blue-Green Deploy
# ---------------------------------------------------------------------------
deploy:
name: Blue-Green Deploy
needs: build
runs-on: ubuntu-latest
outputs:
active_env: ${{ steps.determine_env.outputs.active_env }}
previous_env: ${{ steps.determine_env.outputs.previous_env }}
steps:
- uses: actions/checkout@v4

- name: Determine active environment
id: determine_env
run: |
CURRENT_ACTIVE=$(curl -s ${{ env.PRODUCTION_URL }} | jq -r '.deployment_env // "blue"')
if [ "$CURRENT_ACTIVE" = "blue" ]; then
echo "active_env=blue" >> "$GITHUB_OUTPUT"
echo "previous_env=green" >> "$GITHUB_OUTPUT"
echo "target_env=green" >> "$GITHUB_OUTPUT"
else
echo "active_env=green" >> "$GITHUB_OUTPUT"
echo "previous_env=blue" >> "$GITHUB_OUTPUT"
echo "target_env=blue" >> "$GITHUB_OUTPUT"
fi

- name: Deploy to target environment
run: |
TARGET=${{ steps.determine_env.outputs.target_env }}
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag }}"
echo "Deploying $IMAGE to $TARGET environment"

# Deploy to target (stub - replace with actual deployment commands)
# Example: Update k8s manifest and apply
# kubectl set image deployment/api-$TARGET api=$IMAGE -n stellar-pay
# kubectl rollout status deployment/api-$TARGET -n stellar-pay

# Record the deployment
echo "Deployed to $TARGET at $(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> deploy-log.txt

- name: Smoke test - target environment
run: |
TARGET=${{ steps.determine_env.outputs.target_env }}
if [ "$TARGET" = "blue" ]; then
URL="${{ env.SMOKE_TEST_URL_BLUE }}"
else
URL="${{ env.SMOKE_TEST_URL_GREEN }}"
fi

echo "Running smoke tests against $URL"
for i in $(seq 1 10); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || true)
if [ "$HTTP_CODE" = "200" ]; then
echo "Smoke test passed (attempt $i)"
exit 0
fi
echo "Attempt $i: HTTP $HTTP_CODE, retrying in 5s..."
sleep 5
done
echo "Smoke test failed after 10 attempts"
exit 1

- name: Canary release
run: |
TARGET=${{ steps.determine_env.outputs.target_env }}

# Route CANARY_PERCENTAGE of traffic to the new environment
# (stub - replace with load balancer config update)
# Example: Update nginx upstream with weighted routing
# ./scripts/update-traffic.sh $TARGET ${{ env.CANARY_PERCENTAGE }}

echo "Routing ${{ env.CANARY_PERCENTAGE }}% traffic to $TARGET environment"
sleep 15

- name: Canary smoke test
run: |
TARGET=${{ steps.determine_env.outputs.target_env }}
ERRORS=0

for i in $(seq 1 20); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${{ env.PRODUCTION_URL }}" || true)
if [ "$HTTP_CODE" != "200" ]; then
ERRORS=$((ERRORS + 1))
fi
sleep 3
done

if [ "$ERRORS" -gt 3 ]; then
echo "Canary check failed: $ERRORS errors detected"
exit 1
fi
echo "Canary check passed: $ERRORS errors in 20 requests"

- name: Switch traffic to target environment
run: |
TARGET=${{ steps.determine_env.outputs.target_env }}

# Route 100% of traffic to the new environment
# (stub - replace with load balancer config update)
# Example: ./scripts/update-traffic.sh $TARGET 100

echo "Switching 100% traffic to $TARGET environment"
sleep 5

- name: Final smoke test on production URL
run: |
for i in $(seq 1 5); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${{ env.PRODUCTION_URL }}" || true)
if [ "$HTTP_CODE" = "200" ]; then
echo "Production smoke test passed"
exit 0
fi
sleep 5
done
echo "Production smoke test failed"
exit 1

- name: Cleanup previous environment
if: success()
run: |
PREV=${{ steps.determine_env.outputs.previous_env }}
echo "Scaling down previous environment: $PREV"
# kubectl scale deployment/api-$PREV --replicas=0 -n stellar-pay

- name: Notify deployment success
if: success()
run: |
echo "Deployment completed successfully"
echo "Active environment: ${{ steps.determine_env.outputs.target_env }}"
echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag }}"

# ---------------------------------------------------------------------------
# Rollback (manual trigger)
# ---------------------------------------------------------------------------
rollback:
name: Rollback
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.rollback == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Determine rollback target
id: rollback_env
run: |
CURRENT_ACTIVE=$(curl -s ${{ env.PRODUCTION_URL }} | jq -r '.deployment_env // "blue"')
if [ "$CURRENT_ACTIVE" = "blue" ]; then
echo "Rolling back from blue to green"
echo "target_env=green" >> "$GITHUB_OUTPUT"
else
echo "Rolling back from green to blue"
echo "target_env=blue" >> "$GITHUB_OUTPUT"
fi

- name: Restore previous environment
run: |
TARGET=${{ steps.rollback_env.outputs.target_env }}
echo "Restoring $TARGET environment"
# kubectl rollout undo deployment/api-$TARGET -n stellar-pay

- name: Smoke test after rollback
run: |
TARGET=${{ steps.rollback_env.outputs.target_env }}
if [ "$TARGET" = "blue" ]; then
URL="${{ env.SMOKE_TEST_URL_BLUE }}"
else
URL="${{ env.SMOKE_TEST_URL_GREEN }}"
fi

for i in $(seq 1 10); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$URL" || true)
if [ "$HTTP_CODE" = "200" ]; then
echo "Rollback smoke test passed"
exit 0
fi
sleep 5
done
echo "Rollback smoke test failed"
exit 1

- name: Switch traffic back
run: |
TARGET=${{ steps.rollback_env.outputs.target_env }}
echo "Switching 100% traffic back to $TARGET"
# ./scripts/update-traffic.sh $TARGET 100

- name: Notify rollback
run: echo "Rollback to ${{ steps.rollback_env.outputs.target_env }} completed"
Loading