Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions .github/workflows/openapi-breaking-change.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
name: OpenAPI Breaking Change Detection

on:
pull_request:
paths:
- "BackEnd/src/**"
- "BackEnd/package.json"
- ".github/workflows/openapi-breaking-change.yml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
detect-breaking-changes:
name: Diff OpenAPI Spec
runs-on: ubuntu-latest

env:
# Minimal env so NestJS can bootstrap without a real database/redis
NODE_ENV: production
JWT_SECRET: ci-placeholder-secret-32-chars-ok
JWT_REFRESH_SECRET: ci-placeholder-refresh-secret-ok
JWT_ACCESS_TOKEN_EXPIRATION: 15m
JWT_REFRESH_TOKEN_EXPIRATION: 7d
DATABASE_URL: postgres://ci:ci@localhost:5432/ci
REDIS_URL: redis://localhost:6379
STELLAR_NETWORK: testnet
# Silence optional integrations
SENTRY_DSN: ""
OTEL_SDK_DISABLED: "true"

steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
fetch-depth: 0 # need full history to check out base branch

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-backend-npm-${{ hashFiles('BackEnd/package-lock.json') }}
restore-keys: |
${{ runner.os }}-backend-npm-

# ── Generate spec for the PR branch ────────────────────────────────────
- name: Install dependencies (PR branch)
run: npm ci
working-directory: BackEnd

- name: Generate OpenAPI spec (PR branch)
run: npx ts-node -r tsconfig-paths/register scripts/generate-openapi.ts openapi-pr.json
working-directory: BackEnd

# ── Switch to base branch and generate its spec ────────────────────────
- name: Checkout base branch (${{ github.base_ref }})
run: git checkout origin/${{ github.base_ref }}

- name: Install dependencies (base branch)
run: npm ci
working-directory: BackEnd

- name: Generate OpenAPI spec (base branch)
run: npx ts-node -r tsconfig-paths/register scripts/generate-openapi.ts openapi-base.json
working-directory: BackEnd

# ── Restore PR files so artifacts are accessible ───────────────────────
- name: Restore PR branch files
run: git checkout ${{ github.sha }} -- BackEnd/openapi-pr.json

# ── Install oasdiff and run the diff ───────────────────────────────────
- name: Install oasdiff
run: |
curl -sSL https://raw.githubusercontent.com/tufin/oasdiff/main/install.sh | sh
echo "$HOME/.oasdiff/bin" >> $GITHUB_PATH

- name: Detect breaking changes
id: diff
run: |
set +e
oasdiff breaking \
BackEnd/openapi-base.json \
BackEnd/openapi-pr.json \
--format text > breaking-changes.txt 2>&1
EXIT_CODE=$?
set -e

echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT

if [ -s breaking-changes.txt ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi

- name: Post PR comment with breaking changes
if: steps.diff.outputs.has_changes == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const changes = fs.readFileSync('breaking-changes.txt', 'utf8');
const body = [
'## ⚠️ OpenAPI Breaking Changes Detected',
'',
'The following breaking changes were found between `${{ github.base_ref }}` and this PR:',
'',
'```',
changes.trim(),
'```',
'',
'Please review these changes. If they are intentional, bump the API version and update the changelog.',
].join('\n');

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const existing = comments.find(c =>
c.user.login === 'github-actions[bot]' &&
c.body.startsWith('## ⚠️ OpenAPI Breaking Changes Detected')
);

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}

- name: Upload spec artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: openapi-specs-${{ github.run_id }}
path: |
BackEnd/openapi-base.json
BackEnd/openapi-pr.json
breaking-changes.txt
retention-days: 14

- name: Fail on breaking changes
if: steps.diff.outputs.exit_code != '0'
run: |
echo "::error::Breaking API changes detected. See the PR comment and breaking-changes.txt artifact for details."
exit 1

