Skip to content

Commit 54e61bd

Browse files
amosttAygentic
andcommitted
feat(deploy): add GHCR deployment workflows with image-based promotion [AYG-73]
Replace legacy self-hosted Docker Compose deploy workflows with GHCR-based CI/CD pipeline. Staging builds and pushes to GHCR on every push to main. Production promotes the exact staging image by re-tagging (no rebuild), guaranteeing identical binaries across environments. Key changes: - deploy-staging.yml: ubuntu-latest, docker/build-push-action, SHA+staging tags, concurrency with cancel-in-progress, 5 pluggable deploy blocks - deploy-production.yml: image promotion (pull→re-tag→push), semver+latest tags, concurrency without cancel-in-progress, 5 pluggable deploy blocks - .env.example: added Deployment (CI/CD) section with platform secret placeholders - README.md: rewritten with full deployment docs (environment model, GHCR tagging, staging/production flows, rollback, pre-production checklist, Supabase migrations) - docs/deployment/ci-pipeline.md: updated staging/production sections, secrets tables, self-hosted runners now optional - docs/deployment/environments.md: updated deploy processes, rollback procedures, health check URLs, platform-specific secrets Pluggable deploy targets: Railway, Alibaba Cloud (ACR+ECS), Google Cloud Run, Fly.io, Self-hosted (Docker Compose via SSH). Fixes AYG-73 Related to AYG-64 🤖 Generated by Aygentic Co-Authored-By: Aygentic <noreply@aygentic.com>
1 parent 1de2777 commit 54e61bd

File tree

6 files changed

+627
-286
lines changed

6 files changed

+627
-286
lines changed

.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,18 @@ DOCKER_IMAGE_FRONTEND=frontend
4040
SENTRY_DSN=
4141
# GIT_COMMIT= # Set automatically by CI (git commit SHA)
4242
# BUILD_TIME= # Set automatically by CI (build timestamp)
43+
44+
# ─── Deployment (CI/CD) ─────────────────────────────────────────────────────
45+
# These variables are used by GitHub Actions deployment workflows.
46+
# GHCR authentication uses GITHUB_TOKEN (automatic in GitHub Actions).
47+
#
48+
# Platform-specific deploy tokens (uncomment one for your platform):
49+
# RAILWAY_TOKEN= # Railway deploy token
50+
# RAILWAY_SERVICE_ID_STAGING= # Railway service ID for staging
51+
# RAILWAY_SERVICE_ID_PRODUCTION= # Railway service ID for production
52+
# GCP_SA_KEY= # Google Cloud service account key (JSON)
53+
# GCP_SERVICE_NAME= # Google Cloud Run service name
54+
# FLY_API_TOKEN= # Fly.io API token
55+
# DEPLOY_HOST= # Self-hosted: SSH host for deployment
56+
# ALIBABA_ACCESS_KEY= # Alibaba Cloud access key
57+
# ALIBABA_SECRET_KEY= # Alibaba Cloud secret key

.github/workflows/deploy-production.yml

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,90 @@ name: Deploy to Production
22

33
on:
44
release:
5-
types:
6-
- published
5+
types: [published]
6+
7+
concurrency:
8+
group: deploy-production
9+
cancel-in-progress: false
710

