mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 00:22:43 -05:00
348 lines
10 KiB
TypeScript
348 lines
10 KiB
TypeScript
/**
|
|
* PR Analyzer — shared classification module
|
|
*
|
|
* Pure functions for mapping conventional commit scopes and file paths
|
|
* to domain labels. No side effects, no network calls.
|
|
*
|
|
* Used by: auto-changeset.ts, release-notes.ts
|
|
*/
|
|
|
|
const SCOPE_TO_DOMAIN: Record<string, string> = {
|
|
// core
|
|
core: "core",
|
|
api: "core",
|
|
client: "core",
|
|
cookies: "core",
|
|
crypto: "core",
|
|
account: "core",
|
|
session: "core",
|
|
instrumentation: "core",
|
|
"last-login-method": "core",
|
|
"redis-storage": "core",
|
|
|
|
// database
|
|
db: "database",
|
|
adapters: "database",
|
|
"drizzle-adapter": "database",
|
|
"prisma-adapter": "database",
|
|
"kysely-adapter": "database",
|
|
"mongo-adapter": "database",
|
|
"memory-adapter": "database",
|
|
|
|
// oauth
|
|
"oauth-proxy": "oauth",
|
|
"one-tap": "oauth",
|
|
"generic-oauth": "oauth",
|
|
"social-provider": "oauth",
|
|
|
|
// credentials
|
|
"magic-link": "credentials",
|
|
"email-otp": "credentials",
|
|
"phone-number": "credentials",
|
|
phone: "credentials",
|
|
username: "credentials",
|
|
anonymous: "credentials",
|
|
siwe: "credentials",
|
|
passkey: "credentials",
|
|
|
|
// identity
|
|
"oauth-provider": "identity",
|
|
"oidc-provider": "identity",
|
|
mcp: "identity",
|
|
"device-authorization": "identity",
|
|
|
|
// organization
|
|
organization: "organization",
|
|
admin: "organization",
|
|
access: "organization",
|
|
|
|
// security
|
|
"two-factor": "security",
|
|
"2fa": "security",
|
|
captcha: "security",
|
|
haveibeenpwned: "security",
|
|
"rate-limiter": "security",
|
|
|
|
// enterprise
|
|
sso: "enterprise",
|
|
scim: "enterprise",
|
|
|
|
// payments
|
|
stripe: "payments",
|
|
"api-key": "payments",
|
|
|
|
// platform
|
|
expo: "platform",
|
|
electron: "platform",
|
|
|
|
// devtools
|
|
cli: "devtools",
|
|
telemetry: "devtools",
|
|
i18n: "devtools",
|
|
"test-utils": "devtools",
|
|
"open-api": "devtools",
|
|
|
|
// devops (filtered from release notes)
|
|
build: "devops",
|
|
ci: "devops",
|
|
deps: "devops",
|
|
"deps-dev": "devops",
|
|
knip: "devops",
|
|
|
|
// docs (filtered from release notes)
|
|
docs: "docs",
|
|
blog: "docs",
|
|
landing: "docs",
|
|
};
|
|
|
|
const PATH_TO_DOMAIN: [string, string][] = [
|
|
["packages/oauth-provider/", "identity"],
|
|
["packages/better-auth/src/plugins/oidc-provider/", "identity"],
|
|
["packages/better-auth/src/plugins/mcp/", "identity"],
|
|
["packages/better-auth/src/plugins/device-authorization/", "identity"],
|
|
["packages/better-auth/src/plugins/magic-link/", "credentials"],
|
|
["packages/better-auth/src/plugins/email-otp/", "credentials"],
|
|
["packages/better-auth/src/plugins/phone-number/", "credentials"],
|
|
["packages/better-auth/src/plugins/username/", "credentials"],
|
|
["packages/better-auth/src/plugins/anonymous/", "credentials"],
|
|
["packages/better-auth/src/plugins/siwe/", "credentials"],
|
|
["packages/passkey/", "credentials"],
|
|
["packages/better-auth/src/plugins/two-factor/", "security"],
|
|
["packages/better-auth/src/api/rate-limiter/", "security"],
|
|
["packages/better-auth/src/plugins/captcha/", "security"],
|
|
["packages/better-auth/src/plugins/haveibeenpwned/", "security"],
|
|
["packages/better-auth/src/plugins/organization/", "organization"],
|
|
["packages/better-auth/src/plugins/admin/", "organization"],
|
|
["packages/better-auth/src/plugins/access/", "organization"],
|
|
["packages/better-auth/src/plugins/generic-oauth/", "oauth"],
|
|
["packages/better-auth/src/plugins/oauth-proxy/", "oauth"],
|
|
["packages/better-auth/src/plugins/one-tap/", "oauth"],
|
|
["packages/better-auth/src/oauth2/", "oauth"],
|
|
["packages/core/src/social-providers/", "oauth"],
|
|
["packages/core/src/oauth2/", "oauth"],
|
|
["packages/sso/", "enterprise"],
|
|
["packages/scim/", "enterprise"],
|
|
["packages/stripe/", "payments"],
|
|
["packages/api-key/", "payments"],
|
|
["packages/better-auth/src/db/", "database"],
|
|
["packages/better-auth/src/adapters/", "database"],
|
|
["packages/drizzle-adapter/", "database"],
|
|
["packages/prisma-adapter/", "database"],
|
|
["packages/mongo-adapter/", "database"],
|
|
["packages/kysely-adapter/", "database"],
|
|
["packages/memory-adapter/", "database"],
|
|
["packages/expo/", "platform"],
|
|
["packages/electron/", "platform"],
|
|
["packages/better-auth/src/integrations/", "platform"],
|
|
["packages/cli/", "devtools"],
|
|
["packages/better-auth/src/plugins/open-api/", "devtools"],
|
|
["packages/telemetry/", "devtools"],
|
|
["packages/i18n/", "devtools"],
|
|
["packages/test-utils/", "devtools"],
|
|
// Session-related plugins → core
|
|
["packages/better-auth/src/plugins/jwt/", "core"],
|
|
["packages/better-auth/src/plugins/bearer/", "core"],
|
|
["packages/better-auth/src/plugins/multi-session/", "core"],
|
|
["packages/better-auth/src/plugins/custom-session/", "core"],
|
|
["packages/redis-storage/", "core"],
|
|
// Catch-all for better-auth and core packages
|
|
["packages/better-auth/", "core"],
|
|
["packages/core/", "core"],
|
|
// Non-user-facing
|
|
["docs/", "docs"],
|
|
["demo/", "docs"],
|
|
[".github/", "devops"],
|
|
["e2e/", "devops"],
|
|
];
|
|
|
|
export interface ConventionalCommit {
|
|
type: string;
|
|
scope: string;
|
|
subject: string;
|
|
breaking: boolean;
|
|
}
|
|
|
|
export function parseConventionalCommit(title: string): ConventionalCommit {
|
|
const typeMatch = title.match(/^([a-z]+)/);
|
|
const type = typeMatch?.[1] ?? "";
|
|
const scopeMatch = title.match(/^[a-z]+\(([^)]+)\)/);
|
|
const scope = scopeMatch?.[1] ?? "";
|
|
const breaking = /^[a-z]+(\([^)]+\))?!:/.test(title);
|
|
const subject = title.replace(/^[a-z]+(\([^)]+\))?!?:\s*/, "");
|
|
return { type, scope, subject, breaking };
|
|
}
|
|
|
|
function classifyDomain(filePath: string): string | undefined {
|
|
for (const [prefix, domain] of PATH_TO_DOMAIN) {
|
|
if (filePath.startsWith(prefix)) return domain;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function resolveDomain(
|
|
scope: string | undefined,
|
|
changedFiles: string[],
|
|
): string {
|
|
if (scope) {
|
|
const domain = SCOPE_TO_DOMAIN[scope];
|
|
if (domain) return domain;
|
|
}
|
|
|
|
const counts: Record<string, number> = {};
|
|
for (const file of changedFiles) {
|
|
const domain = classifyDomain(file);
|
|
if (domain) {
|
|
counts[domain] = (counts[domain] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
const domains = Object.keys(counts);
|
|
if (domains.length === 0) return "devops";
|
|
if (domains.length >= 3) return "core";
|
|
|
|
return domains.sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0))[0]!;
|
|
}
|
|
|
|
export function mapTypeToBump(
|
|
type: string,
|
|
breaking: boolean,
|
|
): "patch" | "minor" | "major" | "skip" {
|
|
if (breaking) return "major";
|
|
switch (type) {
|
|
case "fix":
|
|
case "perf":
|
|
case "refactor":
|
|
return "patch";
|
|
case "feat":
|
|
return "minor";
|
|
case "chore":
|
|
case "docs":
|
|
case "ci":
|
|
case "test":
|
|
case "style":
|
|
case "build":
|
|
return "skip";
|
|
default:
|
|
return "patch";
|
|
}
|
|
}
|
|
|
|
/** Domains in display order for release notes */
|
|
export const DOMAIN_ORDER = [
|
|
"core",
|
|
"database",
|
|
"oauth",
|
|
"credentials",
|
|
"identity",
|
|
"organization",
|
|
"security",
|
|
"enterprise",
|
|
"payments",
|
|
"platform",
|
|
"devtools",
|
|
] as const;
|
|
|
|
/** Domains excluded from release notes */
|
|
export const FILTERED_DOMAINS = new Set(["docs", "devops"]);
|
|
|
|
// ── Package resolution (for release notes output) ─────────────────────
|
|
|
|
/**
|
|
* Maps commit scopes to npm package names.
|
|
* Used by release-notes.ts to group entries by the package users install.
|
|
*/
|
|
const SCOPE_TO_PACKAGE: Record<string, string> = {
|
|
sso: "@better-auth/sso",
|
|
scim: "@better-auth/scim",
|
|
passkey: "@better-auth/passkey",
|
|
"oauth-provider": "@better-auth/oauth-provider",
|
|
stripe: "@better-auth/stripe",
|
|
"api-key": "@better-auth/api-key",
|
|
expo: "@better-auth/expo",
|
|
electron: "@better-auth/electron",
|
|
i18n: "@better-auth/i18n",
|
|
"test-utils": "@better-auth/test-utils",
|
|
"drizzle-adapter": "@better-auth/drizzle-adapter",
|
|
"prisma-adapter": "@better-auth/prisma-adapter",
|
|
"kysely-adapter": "@better-auth/kysely-adapter",
|
|
"mongo-adapter": "@better-auth/mongo-adapter",
|
|
"memory-adapter": "@better-auth/memory-adapter",
|
|
"redis-storage": "@better-auth/redis-storage",
|
|
cli: "auth",
|
|
};
|
|
|
|
/**
|
|
* Maps file path prefixes to npm package names.
|
|
* Order matters: more specific paths must come before catch-alls.
|
|
*/
|
|
const PATH_TO_PACKAGE: [string, string][] = [
|
|
["packages/sso/", "@better-auth/sso"],
|
|
["packages/scim/", "@better-auth/scim"],
|
|
["packages/passkey/", "@better-auth/passkey"],
|
|
["packages/oauth-provider/", "@better-auth/oauth-provider"],
|
|
["packages/stripe/", "@better-auth/stripe"],
|
|
["packages/api-key/", "@better-auth/api-key"],
|
|
["packages/expo/", "@better-auth/expo"],
|
|
["packages/electron/", "@better-auth/electron"],
|
|
["packages/i18n/", "@better-auth/i18n"],
|
|
["packages/redis-storage/", "@better-auth/redis-storage"],
|
|
["packages/test-utils/", "@better-auth/test-utils"],
|
|
["packages/telemetry/", "@better-auth/telemetry"],
|
|
["packages/drizzle-adapter/", "@better-auth/drizzle-adapter"],
|
|
["packages/prisma-adapter/", "@better-auth/prisma-adapter"],
|
|
["packages/kysely-adapter/", "@better-auth/kysely-adapter"],
|
|
["packages/mongo-adapter/", "@better-auth/mongo-adapter"],
|
|
["packages/memory-adapter/", "@better-auth/memory-adapter"],
|
|
// Catch-all: everything in better-auth or core maps to the main package
|
|
["packages/better-auth/", "better-auth"],
|
|
["packages/core/", "better-auth"],
|
|
["packages/cli/", "auth"],
|
|
];
|
|
|
|
/**
|
|
* Resolves the npm package name for release notes grouping.
|
|
* Priority: scope match > file path match > "better-auth" fallback.
|
|
*/
|
|
export function resolvePackage(
|
|
scope: string | undefined,
|
|
changedFiles: string[],
|
|
): string {
|
|
if (scope) {
|
|
const pkg = SCOPE_TO_PACKAGE[scope];
|
|
if (pkg) return pkg;
|
|
}
|
|
|
|
const counts: Record<string, number> = {};
|
|
for (const file of changedFiles) {
|
|
for (const [prefix, pkg] of PATH_TO_PACKAGE) {
|
|
if (file.startsWith(prefix)) {
|
|
counts[pkg] = (counts[pkg] ?? 0) + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const packages = Object.keys(counts);
|
|
if (packages.length === 0) return "better-auth";
|
|
|
|
// If files span multiple external packages, return the one with the most hits.
|
|
// If all files are in better-auth, return better-auth.
|
|
return packages.sort((a, b) => {
|
|
// Prefer non-better-auth packages (they're more specific)
|
|
const aIsCore = a === "better-auth" ? 1 : 0;
|
|
const bIsCore = b === "better-auth" ? 1 : 0;
|
|
if (aIsCore !== bIsCore) return aIsCore - bIsCore;
|
|
return (counts[b] ?? 0) - (counts[a] ?? 0);
|
|
})[0]!;
|
|
}
|
|
|
|
/** Classifies a conventional commit type into a release notes category. */
|
|
export function classifyChangeType(
|
|
type: string,
|
|
breaking: boolean,
|
|
): "breaking" | "feat" | "fix" {
|
|
if (breaking) return "breaking";
|
|
if (type === "feat") return "feat";
|
|
return "fix";
|
|
}
|