Skip to content

Commit 4c5470f

Browse files
baijumclaude
andcommitted
feat: add preview environment workflow for PR deployments
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent db2fd3d commit 4c5470f

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

.github/workflows/preview.yml

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
name: Preview
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, closed]
6+
7+
jobs:
8+
deploy-preview:
9+
if: github.event.action != 'closed'
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- name: Deploy preview environment
15+
uses: appleboy/ssh-action@v1
16+
with:
17+
host: ${{ secrets.SERVER_HOST }}
18+
username: ${{ secrets.SERVER_USER }}
19+
key: ${{ secrets.SERVER_SSH_KEY }}
20+
script: |
21+
set -e
22+
APP_NAME="${{ github.event.repository.name }}"
23+
PR_NUMBER="${{ github.event.number }}"
24+
PREVIEW_DOMAIN="pr-${PR_NUMBER}.preview.${{ secrets.PREVIEW_DOMAIN }}"
25+
APP_DB=$(echo "${APP_NAME}" | tr '-' '_')_db
26+
SCHEMA_NAME="pr_${PR_NUMBER}"
27+
28+
cd /opt/apps/${APP_NAME}
29+
30+
# Fetch and checkout the PR branch
31+
git fetch origin pull/${PR_NUMBER}/head:pr-${PR_NUMBER} --force
32+
git checkout pr-${PR_NUMBER}
33+
34+
# Create preview schema in the app's database
35+
docker compose -f /opt/platform/docker-compose.yml exec -T postgres \
36+
psql -U postgres -d ${APP_DB} -c "CREATE SCHEMA IF NOT EXISTS ${SCHEMA_NAME}"
37+
38+
# Read the app's .env and generate a preview-specific version
39+
# with DATABASE_URL pointing to the PR schema
40+
if [ ! -f deploy/.env ]; then
41+
echo "ERROR: deploy/.env not found. Create it from deploy/env.template first."
42+
exit 1
43+
fi
44+
45+
# Generate preview .env with schema-aware DATABASE_URL
46+
sed "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://postgres:$(grep POSTGRES_PASSWORD /opt/platform/.env | cut -d= -f2)@postgres:5432/${APP_DB}?options=-csearch_path%3D${SCHEMA_NAME}|" \
47+
deploy/.env > deploy/.env.pr-${PR_NUMBER}
48+
49+
# Build and start preview containers
50+
docker compose -p ${APP_NAME}-pr-${PR_NUMBER} -f deploy/docker-compose.yml \
51+
--env-file deploy/.env.pr-${PR_NUMBER} up -d --build
52+
53+
# Run database migrations against the preview schema
54+
docker compose -p ${APP_NAME}-pr-${PR_NUMBER} -f deploy/docker-compose.yml \
55+
exec -T app alembic -c app/alembic.ini upgrade head
56+
57+
# Generate Caddyfile for preview domain
58+
cat > /opt/platform/caddy-apps/${APP_NAME}-pr-${PR_NUMBER}.caddy <<CADDYEOF
59+
${PREVIEW_DOMAIN} {
60+
reverse_proxy ${APP_NAME}-pr-${PR_NUMBER}-app-1:8000
61+
}
62+
CADDYEOF
63+
64+
# Reload Caddy to pick up new route
65+
docker compose -f /opt/platform/docker-compose.yml exec -T caddy \
66+
caddy reload --config /etc/caddy/Caddyfile
67+
68+
# Switch back to main so production deploys aren't affected
69+
git checkout main
70+
71+
- name: Post preview URL
72+
uses: actions/github-script@v7
73+
with:
74+
script: |
75+
const previewDomain = `pr-${{ github.event.number }}.preview.${{ secrets.PREVIEW_DOMAIN }}`;
76+
const body = `Preview deployed: https://${previewDomain}\n\nHealth check: https://${previewDomain}/health`;
77+
const { data: comments } = await github.rest.issues.listComments({
78+
owner: context.repo.owner,
79+
repo: context.repo.repo,
80+
issue_number: context.issue.number,
81+
});
82+
const existing = comments.find(c => c.body.startsWith('Preview deployed:'));
83+
if (existing) {
84+
await github.rest.issues.updateComment({
85+
owner: context.repo.owner,
86+
repo: context.repo.repo,
87+
comment_id: existing.id,
88+
body,
89+
});
90+
} else {
91+
await github.rest.issues.createComment({
92+
owner: context.repo.owner,
93+
repo: context.repo.repo,
94+
issue_number: context.issue.number,
95+
body,
96+
});
97+
}
98+
99+
cleanup-preview:
100+
if: github.event.action == 'closed'
101+
runs-on: ubuntu-latest
102+
steps:
103+
- name: Clean up preview environment
104+
uses: appleboy/ssh-action@v1
105+
with:
106+
host: ${{ secrets.SERVER_HOST }}
107+
username: ${{ secrets.SERVER_USER }}
108+
key: ${{ secrets.SERVER_SSH_KEY }}
109+
script: |
110+
set -e
111+
APP_NAME="${{ github.event.repository.name }}"
112+
PR_NUMBER="${{ github.event.number }}"
113+
APP_DB=$(echo "${APP_NAME}" | tr '-' '_')_db
114+
SCHEMA_NAME="pr_${PR_NUMBER}"
115+
116+
# Stop and remove preview containers
117+
cd /opt/apps/${APP_NAME}
118+
docker compose -p ${APP_NAME}-pr-${PR_NUMBER} -f deploy/docker-compose.yml down --rmi local || true
119+
120+
# Drop the preview schema
121+
docker compose -f /opt/platform/docker-compose.yml exec -T postgres \
122+
psql -U postgres -d ${APP_DB} -c "DROP SCHEMA IF EXISTS ${SCHEMA_NAME} CASCADE"
123+
124+
# Remove preview Caddyfile
125+
rm -f /opt/platform/caddy-apps/${APP_NAME}-pr-${PR_NUMBER}.caddy
126+
127+
# Reload Caddy
128+
docker compose -f /opt/platform/docker-compose.yml exec -T caddy \
129+
caddy reload --config /etc/caddy/Caddyfile
130+
131+
# Clean up preview .env and branch
132+
rm -f deploy/.env.pr-${PR_NUMBER}
133+
git branch -D pr-${PR_NUMBER} || true

0 commit comments

Comments
 (0)