Skip to content

ci: add API and database Schema validation workflow #1

ci: add API and database Schema validation workflow

ci: add API and database Schema validation workflow #1

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
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
steps:
- name: Generate a token
if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }}
private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }}
- name: Checkout PR from fork
if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) }}
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Checkout PR with token
if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }}
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
token: ${{ steps.generate_token.outputs.token }}
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable 2 weeks ago
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
with:
# Cache key based on Cargo files
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# Cache the target directory
cache-targets: true
# Cache cargo registry
cache-all-crates: true
- 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@latest
echo "$HOME/go/bin" >> $GITHUB_PATH
- name: Verify oasdiff installation
run: |
export PATH="$HOME/go/bin:$PATH"
oasdiff --version || oasdiff --help | head -5
- name: Generate current API schemas
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C debuginfo=0"
run: |
cargo run -p openapi --features v1 2>&1
cargo run -p openapi --features v2 2>&1
if [[ -f "api-reference/v1/openapi_spec_v1.json" ]]; then
if jq empty api-reference/v1/openapi_spec_v1.json 2>/dev/null; then
cp api-reference/v1/openapi_spec_v1.json pr-v1-schema.json
else
echo "{}" > pr-v1-schema.json
fi
else
echo "{}" > pr-v1-schema.json
fi
if [[ -f "api-reference/v2/openapi_spec_v2.json" ]]; then
if jq empty api-reference/v2/openapi_spec_v2.json 2>/dev/null; then
cp api-reference/v2/openapi_spec_v2.json pr-v2-schema.json
else
echo "{}" > pr-v2-schema.json
fi
else
echo "{}" > pr-v2-schema.json
fi
- 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 \
--err-ignore .github/oasdiff/.oasdiff-err-ignore.yaml \
--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 \
--err-ignore .github/oasdiff/.oasdiff-err-ignore.yaml \
--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
if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
steps:
- name: Generate a token
if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.HYPERSWITCH_BOT_APP_ID }}
private-key: ${{ secrets.HYPERSWITCH_BOT_APP_PRIVATE_KEY }}
- name: Checkout PR from fork
if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) }}
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Checkout PR with token
if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) }}
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
token: ${{ steps.generate_token.outputs.token }}
- 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