811
jobs:
912
deploy:
10-
if: github.repository_owner != 'fastapi'
11-
runs-on:
12-
- self-hosted
13-
- production
14-
env:
15-
ENVIRONMENT: production
16-
DOMAIN: ${{ secrets.DOMAIN_PRODUCTION }}
17-
STACK_NAME: ${{ secrets.STACK_NAME_PRODUCTION }}
18-
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
19-
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
20-
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
21-
BACKEND_CORS_ORIGINS: ${{ secrets.BACKEND_CORS_ORIGINS }}
22-
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
23-
SERVICE_NAME: ${{ secrets.SERVICE_NAME }}
24-
DOCKER_IMAGE_BACKEND: ${{ secrets.DOCKER_IMAGE_BACKEND }}
25-
DOCKER_IMAGE_FRONTEND: ${{ secrets.DOCKER_IMAGE_FRONTEND }}
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
packages: write
2617
steps:
2718
- name: Checkout
2819
uses: actions/checkout@v6
29-
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} build
30-
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_PRODUCTION }} up -d
20+
21+
- name: Log in to GHCR
22+
uses: docker/login-action@v3
23+
with:
24+
registry: ghcr.io
25+
username: ${{ github.actor }}
26+
password: ${{ secrets.GITHUB_TOKEN }}
27+
28+
- name: Verify staging image exists in GHCR
29+
env:
30+
IMAGE: ghcr.io/${{ github.repository }}/backend:${{ github.sha }}
31+
run: |
32+
docker manifest inspect "${IMAGE}" > /dev/null || \
33+
(echo "ERROR: Staging image not found for SHA ${{ github.sha }}. Ensure the staging deploy completed before publishing a release." && exit 1)
34+
35+
- name: Promote staging image to production (re-tag, no rebuild)
36+
env:
37+
REPO: ${{ github.repository }}
38+
SHA: ${{ github.sha }}
39+
TAG_NAME: ${{ github.event.release.tag_name }}
40+
run: |
41+
# Pull the exact image built and validated in the staging pipeline
42+
docker pull "ghcr.io/${REPO}/backend:${SHA}"
43+
44+
# Re-tag as version and latest
45+
docker tag "ghcr.io/${REPO}/backend:${SHA}" \
46+
"ghcr.io/${REPO}/backend:${TAG_NAME}"
47+
docker tag "ghcr.io/${REPO}/backend:${SHA}" \
48+
"ghcr.io/${REPO}/backend:latest"
49+
50+
# Push version and latest tags
51+
docker push "ghcr.io/${REPO}/backend:${TAG_NAME}"
52+
docker push "ghcr.io/${REPO}/backend:latest"
53+
54+
# --- PLUGGABLE DEPLOY STEP ---
55+
# Uncomment ONE of the following blocks for your platform.
56+
# Use ${{ github.event.release.tag_name }} as the production image tag.
57+
# The image is identical to what was validated on staging — no rebuild.
58+
59+
# --- Railway ---
60+
# - name: Deploy to Railway
61+
# run: railway up --service ${{ secrets.RAILWAY_SERVICE_ID_PRODUCTION }}
62+
# env:
63+
# RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
64+
65+
# --- Alibaba Cloud (ACR + ECS) ---
66+
# - name: Push to Alibaba Cloud ACR
67+
# run: |
68+
# docker tag ghcr.io/${{ github.repository }}/backend:${{ github.event.release.tag_name }} \
69+
# registry.{region}.aliyuncs.com/{namespace}/{service}:${{ github.event.release.tag_name }}
70+
# docker push registry.{region}.aliyuncs.com/{namespace}/{service}:${{ github.event.release.tag_name }}
71+
# - name: Deploy to ECS
72+
# run: aliyun ecs ... # Update ECS service with new image
73+
74+
# --- Google Cloud Run ---
75+
# - name: Deploy to Cloud Run
76+
# uses: google-github-actions/deploy-cloudrun@v2
77+
# with:
78+
# service: ${{ secrets.GCP_SERVICE_NAME }}
79+
# image: ghcr.io/${{ github.repository }}/backend:${{ github.event.release.tag_name }}
80+
81+
# --- Fly.io ---
82+
# - name: Deploy to Fly.io
83+
# uses: superfly/flyctl-actions/setup-flyctl@main
84+
# - run: flyctl deploy --image ghcr.io/${{ github.repository }}/backend:${{ github.event.release.tag_name }}
85+
# env:
86+
# FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
87+
88+
# --- Self-hosted (Docker Compose via SSH) ---
89+
# - name: Deploy via SSH
90+
# run: |
91+
# ssh ${{ secrets.DEPLOY_HOST }} "docker pull ghcr.io/${{ github.repository }}/backend:${{ github.event.release.tag_name }} && docker compose up -d"

.github/workflows/deploy-staging.yml

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,76 @@ name: Deploy to Staging
22

