-
Notifications
You must be signed in to change notification settings - Fork 0
369 lines (316 loc) · 13.7 KB
/
Copy pathdeploy-production.yml
File metadata and controls
369 lines (316 loc) · 13.7 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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
name: Deploy Production
on:
pull_request:
branches: [master]
types: [closed]
workflow_dispatch:
inputs:
skip_tests:
description: 'Skip tests before deployment (NOT RECOMMENDED)'
required: false
default: 'false'
type: choice
options:
- 'true'
- 'false'
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
validate-promotion:
name: Validate Promotion Event
runs-on: ubuntu-latest
outputs:
should_deploy: ${{ steps.promotion-check.outputs.should_deploy }}
trigger_reason: ${{ steps.promotion-check.outputs.trigger_reason }}
steps:
- name: Evaluate deployment trigger
id: promotion-check
shell: bash
run: |
SHOULD_DEPLOY="false"
TRIGGER_REASON="not-eligible"
HEAD_REF="${{ github.event.pull_request.head.ref }}"
if [ "${{ github.event_name }}" == "pull_request" ] && \
[ "${{ github.event.pull_request.merged }}" == "true" ] && \
[ "${{ github.event.pull_request.base.ref }}" == "master" ] && \
[ "$HEAD_REF" == "staging" ]; then
SHOULD_DEPLOY="true"
TRIGGER_REASON="merged-staging-to-master-pr"
elif [ "${{ github.event_name }}" == "pull_request" ] && \
[ "${{ github.event.pull_request.merged }}" == "true" ] && \
[ "${{ github.event.pull_request.base.ref }}" == "master" ] && \
[[ "$HEAD_REF" == hotfix/* ]]; then
SHOULD_DEPLOY="true"
TRIGGER_REASON="merged-hotfix-to-master-pr"
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
SHOULD_DEPLOY="true"
TRIGGER_REASON="manual-dispatch"
fi
echo "should_deploy=$SHOULD_DEPLOY" >> "$GITHUB_OUTPUT"
echo "trigger_reason=$TRIGGER_REASON" >> "$GITHUB_OUTPUT"
echo "Promotion check result: $SHOULD_DEPLOY"
echo "Reason: $TRIGGER_REASON"
- name: Skip notice
if: steps.promotion-check.outputs.should_deploy != 'true'
run: |
echo "No production deployment triggered."
echo "This workflow deploys on merged PRs from staging/hotfix -> master, or manual dispatch."
pre-deployment-checks:
name: Pre-Deployment Validation
runs-on: ubuntu-latest
needs: [validate-promotion]
if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && (github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true') }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
version: "latest"
enable-cache: true
- name: Create virtual environment
run: uv venv .venv
- name: Install dependencies
run: |
source .venv/bin/activate
uv pip install -r coaching/requirements.txt
uv pip install -r coaching/requirements-dev.txt
shell: bash
- name: Run Ruff Linting
run: |
source .venv/bin/activate
python -m ruff check . --exclude=".venv,venv,__pycache__,.pytest_cache"
shell: bash
- name: Run MyPy Type Checking
run: |
source .venv/bin/activate
python -m mypy coaching/src/ shared/ --config-file=pyproject.toml
shell: bash
- name: Run Unit Tests
run: |
source .venv/bin/activate
python -m pytest coaching/tests/unit/ -v --cov=coaching/src --cov-fail-under=70
shell: bash
env:
PYTHONPATH: coaching:shared:.
deploy-infrastructure:
name: Deploy Infrastructure
runs-on: ubuntu-latest
needs: [validate-promotion, pre-deployment-checks]
if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && (needs.pre-deployment-checks.result == 'success' || (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_tests == 'true')) }}
permissions:
id-token: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Pulumi Python dependencies
working-directory: infrastructure/pulumi
run: pip install -r requirements.txt
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-east-1
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Deploy Infrastructure
uses: pulumi/actions@v5
with:
command: up
stack-name: prod
work-dir: infrastructure/pulumi
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
deploy-coaching:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [validate-promotion, deploy-infrastructure]
if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && needs.deploy-infrastructure.result == 'success' }}
permissions:
id-token: write
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Pulumi Python dependencies
working-directory: coaching/pulumi
run: pip install -r requirements.txt
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-east-1
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Deploy Coaching Service
uses: pulumi/actions@v5
with:
command: up
stack-name: prod
work-dir: coaching/pulumi
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
- name: Get API Gateway URL
id: api-url
working-directory: coaching/pulumi
run: |
URL=$(pulumi stack output customDomainUrl --stack prod)
echo "url=$URL" >> $GITHUB_OUTPUT
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
- name: Lambda Runtime State Check
working-directory: coaching/pulumi
run: |
echo "Validating Lambda runtime state..."
LAMBDA_ARN=$(pulumi stack output lambdaArn --stack prod)
LAMBDA_NAME=${LAMBDA_ARN##*:function:}
for ATTEMPT in {1..18}; do
STATE=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.State" --output text)
REASON=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.StateReason" --output text)
UPDATE_STATUS=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.LastUpdateStatus" --output text)
echo "Attempt $ATTEMPT: state=$STATE, updateStatus=$UPDATE_STATUS, reason=$REASON"
if [ "$STATE" == "Active" ] && [ "$UPDATE_STATUS" == "Successful" ]; then
echo "✅ Lambda state is Active and update status is Successful"
exit 0
fi
sleep 10
done
echo "❌ Lambda did not reach Active/Successful in expected time window"
exit 1
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ github.run_number }}
release_name: Release v${{ github.run_number }}
body: |
Production deployment of PurposePath Coaching API
**Deployment Details:**
- Environment: Production
- Stack: prod
- API URL: ${{ steps.api-url.outputs.url }}
- Deployed at: ${{ github.event.pull_request.merged_at || github.event.repository.updated_at }}
- Commit: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
draft: false
prerelease: false
continue-on-error: true
- name: Deployment Summary
run: |
echo "## 🚀 Production Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ Deployment successful" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Environment:** Production" >> $GITHUB_STEP_SUMMARY
echo "**Stack:** prod" >> $GITHUB_STEP_SUMMARY
echo "**API URL:** ${{ steps.api-url.outputs.url }}" >> $GITHUB_STEP_SUMMARY
echo "**Region:** us-east-1" >> $GITHUB_STEP_SUMMARY
echo "**Release:** v${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "**Deployed at:** $(date -u)" >> $GITHUB_STEP_SUMMARY
smoke-tests:
name: Post-Deployment Smoke Tests
runs-on: ubuntu-latest
needs: [validate-promotion, deploy-coaching]
if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && needs.deploy-coaching.result == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Pulumi CLI
uses: pulumi/actions@v5
with:
pulumi-version: 'latest'
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: us-east-1
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Get API Gateway URL
id: api-url
working-directory: coaching/pulumi
run: |
URL=$(pulumi stack output customDomainUrl --stack prod)
echo "url=$URL" >> $GITHUB_OUTPUT
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
- name: Health Check
run: |
echo "Testing API health endpoint..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${{ steps.api-url.outputs.url }}/health || echo "000")
if [ "$HTTP_CODE" == "200" ] || [ "$HTTP_CODE" == "404" ]; then
echo "✅ API is responding (HTTP $HTTP_CODE)"
else
echo "⚠️ API returned HTTP $HTTP_CODE - Investigation needed"
exit 1
fi
- name: CORS Preflight Check
run: |
echo "Testing CORS preflight behavior..."
TARGET="${{ steps.api-url.outputs.url }}/api/v1/health"
ORIGINS=(
"https://dev.purposepath.app"
"https://staging.purposepath.app"
"https://preprod.purposepath.app"
"https://www.purposepath.app"
"https://purposepath.app"
)
for ORIGIN in "${ORIGINS[@]}"; do
echo "Checking origin: $ORIGIN"
CORS_HEADERS=$(curl -s -D - -o /dev/null -X OPTIONS "$TARGET" \
-H "Origin: $ORIGIN" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization,Content-Type,Baggage,Sentry-Trace")
ALLOW_ORIGIN=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-origin"{print $2}' | tail -n 1)
ALLOW_CREDENTIALS=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-credentials"{print $2}' | tail -n 1)
if [ "$ALLOW_ORIGIN" != "$ORIGIN" ]; then
echo "❌ Invalid Access-Control-Allow-Origin: '$ALLOW_ORIGIN' (expected '$ORIGIN')"
exit 1
fi
if [ "$ALLOW_CREDENTIALS" != "true" ]; then
echo "❌ Invalid Access-Control-Allow-Credentials: '$ALLOW_CREDENTIALS' (expected 'true')"
exit 1
fi
done
echo "✅ CORS preflight returned expected headers for all production frontend origins"
- name: Smoke Test Summary
run: |
echo "## Smoke Tests - Production" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ Health check passed" >> $GITHUB_STEP_SUMMARY
echo "✅ API is responsive" >> $GITHUB_STEP_SUMMARY
echo "✅ CORS preflight check passed" >> $GITHUB_STEP_SUMMARY
echo "✅ Production deployment verified" >> $GITHUB_STEP_SUMMARY
notify-deployment:
name: Notify Team
runs-on: ubuntu-latest
needs: [validate-promotion, deploy-coaching, smoke-tests]
if: ${{ always() && needs.validate-promotion.outputs.should_deploy == 'true' }}
steps:
- name: Deployment Status
run: |
if [ "${{ needs.deploy-coaching.result }}" == "success" ] && [ "${{ needs.smoke-tests.result }}" == "success" ]; then
echo "✅ Production deployment completed successfully"
else
echo "❌ Production deployment encountered issues"
exit 1
fi