-
Notifications
You must be signed in to change notification settings - Fork 12k
feat: CI job to detect breaking api v2 changes #24028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ThyMinimalDev
merged 18 commits into
main
from
lauris/cal-6017-feature-detect-breaking-changes-in-v2-apis
Sep 25, 2025
+161
−30,278
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
bbbd099
refactor: move swagger to src/swagger
supalarry bc3530e
refactor: standalone swagger generation script
supalarry b382db9
refactor: generate only 1 swagger file
supalarry ca833d9
feat: github action checking breaking changes
supalarry 94636f7
chore: ensure openapi file is formatted
supalarry a91d8f0
chore: run breaking change check on label
supalarry 0a4007a
Merge branch 'main' into lauris/cal-6017-feature-detect-breaking-chan…
supalarry da90518
chore: have only 1 swagger file
supalarry ca5fc0b
fix: run breaking changes check on workflow call
supalarry 3c5a0ff
refactor: pr breaking jobs dependency
supalarry 531c4bd
fix: copy swagger module
supalarry 5657ebb
refactor: add check-label as dep
supalarry 6640b29
refactor: breaking changes check part of v2 e2e workflow
supalarry df77a66
refactor: run breaking changes before e2e
supalarry 096ddad
chore: add vapid env keys to workflow
supalarry 0ebb943
chore: add CI_JWT_SECRET to e2e api v2
ThyMinimalDev 7a0b598
chore: add NODE_ENV
supalarry 77a662a
Merge branch 'main' into lauris/cal-6017-feature-detect-breaking-chan…
supalarry File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import "dotenv/config"; | ||
|
|
||
| import { bootstrap } from "../app"; | ||
| import { createNestApp } from "../main"; | ||
| import { generateSwaggerForApp } from "../swagger/generate-swagger"; | ||
|
|
||
| generateSwagger() | ||
| .then(() => { | ||
| console.log("✅ Swagger generation completed successfully"); | ||
| process.exit(0); | ||
| }) | ||
| .catch((error: Error) => { | ||
| console.error("❌ Failed to generate swagger", { error: error.stack }); | ||
| process.exit(1); | ||
| }); | ||
|
|
||
| async function generateSwagger() { | ||
| const app = await createNestApp(); | ||
|
|
||
| try { | ||
| bootstrap(app); | ||
| await generateSwaggerForApp(app); | ||
| } catch (error) { | ||
| console.error(error); | ||
| throw error; | ||
| } finally { | ||
| await app.close(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { getEnv } from "@/env"; | ||
| import { Logger } from "@nestjs/common"; | ||
| import type { NestExpressApplication } from "@nestjs/platform-express"; | ||
| import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; | ||
| import { | ||
| PathItemObject, | ||
| PathsObject, | ||
| OperationObject, | ||
| } from "@nestjs/swagger/dist/interfaces/open-api-spec.interface"; | ||
| import "dotenv/config"; | ||
| import * as fs from "fs"; | ||
| import { Server } from "http"; | ||
| import { spawnSync } from "node:child_process"; | ||
|
|
||
| const HttpMethods: (keyof PathItemObject)[] = ["get", "post", "put", "delete", "patch", "options", "head"]; | ||
|
|
||
| export async function generateSwaggerForApp(app: NestExpressApplication<Server>) { | ||
| const logger = new Logger("App"); | ||
| logger.log(`Generating Swagger documentation...\n`); | ||
|
|
||
| const config = new DocumentBuilder().setTitle("Cal.com API v2").build(); | ||
| const document = SwaggerModule.createDocument(app, config); | ||
| document.paths = groupAndSortPathsByFirstTag(document.paths); | ||
|
|
||
| const docsOutputFile = "../../../docs/api-reference/v2/openapi.json"; | ||
| const stringifiedContents = JSON.stringify(document, null, 2); | ||
|
|
||
| if (fs.existsSync(docsOutputFile) && getEnv("NODE_ENV") === "development") { | ||
| fs.unlinkSync(docsOutputFile); | ||
| fs.writeFileSync(docsOutputFile, stringifiedContents, { encoding: "utf8" }); | ||
| spawnSync("npx", ["prettier", docsOutputFile, "--write"], { stdio: "inherit" }); | ||
| } | ||
|
|
||
| if (!process.env.DOCS_URL) { | ||
| SwaggerModule.setup("docs", app, document, { | ||
| customCss: ".swagger-ui .topbar { display: none }", | ||
| }); | ||
|
|
||
| logger.log(`Swagger documentation available in the "/docs" endpoint\n`); | ||
| } | ||
| } | ||
|
|
||
| function groupAndSortPathsByFirstTag(paths: PathsObject): PathsObject { | ||
| const groupedPaths: { [key: string]: PathsObject } = {}; | ||
|
|
||
| Object.keys(paths).forEach((pathKey) => { | ||
| const pathItem = paths[pathKey]; | ||
|
|
||
| HttpMethods.forEach((method) => { | ||
| const operation = pathItem[method]; | ||
|
|
||
| if (isOperationObject(operation) && operation.tags && operation.tags.length > 0) { | ||
| const firstTag = operation.tags[0]; | ||
|
|
||
| if (!groupedPaths[firstTag]) { | ||
| groupedPaths[firstTag] = {}; | ||
| } | ||
|
|
||
| groupedPaths[firstTag][pathKey] = pathItem; | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| const sortedTags = Object.keys(groupedPaths).sort(customTagSort); | ||
| const sortedPaths: PathsObject = {}; | ||
|
|
||
| sortedTags.forEach((tag) => { | ||
| Object.assign(sortedPaths, groupedPaths[tag]); | ||
| }); | ||
|
|
||
| return sortedPaths; | ||
| } | ||
|
|
||
| function customTagSort(a: string, b: string): number { | ||
| const platformPrefix = "Platform"; | ||
| const orgsPrefix = "Orgs"; | ||
|
|
||
| if (a.startsWith(platformPrefix) && !b.startsWith(platformPrefix)) { | ||
| return -1; | ||
| } | ||
| if (!a.startsWith(platformPrefix) && b.startsWith(platformPrefix)) { | ||
| return 1; | ||
| } | ||
|
|
||
| if (a.startsWith(orgsPrefix) && !b.startsWith(orgsPrefix)) { | ||
| return -1; | ||
| } | ||
| if (!a.startsWith(orgsPrefix) && b.startsWith(orgsPrefix)) { | ||
| return 1; | ||
| } | ||
|
|
||
| return a.localeCompare(b); | ||
| } | ||
|
|
||
| function isOperationObject(obj: any): obj is OperationObject { | ||
| return obj && typeof obj === "object" && "tags" in obj; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
🧩 Analysis chain
Pin the action version and stabilize the base spec reference
Apply this minimal change to stop tracking a moving branch:
Optionally, make the base deterministic by checking out main locally (example step to insert before “Check breaking changes”):
🌐 Web query:
💡 Result:
Latest stable release tag: v0.0.21. [1]
Recommended pinning: pin to the release's full commit SHA for immutability (or use the release tag if you want auto-updates); GitHub recommends using a commit SHA to ensure stability/security. [2]
Example:
Sources:
[1] oasdiff/oasdiff-action releases.
[2] GitHub Actions docs — pin actions to SHAs.
Pin oasdiff action and make base spec deterministic
Pin the action to the latest stable release (v0.0.21) — preferably the release's full commit SHA for immutability — and stop fetching the base spec from a moving remote URL; checkout main and diff a local file.
File: .github/workflows/e2e-api-v2.yml (lines 83–89)
Optional step to add before the check step:
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@supalarry might be worth checking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.