-
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathpre-post-migration-validate.yml
More file actions
376 lines (336 loc) · 13.7 KB
/
Copy pathpre-post-migration-validate.yml
File metadata and controls
376 lines (336 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
370
371
372
373
374
375
376
# Pre/Post Migration Validation Workflow
# This workflow focuses only on migration safety:
# pre-snapshot baseline → apply migration → post-comparison → deployment gate.
# It is the ONLY workflow that gates deployments.
# Does NOT overlap with db-health-checks.yml (scheduled monitoring) or
# pgfirstaid-pr-audit.yml (PR comments).
name: Pre/Post Migration Validation
on:
pull_request:
paths:
- 'migrations/**'
- 'db/**'
- 'pgFirstAid.sql'
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
concurrency:
group: migration-${{ github.head_ref || github.sha }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# Job 1: Capture baseline health before migrations
pre-migration-baseline:
name: Pre-Migration Health Baseline
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Install PostgreSQL client
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq postgresql-client >/dev/null
psql --version
- name: Checkout code
uses: actions/checkout@v4
- name: Install pgFirstAid
run: |
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} <<'EOF'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
DROP FUNCTION IF EXISTS pg_firstAid();
\i pgFirstAid.sql
EOF
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
- name: Generate Baseline Report
run: |
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} <<'EOF'
CREATE TEMP TABLE _pre_health_snapshot AS SELECT * FROM pg_firstAid();
\echo '=== Pre-Migration Health Baseline ==='
\echo ''
\echo 'Severity Summary:'
SELECT severity, COUNT(*) as issue_count
FROM _pre_health_snapshot
GROUP BY severity
ORDER BY
CASE severity
WHEN 'CRITICAL' THEN 1
WHEN 'HIGH' THEN 2
WHEN 'MEDIUM' THEN 3
WHEN 'LOW' THEN 4
ELSE 5
END;
\echo ''
\echo 'Critical Issues (blocking):'
SELECT severity, check_name, object_name, issue_description
FROM _pre_health_snapshot
WHERE severity = 'CRITICAL'
ORDER BY check_name;
\echo ''
\echo 'High Priority Issues:'
SELECT severity, check_name, object_name, issue_description
FROM _pre_health_snapshot
WHERE severity = 'HIGH'
ORDER BY check_name;
EOF
continue-on-error: true
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
- name: Save Baseline Data
run: |
# Create persistent snapshot table for cross-job comparison
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} <<'EOF'
DROP TABLE IF EXISTS _pre_health_snapshot;
CREATE UNLOGGED TABLE _pre_health_snapshot AS SELECT * FROM pg_firstAid();
EOF
# Save severity counts as CSV
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} --csv \
-c "SELECT severity, COUNT(*)::int AS count FROM _pre_health_snapshot GROUP BY severity ORDER BY 1;" \
> ${{ github.workspace }}/baseline_severity.csv
# Save critical count
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} -t \
-c "SELECT count(*) FROM _pre_health_snapshot WHERE severity = 'CRITICAL';" \
| tr -d '[:space:]' > ${{ github.workspace }}/baseline_critical_count.txt
continue-on-error: true
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
- name: Upload Baseline Report
uses: actions/upload-artifact@v4
if: always()
with:
name: pre-migration-baseline
path: |
${{ github.workspace }}/baseline_severity.csv
${{ github.workspace }}/baseline_critical_count.txt
retention-days: 7
# Job 2: Apply migrations
apply-migrations:
name: Apply Database Migrations
runs-on: ubuntu-latest
needs: pre-migration-baseline
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Migrations
run: |
# Flyway example
# flyway migrate
# Or Liquibase
# mvn liquibase:update
# Or direct SQL
# psql -h $PGHOST -U $PGUSER -d $PGDATABASE -f migrations/apply.sql
echo "Migrations applied successfully"
env:
PGHOST: ${{ secrets.PGHOST }}
PGUSER: ${{ secrets.PGUSER }}
PGDATABASE: ${{ secrets.PGDATABASE }}
PGPASSWORD: ${{ secrets.PGPASSWORD }}
PGPORT: '5432'
FLYWAY_URL: jdbc:postgresql://${{ secrets.PGHOST }}:5432/${{ secrets.PGDATABASE }}
FLYWAY_USER: ${{ secrets.PGUSER }}
FLYWAY_PASSWORD: ${{ secrets.PGPASSWORD }}
# Job 3: Validate post-migration health
post-migration-validation:
name: Post-Migration Health Validation
runs-on: ubuntu-latest
needs: apply-migrations
timeout-minutes: 15
steps:
- name: Install PostgreSQL client
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq postgresql-client >/dev/null
psql --version
- name: Checkout code
uses: actions/checkout@v4
- name: Re-install pgFirstAid
run: |
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} <<'EOF'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
DROP FUNCTION IF EXISTS pg_firstAid();
\i pgFirstAid.sql
EOF
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
- name: Download baseline data
uses: actions/download-artifact@v4
with:
name: pre-migration-baseline
path: ./
- name: Run Post-Migration Health Check
run: |
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} <<'EOF'
CREATE TEMP TABLE _health_snapshot AS SELECT * FROM pg_firstAid();
\echo '=== Post-Migration Health Report ==='
\echo ''
\echo 'Severity Summary:'
SELECT severity, COUNT(*) as issue_count
FROM _health_snapshot
GROUP BY severity
ORDER BY
CASE severity
WHEN 'CRITICAL' THEN 1
WHEN 'HIGH' THEN 2
WHEN 'MEDIUM' THEN 3
WHEN 'LOW' THEN 4
ELSE 5
END;
\echo ''
\echo 'All Issues (ordered by priority):'
SELECT severity, check_name, object_name, issue_description, recommended_action
FROM _health_snapshot
ORDER BY
CASE severity
WHEN 'CRITICAL' THEN 1
WHEN 'HIGH' THEN 2
WHEN 'MEDIUM' THEN 3
WHEN 'LOW' THEN 4
ELSE 5
END,
check_name;
\echo ''
\echo 'New Issues (compared to baseline):'
SELECT severity, check_name, object_name, issue_description, recommended_action
FROM _health_snapshot
WHERE (check_name, object_name) NOT IN (
SELECT check_name, object_name FROM _pre_health_snapshot
)
ORDER BY severity, check_name;
EOF
continue-on-error: true
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
- name: Compare Critical Issue Count
id: compare-critical
run: |
BASELINE_CRITICAL=$(cat baseline_critical_count.txt | tr -d '[:space:]' || echo "0")
CURRENT_CRITICAL=$(psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} \
-t -c "SELECT count(*) FROM pg_firstAid() WHERE severity = 'CRITICAL';" | tr -d '[:space:]')
echo "baseline_critical=$BASELINE_CRITICAL" >> $GITHUB_OUTPUT
echo "current_critical=$CURRENT_CRITICAL" >> $GITHUB_OUTPUT
if [ "$CURRENT_CRITICAL" -gt "$BASELINE_CRITICAL" ]; then
NEW_CRITICAL=$((CURRENT_CRITICAL - BASELINE_CRITICAL))
echo "::error::Migration introduced $NEW_CRITICAL new critical issues!"
echo "Baseline: $BASELINE_CRITICAL | Current: $CURRENT_CRITICAL"
exit 1
else
echo "No new critical issues introduced"
fi
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
- name: Check for New High Priority Issues
id: check-high
run: |
CURRENT_HIGH=$(psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} \
-t -c "SELECT count(*) FROM pg_firstAid() WHERE severity = 'HIGH';" | tr -d '[:space:]')
echo "high_issues=$CURRENT_HIGH" >> $GITHUB_OUTPUT
if [ "$CURRENT_HIGH" -gt 0 ]; then
echo "Found $CURRENT_HIGH high priority issues:"
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} \
-c "SELECT check_name, object_name, issue_description FROM pg_firstAid() WHERE severity = 'HIGH';"
echo "high_issues_warn=true" >> $GITHUB_OUTPUT
else
echo "No high priority issues"
echo "high_issues_warn=false" >> $GITHUB_OUTPUT
fi
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
- name: Save Post-Migration CSV
run: |
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} --csv \
-c "SELECT severity, COUNT(*)::int AS count FROM pg_firstAid() GROUP BY severity ORDER BY 1;" \
> ${{ github.workspace }}/post_migration_severity.csv
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} --csv \
-c "SELECT severity, check_name, object_name, issue_description, recommended_action FROM pg_firstAid() ORDER BY severity, check_name;" \
> ${{ github.workspace }}/post_migration_health.csv
continue-on-error: true
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
- name: Upload Post-Migration Report
uses: actions/upload-artifact@v4
if: always()
with:
name: post-migration-report
path: |
${{ github.workspace }}/post_migration_severity.csv
${{ github.workspace }}/post_migration_health.csv
retention-days: 7
if-no-files-found: warn
- name: Cleanup baseline snapshot
if: always()
run: |
psql -h ${{ secrets.PGHOST }} -U ${{ secrets.PGUSER }} -d ${{ secrets.PGDATABASE }} \
-c "DROP TABLE IF EXISTS _pre_health_snapshot;"
env:
PGPASSWORD: ${{ secrets.PGPASSWORD }}
# Job 4: Summary and notification
generate-summary:
name: Generate Summary Report
runs-on: ubuntu-latest
needs: [pre-migration-baseline, post-migration-validation]
steps:
- name: Download baseline artifacts
uses: actions/download-artifact@v4
with:
name: pre-migration-baseline
path: baseline/
- name: Download post-migration artifacts
uses: actions/download-artifact@v4
with:
name: post-migration-report
path: post-migration/
- name: Generate Summary
run: |
echo "=== MIGRATION VALIDATION SUMMARY ===" > summary.md
echo "" >> summary.md
echo "## Baseline (Pre-Migration)" >> summary.md
echo "" >> summary.md
echo "| Severity | Count |" >> summary.md
echo "|----------|-------|" >> summary.md
if [ -f "baseline/baseline_severity.csv" ]; then
awk -F',' 'NR>1 {printf "| %s | %s |\n", $1, $2}' baseline/baseline_severity.csv >> summary.md
else
echo "| Not Available | |" >> summary.md
fi
echo "" >> summary.md
echo "## Post-Migration" >> summary.md
echo "" >> summary.md
echo "| Severity | Count |" >> summary.md
echo "|----------|-------|" >> summary.md
if [ -f "post-migration/post_migration_severity.csv" ]; then
awk -F',' 'NR>1 {printf "| %s | %s |\n", $1, $2}' post-migration/post_migration_severity.csv >> summary.md
else
echo "| Not Available | |" >> summary.md
fi
echo "" >> summary.md
echo "## Critical Issue Change" >> summary.md
echo "" >> summary.md
BASELINE_CRIT=$(cat baseline/baseline_critical_count.txt 2>/dev/null || echo "N/A")
echo "Baseline: $BASELINE_CRIT critical issues" >> summary.md
CURRENT_CRIT=$(awk -F',' '$1 == "CRITICAL" {print $2}' post-migration/post_migration_severity.csv 2>/dev/null || echo "N/A")
echo "Current: $CURRENT_CRIT critical issues" >> summary.md
cat summary.md
- name: Upload Summary
uses: actions/upload-artifact@v4
with:
name: migration-validation-summary
path: summary.md
retention-days: 30
# Job 5: Block deployment if critical issues introduced
gate-check:
name: Gate Check (Block Deployment)
runs-on: ubuntu-latest
needs: [pre-migration-baseline, post-migration-validation, generate-summary]
if: success() || failure()
steps:
- name: Check if migration should be blocked
run: |
if [ "${{ needs.post-migration-validation.result }}" != "success" ]; then
echo "::error::Migration validation failed. Please review pgFirstAid health checks."
exit 1
fi
echo "Migration passed validation - safe to deploy"