- name: Workflow summary
if: always()
run: |
echo "## OpenAPI Breaking Change Detection" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.diff.outputs.exit_code }}" = "0" ]; then
echo "✅ No breaking changes detected." >> $GITHUB_STEP_SUMMARY
else
echo "❌ Breaking changes were found — see artifact \`breaking-changes.txt\`." >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
cat breaking-changes.txt >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
5 changes: 5 additions & 0 deletions BackEnd/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

/generated/prisma
/exports

# Generated OpenAPI spec (produced by scripts/generate-openapi.ts)
openapi.json
openapi-base.json
openapi-pr.json
1 change: 1 addition & 0 deletions BackEnd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"schema:sync": "bun run typeorm -- schema:sync",
"schema:drop": "bun run typeorm -- schema:drop",
"schema:log": "bun run typeorm -- schema:log",
"generate:openapi": "ts-node -r tsconfig-paths/register scripts/generate-openapi.ts openapi.json"
"check:toolchain": "ts-node scripts/check-toolchain.ts",
"onboarding": "ts-node scripts/check-toolchain.ts"
},
Expand Down
50 changes: 50 additions & 0 deletions BackEnd/scripts/generate-openapi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,43 @@
/**
* Standalone script to generate the OpenAPI spec JSON file.
* Used by CI to produce a spec artifact for breaking-change diffing.
*
* Usage: ts-node scripts/generate-openapi.ts [output-path]
* Default output: openapi.json (repo root of BackEnd)
*/
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { writeFileSync } from 'fs';
import { resolve } from 'path';

// Minimal env defaults so the app can bootstrap without a full .env
process.env.NODE_ENV = process.env.NODE_ENV ?? 'production';
process.env.JWT_SECRET =
process.env.JWT_SECRET ?? 'ci-placeholder-secret-32-chars-ok';
process.env.JWT_REFRESH_SECRET =
process.env.JWT_REFRESH_SECRET ?? 'ci-placeholder-refresh-secret-ok';
process.env.JWT_ACCESS_TOKEN_EXPIRATION =
process.env.JWT_ACCESS_TOKEN_EXPIRATION ?? '15m';
process.env.JWT_REFRESH_TOKEN_EXPIRATION =
process.env.JWT_REFRESH_TOKEN_EXPIRATION ?? '7d';
process.env.DATABASE_URL =
process.env.DATABASE_URL ?? 'postgres://ci:ci@localhost:5432/ci';
process.env.REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379';
process.env.STELLAR_NETWORK = process.env.STELLAR_NETWORK ?? 'testnet';

async function generate() {
// Lazy import so env defaults are set first
const { AppModule } = await import('../src/app.module');

const app = await NestFactory.create(AppModule, { logger: false });
app.setGlobalPrefix('api');

const builder = new DocumentBuilder()
.setTitle('StellarEarn API')
.setDescription('Quest-based earning platform on Stellar blockchain')
.setVersion('1.0')
.addServer('/api/v1', 'API v1')
.addServer('/api/v2', 'API v2')
* generate-openapi.ts
*
* Headless NestJS bootstrap that produces the OpenAPI JSON specification
Expand Down Expand Up @@ -113,6 +152,17 @@ async function main(): Promise<void> {
deepScanRoutes: true,
});

const outputPath = resolve(
process.argv[2] ?? `${__dirname}/../openapi.json`,
);
writeFileSync(outputPath, JSON.stringify(document, null, 2));
console.log(`OpenAPI spec written to ${outputPath}`);

await app.close();
}

generate().catch((err) => {
console.error('Failed to generate OpenAPI spec:', err);
// Validate the generated document has the expected structure
if (!document.openapi) {
throw new Error('[openapi-gen] Generated document is missing "openapi" field');
Expand Down
2 changes: 1 addition & 1 deletion BackEnd/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
"#src/*": ["src/*"]
}
},
"include": ["src/**/*"],
"include": ["src/**/*", "scripts/**/*"],
"exclude": ["node_modules", "dist"]
}
8 changes: 8 additions & 0 deletions BackEnd/tsconfig.scripts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist-scripts",
"noEmit": true
},
"include": ["scripts/**/*", "src/**/*"]
}
Loading