33
on:
44
push:
5-
branches:
6-
- main
5+
branches: [main]
6+
7+
concurrency:
8+
group: deploy-staging
9+
cancel-in-progress: true
710

811
jobs:
912
deploy:
10-
if: github.repository_owner != 'fastapi'
11-
runs-on:
12-
- self-hosted
13-
- staging
14-
env:
15-
ENVIRONMENT: staging
16-
DOMAIN: ${{ secrets.DOMAIN_STAGING }}
17-
STACK_NAME: ${{ secrets.STACK_NAME_STAGING }}
18-
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
19-
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
20-
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
21-
BACKEND_CORS_ORIGINS: ${{ secrets.BACKEND_CORS_ORIGINS }}
22-
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
23-
SERVICE_NAME: ${{ secrets.SERVICE_NAME }}
24-
DOCKER_IMAGE_BACKEND: ${{ secrets.DOCKER_IMAGE_BACKEND }}
25-
DOCKER_IMAGE_FRONTEND: ${{ secrets.DOCKER_IMAGE_FRONTEND }}
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
packages: write
17+
2618
steps:
2719
- name: Checkout
2820
uses: actions/checkout@v6
29-
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} build
30-
- run: docker compose -f compose.yml --project-name ${{ secrets.STACK_NAME_STAGING }} up -d
21+
22+
- name: Log in to GHCR
23+
uses: docker/login-action@v3
24+
with:
25+
registry: ghcr.io
26+
username: ${{ github.actor }}
27+
password: ${{ secrets.GITHUB_TOKEN }}
28+
29+
- name: Build and push Docker image
30+
uses: docker/build-push-action@v6
31+
with:
32+
context: .
33+
file: backend/Dockerfile
34+
push: true
35+
tags: |
36+
ghcr.io/${{ github.repository }}/backend:${{ github.sha }}
37+
ghcr.io/${{ github.repository }}/backend:staging
38+
build-args: |
39+
GIT_COMMIT=${{ github.sha }}
40+
BUILD_TIME=${{ github.event.head_commit.timestamp }}
41+
42+
# --- PLUGGABLE DEPLOY STEP ---
43+
# Uncomment ONE of the following blocks for your platform:
44+
45+
# --- Railway ---
46+
# - name: Deploy to Railway
47+
# run: railway up --service ${{ secrets.RAILWAY_SERVICE_ID_STAGING }}
48+
# env:
49+
# RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
50+
51+
# --- Alibaba Cloud (ACR + ECS) ---
52+
# - name: Push to Alibaba Cloud ACR
53+
# run: |
54+
# docker tag ghcr.io/${{ github.repository }}/backend:${{ github.sha }} \
55+
# registry.{region}.aliyuncs.com/{namespace}/{service}:${{ github.sha }}
56+
# docker push registry.{region}.aliyuncs.com/{namespace}/{service}:${{ github.sha }}
57+
# - name: Deploy to ECS
58+
# run: aliyun ecs ... # Update ECS service with new image
59+
60+
# --- Google Cloud Run ---
61+
# - name: Deploy to Cloud Run
62+
# uses: google-github-actions/deploy-cloudrun@v2
63+
# with:
64+
# service: ${{ secrets.GCP_SERVICE_NAME }}
65+
# image: ghcr.io/${{ github.repository }}/backend:${{ github.sha }}
66+
67+
# --- Fly.io ---
68+
# - name: Deploy to Fly.io
69+
# uses: superfly/flyctl-actions/setup-flyctl@main
70+
# - run: flyctl deploy --image ghcr.io/${{ github.repository }}/backend:${{ github.sha }}
71+
# env:
72+
# FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
73+
74+
# --- Self-hosted (Docker Compose via SSH) ---
75+
# - name: Deploy via SSH
76+
# run: |
77+
# ssh ${{ secrets.DEPLOY_HOST }} "docker pull ghcr.io/${{ github.repository }}/backend:${{ github.sha }} && docker compose up -d"

0 commit comments

Comments
 (0)