-
Notifications
You must be signed in to change notification settings - Fork 2
231 lines (191 loc) · 8.64 KB
/
push-cd-prod.yml
File metadata and controls
231 lines (191 loc) · 8.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
name: CI/CD for Production Server
on:
push:
branches: [ main ] # Only main branch
permissions:
contents: read
jobs:
ci-cd-prod:
name: Test, Build, and Deploy to Production Server
runs-on: ubuntu-latest
steps:
# ========================================
# CI Stage: Test & Lint
# ========================================
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test -- --ci --coverage --maxWorkers=2
continue-on-error: true
# ========================================
# CD Stage: Build & Push Docker Image
# ========================================
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64 # Multi-architecture build
tags: |
${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:latest
${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod
${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1
# ========================================
# Deploy Stage: SSH & Deploy
# ========================================
- name: Setup SSH key and config
run: |
set -e
mkdir -p ~/.ssh
# Write SSH private key (handle \n in secret)
echo "${{ secrets.PROD_SSH_PRIVATE_KEY }}" | tr -d '\r' > ~/.ssh/my-key.pem
chmod 600 ~/.ssh/my-key.pem
# Verify key file was created
if [ ! -s ~/.ssh/my-key.pem ]; then
echo "Error: SSH key file is empty"
exit 1
fi
echo "SSH key written successfully ($(wc -l < ~/.ssh/my-key.pem) lines)"
# Add server to known_hosts
echo "Adding ${{ secrets.PROD_SERVER_HOST }}:${{ secrets.PROD_SSH_PORT }} to known_hosts..."
ssh-keyscan -p ${{ secrets.PROD_SSH_PORT }} -H ${{ secrets.PROD_SERVER_HOST }} >> ~/.ssh/known_hosts 2>&1 || {
echo "Warning: ssh-keyscan failed, but continuing..."
echo "You may need to manually verify the host key on first connection"
}
# Configure SSH keep-alive and disable strict host checking for automation
cat >> ~/.ssh/config << 'EOF'
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
StrictHostKeyChecking no
UserKnownHostsFile ~/.ssh/known_hosts
EOF
echo "✓ SSH configuration complete"
- name: Create app directory on server
run: |
ssh -i ~/.ssh/my-key.pem -p ${{ secrets.PROD_SSH_PORT }} ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} "mkdir -p ~/devnogi-react"
- name: Copy deployment files to server
run: |
scp -i ~/.ssh/my-key.pem -P ${{ secrets.PROD_SSH_PORT }} docker-compose-prod.yml ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }}:~/devnogi-react/
- name: Deploy to Production Server
run: |
ssh -i ~/.ssh/my-key.pem -p ${{ secrets.PROD_SSH_PORT }} ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} << 'EOF'
cd ~/devnogi-react
# Write .env.prod content from GitHub Secret
echo "${{ secrets.ENV_FILE_PROD }}" > .env.prod
# Pull latest production image
docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod
# Stop and remove existing containers
docker compose -f docker-compose-prod.yml --env-file .env.prod down
# Start new containers
docker compose -f docker-compose-prod.yml --env-file .env.prod up -d
echo "✅ Production deployment complete"
EOF
# ========================================
# Health Check Stage (Stricter for Production)
# ========================================
- name: Comprehensive Health Check
run: |
ssh -i ~/.ssh/my-key.pem -p ${{ secrets.PROD_SSH_PORT }} ${{ secrets.PROD_SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} << 'EOF'
echo "=== Starting Production Health Check ==="
# 1. Check if Next.js container is running
CONTAINER_ID=$(docker ps -q --filter "name=nextjs-app-prod")
if [ -z "$CONTAINER_ID" ]; then
echo "❌ Next.js container not running"
docker ps -a
docker logs nextjs-app-prod --tail 50
exit 1
fi
echo "✅ Next.js container is running (ID: $CONTAINER_ID)"
# 2. Wait for Docker health check (Next.js)
echo "Waiting for Next.js container to become healthy..."
for i in {1..36}; do
HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' nextjs-app-prod 2>/dev/null || echo "no-healthcheck")
if [ "$HEALTH_STATUS" == "healthy" ]; then
echo "✅ Next.js container is healthy"
break
elif [ "$HEALTH_STATUS" == "no-healthcheck" ]; then
echo "⚠️ No healthcheck configured, checking endpoint directly"
break
fi
echo "Current health status: $HEALTH_STATUS ($i/36)"
sleep 10
if [ $i -eq 36 ]; then
echo "❌ Container failed to become healthy after 6 minutes"
docker logs nextjs-app-prod --tail 100
exit 1
fi
done
# 3. Check Next.js health endpoint directly
echo "Checking health endpoint (port 3010)..."
for i in {1..30}; do
HEALTH_RESPONSE=$(curl -s http://localhost:3010/api/health || echo "")
if echo "$HEALTH_RESPONSE" | grep -q '"status":"ok"'; then
echo "✅ Application health check passed"
echo "Health response: $HEALTH_RESPONSE"
break
fi
echo "Waiting for application to start... ($i/30)"
sleep 10
if [ $i -eq 30 ]; then
echo "❌ Application health check failed after 5 minutes"
echo "Last response: $HEALTH_RESPONSE"
docker logs nextjs-app-prod --tail 100
exit 1
fi
done
# 4. Smoke test: Check if Next.js responds
echo "Running smoke test..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3010/api/health)
if [ "$HTTP_CODE" == "200" ]; then
echo "✅ Smoke test passed (HTTP $HTTP_CODE)"
else
echo "❌ Smoke test failed (HTTP $HTTP_CODE)"
exit 1
fi
echo "=== Health Check Complete ==="
docker ps --filter "name=nextjs-app-prod"
EOF
- name: Display deployment info
if: success()
run: |
echo "✅ Production deployment successful!"
echo "🐳 Image: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod"
echo "📦 Commit: ${{ github.sha }}"
echo "🔗 Next.js App: http://${{ secrets.PROD_SERVER_HOST }}:3010"
echo "⚠️ Configure your host Nginx to proxy to localhost:3010"
echo "⚠️ Please verify the production deployment manually"
# ========================================
# Rollback on Failure (Optional)
# ========================================
- name: Rollback on failure
if: failure()
run: |
echo "❌ Deployment failed! Consider manual rollback if needed."
echo "To rollback, SSH to server and run:"
echo " cd ~/devnogi-react"
echo " docker pull ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_REPO }}:prod-<previous-sha>"
echo " docker compose -f docker-compose-prod.yml --env-file .env.prod down"
echo " docker compose -f docker-compose-prod.yml --env-file .env.prod up -d"