Skip to content

ci: add API and database Schema validation workflow #5

ci: add API and database Schema validation workflow

ci: add API and database Schema validation workflow #5

name: API Schema Compatibility Check
# This workflow:
# - Runs on ALL PRs that modify API-related files
# - Always fails if breaking changes are detected
on:
pull_request:
paths:
- 'crates/api_models/**'
- 'crates/openapi/**'
- 'crates/router/**'
- 'api-reference/**'
- 'migrations/**'
- 'v2_migrations/**'
- 'v2_compatible_migrations/**'
- '.github/oasdiff/**'
types: [opened, synchronize, reopened]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
api-compatibility-check:
name: API & Migration Compatibility Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y jq
- name: Setup Go for oasdiff
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Install oasdiff
run: |
go install github.com/oasdiff/oasdiff@v1.11.7
echo "$HOME/go/bin" >> $GITHUB_PATH
- name: Use committed API schemas
run: |
echo "Using OpenAPI schemas from PR (generated by validate-openapi-spec workflow)..."
# Verify schemas exist (they should be committed by validate-openapi-spec workflow)
if [[ ! -f "api-reference/v1/openapi_spec_v1.json" ]]; then
echo "::error::V1 schema not found. The validate-openapi-spec workflow should have generated it."
exit 1
fi
if [[ ! -f "api-reference/v2/openapi_spec_v2.json" ]]; then
echo "::error::V2 schema not found. The validate-openapi-spec workflow should have generated it."
exit 1
fi
# Validate JSON format
if ! jq empty api-reference/v1/openapi_spec_v1.json 2>/dev/null; then
echo "::error::V1 schema contains invalid JSON"
exit 1
fi
if ! jq empty api-reference/v2/openapi_spec_v2.json 2>/dev/null; then
echo "::error::V2 schema contains invalid JSON"
exit 1
fi
# Copy to working files
cp api-reference/v1/openapi_spec_v1.json pr-v1-schema.json
cp api-reference/v2/openapi_spec_v2.json pr-v2-schema.json
echo "V1 schema: $(wc -c < pr-v1-schema.json) bytes"
echo "V2 schema: $(wc -c < pr-v2-schema.json) bytes"
- name: Extract base branch schemas
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if git show "$BASE_SHA:api-reference/v1/openapi_spec_v1.json" > base-v1-schema.json 2>/dev/null; then
echo "V1 schema extracted from $BASE_REF"
else
echo "{}" > base-v1-schema.json
fi
if git show "$BASE_SHA:api-reference/v2/openapi_spec_v2.json" > base-v2-schema.json 2>/dev/null; then
echo "V2 schema extracted from $BASE_REF"
else
echo "{}" > base-v2-schema.json
fi
- name: Run breaking change detection
id: breaking_changes
run: |
BREAKING_CHANGES=0
if oasdiff breaking \
--fail-on ERR \
--warn-ignore .github/oasdiff/.oasdiff-warn-ignore.yaml \
base-v1-schema.json pr-v1-schema.json > v1-breaking-report.txt 2>&1; then
echo "V1 API is backward compatible" > v1-breaking-status.txt
else
echo "Breaking changes detected in V1 API" > v1-breaking-status.txt
BREAKING_CHANGES=$((BREAKING_CHANGES + 1))
cat v1-breaking-report.txt
fi
if oasdiff breaking \
--fail-on ERR \
--warn-ignore .github/oasdiff/.oasdiff-warn-ignore.yaml \
base-v2-schema.json pr-v2-schema.json > v2-breaking-report.txt 2>&1; then
echo "V2 API is backward compatible" > v2-breaking-status.txt
else
echo "Breaking changes detected in V2 API" > v2-breaking-status.txt
BREAKING_CHANGES=$((BREAKING_CHANGES + 1))
cat v2-breaking-report.txt
fi
oasdiff diff base-v1-schema.json pr-v1-schema.json > v1-detailed-diff.txt 2>/dev/null || true
oasdiff diff base-v2-schema.json pr-v2-schema.json > v2-detailed-diff.txt 2>/dev/null || true
echo "breaking_changes=$BREAKING_CHANGES" >> $GITHUB_OUTPUT
echo "total_issues=$BREAKING_CHANGES" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Display API Compatibility Report
if: always()
run: |
echo "=========================================="
echo "API COMPATIBILITY REPORT"
echo "=========================================="
echo ""
echo "V1 API Breaking Changes:"
cat v1-breaking-report.txt 2>/dev/null || echo "No breaking changes detected"
echo ""
echo "V2 API Breaking Changes:"
cat v2-breaking-report.txt 2>/dev/null || echo "No breaking changes detected"
echo ""
echo "V1 API Detailed Diff:"
cat v1-detailed-diff.txt 2>/dev/null || echo "No changes detected"
echo ""
echo "V2 API Detailed Diff:"
cat v2-detailed-diff.txt 2>/dev/null || echo "No changes detected"
echo "=========================================="
- name: Fail if breaking changes detected
if: steps.breaking_changes.outputs.breaking_changes > 0
run: |
echo "::error::Breaking changes detected in API schema."
echo "::error::Found ${{ steps.breaking_changes.outputs.breaking_changes }} breaking change(s)."
echo "::error::Please review the compatibility report and consider:"
echo "::error::- Creating a new API version (v3)"
echo "::error::- Using backward-compatible changes instead"
echo "::error::- Coordinating with API consumers before deployment"
exit 1
- name: Upload validation artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: api-validation-artifacts-${{ github.run_number }}
path: |
pr-v1-schema.json
pr-v2-schema.json
base-v1-schema.json
base-v2-schema.json
v1-breaking-report.txt
v2-breaking-report.txt
v1-detailed-diff.txt
v2-detailed-diff.txt
v1-breaking-status.txt
v2-breaking-status.txt
retention-days: 30
migration-compatibility-check:
name: Migration Breaking Change Detection
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Fetch base branch
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
git fetch origin "$BASE_REF:$BASE_REF"
- name: Validate V1 migrations
id: migration_check
run: |
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
NEW_MIGRATIONS=$(git diff --name-only --diff-filter=AM "$BASE_REF"...HEAD -- "migrations/**/*.sql" 2>/dev/null || echo "")
if [[ -z "$NEW_MIGRATIONS" ]]; then
echo "breaking_changes=0" >> $GITHUB_OUTPUT
echo "warnings=0" >> $GITHUB_OUTPUT
exit 0
fi
BREAKING=0
WARNINGS=0
while IFS= read -r file; do
[[ -z "$file" ]] && continue
CONTENT=$(cat "$file")
if echo "$CONTENT" | grep -iE "(DROP TABLE|DROP COLUMN|DELETE FROM|TRUNCATE)" >/dev/null; then
echo "BREAKING: $file"
echo "$CONTENT" | grep -inE "(DROP TABLE|DROP COLUMN|DELETE FROM|TRUNCATE)" | sed 's/^/ /'
BREAKING=$((BREAKING + 1))
fi
if echo "$CONTENT" | grep -iE "(ALTER COLUMN .* TYPE|ALTER COLUMN .* SET NOT NULL|DROP INDEX|DROP CONSTRAINT)" >/dev/null; then
echo "WARNING: $file"
echo "$CONTENT" | grep -inE "(ALTER COLUMN .* TYPE|ALTER COLUMN .* SET NOT NULL|DROP INDEX|DROP CONSTRAINT)" | sed 's/^/ /'
WARNINGS=$((WARNINGS + 1))
fi
done <<< "$NEW_MIGRATIONS"
echo "breaking_changes=$BREAKING" >> $GITHUB_OUTPUT
echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Validate V2 migrations
id: v2_migration_check
run: |
if [[ ! -d "v2_migrations" ]]; then
echo "breaking_changes=0" >> $GITHUB_OUTPUT
echo "warnings=0" >> $GITHUB_OUTPUT
exit 0
fi
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
NEW_MIGRATIONS=$(git diff --name-only --diff-filter=AM "$BASE_REF"...HEAD -- "v2_migrations/**/*.sql" 2>/dev/null || echo "")
if [[ -z "$NEW_MIGRATIONS" ]]; then
echo "breaking_changes=0" >> $GITHUB_OUTPUT
echo "warnings=0" >> $GITHUB_OUTPUT
exit 0
fi
BREAKING=0
WARNINGS=0
while IFS= read -r file; do
[[ -z "$file" ]] && continue
CONTENT=$(cat "$file")
if echo "$CONTENT" | grep -iE "(DROP TABLE|DROP COLUMN|DELETE FROM|TRUNCATE)" >/dev/null; then
echo "BREAKING: $file"
echo "$CONTENT" | grep -inE "(DROP TABLE|DROP COLUMN|DELETE FROM|TRUNCATE)" | sed 's/^/ /'
BREAKING=$((BREAKING + 1))
fi
if echo "$CONTENT" | grep -iE "(ALTER COLUMN .* TYPE|ALTER COLUMN .* SET NOT NULL|DROP INDEX|DROP CONSTRAINT)" >/dev/null; then
echo "WARNING: $file"
echo "$CONTENT" | grep -inE "(ALTER COLUMN .* TYPE|ALTER COLUMN .* SET NOT NULL|DROP INDEX|DROP CONSTRAINT)" | sed 's/^/ /'
WARNINGS=$((WARNINGS + 1))
fi
done <<< "$NEW_MIGRATIONS"
echo "breaking_changes=$BREAKING" >> $GITHUB_OUTPUT
echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Validate V2 compatible migrations
id: v2_compat_migration_check
run: |
if [[ ! -d "v2_compatible_migrations" ]]; then
echo "breaking_changes=0" >> $GITHUB_OUTPUT
echo "warnings=0" >> $GITHUB_OUTPUT
exit 0
fi
BASE_REF="origin/${{ github.event.pull_request.base.ref }}"
NEW_MIGRATIONS=$(git diff --name-only --diff-filter=AM "$BASE_REF"...HEAD -- "v2_compatible_migrations/**/*.sql" 2>/dev/null || echo "")
if [[ -z "$NEW_MIGRATIONS" ]]; then
echo "breaking_changes=0" >> $GITHUB_OUTPUT
echo "warnings=0" >> $GITHUB_OUTPUT
exit 0
fi
BREAKING=0
WARNINGS=0
while IFS= read -r file; do
[[ -z "$file" ]] && continue
CONTENT=$(cat "$file")
if echo "$CONTENT" | grep -iE "(DROP TABLE|DROP COLUMN|DELETE FROM|TRUNCATE)" >/dev/null; then
echo "BREAKING: $file"
echo "$CONTENT" | grep -inE "(DROP TABLE|DROP COLUMN|DELETE FROM|TRUNCATE)" | sed 's/^/ /'
BREAKING=$((BREAKING + 1))
fi
if echo "$CONTENT" | grep -iE "(ALTER COLUMN .* TYPE|ALTER COLUMN .* SET NOT NULL|DROP INDEX|DROP CONSTRAINT)" >/dev/null; then
echo "WARNING: $file"
echo "$CONTENT" | grep -inE "(ALTER COLUMN .* TYPE|ALTER COLUMN .* SET NOT NULL|DROP INDEX|DROP CONSTRAINT)" | sed 's/^/ /'
WARNINGS=$((WARNINGS + 1))
fi
done <<< "$NEW_MIGRATIONS"
echo "breaking_changes=$BREAKING" >> $GITHUB_OUTPUT
echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Calculate migration totals
id: migration_report
run: |
TOTAL_BREAKING=$((
${MIGRATION_BREAKING:-0} +
${V2_MIGRATION_BREAKING:-0} +
${V2_COMPAT_MIGRATION_BREAKING:-0}
))
TOTAL_WARNINGS=$((
${MIGRATION_WARNINGS:-0} +
${V2_MIGRATION_WARNINGS:-0} +
${V2_COMPAT_MIGRATION_WARNINGS:-0}
))
echo "total_breaking=$TOTAL_BREAKING" >> $GITHUB_OUTPUT
echo "total_warnings=$TOTAL_WARNINGS" >> $GITHUB_OUTPUT
if [[ $TOTAL_BREAKING -gt 0 ]]; then
echo "validation_status=BREAKING CHANGES DETECTED" >> $GITHUB_OUTPUT
elif [[ $TOTAL_WARNINGS -gt 0 ]]; then
echo "validation_status=WARNINGS FOUND" >> $GITHUB_OUTPUT
else
echo "validation_status=ALL CHECKS PASSED" >> $GITHUB_OUTPUT
fi
env:
MIGRATION_BREAKING: ${{ steps.migration_check.outputs.breaking_changes }}
MIGRATION_WARNINGS: ${{ steps.migration_check.outputs.warnings }}
V2_MIGRATION_BREAKING: ${{ steps.v2_migration_check.outputs.breaking_changes }}
V2_MIGRATION_WARNINGS: ${{ steps.v2_migration_check.outputs.warnings }}
V2_COMPAT_MIGRATION_BREAKING: ${{ steps.v2_compat_migration_check.outputs.breaking_changes }}
V2_COMPAT_MIGRATION_WARNINGS: ${{ steps.v2_compat_migration_check.outputs.warnings }}
- name: Display Migration Summary
if: always()
run: |
echo "=========================================="
echo "MIGRATION VALIDATION SUMMARY"
echo "=========================================="
echo "V1: Breaking=${{ steps.migration_check.outputs.breaking_changes }} Warnings=${{ steps.migration_check.outputs.warnings }}"
echo "V2: Breaking=${{ steps.v2_migration_check.outputs.breaking_changes }} Warnings=${{ steps.v2_migration_check.outputs.warnings }}"
echo "V2 Compat: Breaking=${{ steps.v2_compat_migration_check.outputs.breaking_changes }} Warnings=${{ steps.v2_compat_migration_check.outputs.warnings }}"
echo "----------------------------------------"
echo "Total: Breaking=${{ steps.migration_report.outputs.total_breaking }} Warnings=${{ steps.migration_report.outputs.total_warnings }}"
echo "Status: ${{ steps.migration_report.outputs.validation_status }}"
echo "=========================================="
- name: Fail if migration breaking changes detected
if: steps.migration_report.outputs.total_breaking != '0' && steps.migration_report.outputs.total_breaking != ''
run: |
echo "::error::Breaking changes detected in database migrations."
echo "::error::Found ${{ steps.migration_report.outputs.total_breaking }} breaking change(s)."
exit 1