mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
ci(release-tooling): update release note models and prompts (#11936)
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
"@actions/core": "3.0.1",
|
||||
"@octokit/request-error": "7.1.1",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"ai": "7.0.112",
|
||||
"ai": "7.0.143",
|
||||
"conventional-changelog-conventionalcommits": "10.4.0",
|
||||
"conventional-commits-parser": "7.1.2",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
|
||||
@@ -30,6 +30,13 @@ function containsUnsupportedNode(
|
||||
);
|
||||
}
|
||||
|
||||
function parseMarkdown(value: string) {
|
||||
return fromMarkdown(value, {
|
||||
extensions: [gfm()],
|
||||
mdastExtensions: [gfmFromMarkdown()],
|
||||
});
|
||||
}
|
||||
|
||||
export function containsUnsupportedGeneratedMarkdown(
|
||||
value: string,
|
||||
policy: GeneratedMarkdownPolicy,
|
||||
@@ -39,11 +46,17 @@ export function containsUnsupportedGeneratedMarkdown(
|
||||
}
|
||||
const allowedNodes =
|
||||
policy === "description" ? descriptionNodes : inlineNodes;
|
||||
const markdown = fromMarkdown(value, {
|
||||
extensions: [gfm()],
|
||||
mdastExtensions: [gfmFromMarkdown()],
|
||||
});
|
||||
return containsUnsupportedNode(markdown, allowedNodes);
|
||||
return containsUnsupportedNode(parseMarkdown(value), allowedNodes);
|
||||
}
|
||||
|
||||
export function inlineCodeValues(value: string): string[] {
|
||||
return parseMarkdown(value).children.flatMap((node) =>
|
||||
node.type === "paragraph"
|
||||
? node.children.flatMap((child) =>
|
||||
child.type === "inlineCode" ? [child.value] : [],
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
export function formatUntrustedInlineMarkdown(value: string): string {
|
||||
|
||||
@@ -8,7 +8,7 @@ interface ReleaseModels {
|
||||
}
|
||||
|
||||
export const models: ReleaseModels = {
|
||||
changeset: gateway("openai/gpt-6-luna"),
|
||||
releaseNotes: gateway("openai/gpt-6-sol"),
|
||||
releaseNotesReviewer: gateway("anthropic/claude-sonnet-5"),
|
||||
changeset: gateway("anthropic/claude-haiku-5.5"),
|
||||
releaseNotes: gateway("anthropic/claude-sonnet-5.5"),
|
||||
releaseNotesReviewer: gateway("openai/gpt-6.1-sol"),
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
containsUnsupportedGeneratedMarkdown,
|
||||
formatUntrustedInlineMarkdown,
|
||||
inlineCodeValues,
|
||||
} from "../ai/generated-copy.ts";
|
||||
import { FILTERED_DOMAINS } from "../change-classifier.ts";
|
||||
import type {
|
||||
@@ -107,8 +108,8 @@ export function formatReleaseBody(
|
||||
|
||||
lines.push(`- ${title}${prLink}`);
|
||||
|
||||
if (changeType === "breaking" && rewrite?.migration) {
|
||||
lines.push(`> **Migration:** ${rewrite.migration}`);
|
||||
if (rewrite?.migration) {
|
||||
lines.push(`> **Action required:** ${rewrite.migration}`);
|
||||
} else if (
|
||||
changeType === "breaking" &&
|
||||
entry.changesetDescription &&
|
||||
@@ -119,7 +120,7 @@ export function formatReleaseBody(
|
||||
) {
|
||||
const [migration, ...details] =
|
||||
entry.changesetDescription.split("\n");
|
||||
lines.push(`> **Migration:** ${migration}`);
|
||||
lines.push(`> **Action required:** ${migration}`);
|
||||
for (const line of details) {
|
||||
lines.push(line ? `> ${line}` : ">");
|
||||
}
|
||||
@@ -193,17 +194,15 @@ function readReleaseRewrites(
|
||||
parsed.rewrites.map((rewrite) => [rewrite.id, rewrite]),
|
||||
);
|
||||
|
||||
const breakingKeys = new Set(
|
||||
manifest.entries
|
||||
.filter((entry) => entry.breaking)
|
||||
.map((entry) => entry.rewriteKey),
|
||||
const entriesByKey = new Map(
|
||||
manifest.entries.map((entry) => [entry.rewriteKey, entry]),
|
||||
);
|
||||
const rewrites: ReleaseRewrites = {};
|
||||
for (const [key, value] of generatedByKey) {
|
||||
try {
|
||||
rewrites[key] = validateGeneratedReleaseRewrite(
|
||||
value,
|
||||
breakingKeys.has(key),
|
||||
entriesByKey.get(key)!,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
@@ -217,7 +216,10 @@ function readReleaseRewrites(
|
||||
|
||||
export function validateGeneratedReleaseRewrite(
|
||||
value: GeneratedReleaseRewrites[number],
|
||||
breaking: boolean,
|
||||
{
|
||||
changeType,
|
||||
changesetDescription,
|
||||
}: Pick<ReleaseEntry, "changeType" | "changesetDescription">,
|
||||
): ReleaseRewrites[string] {
|
||||
const title = validateGeneratedCopy(value.title, "title", value.id, 300);
|
||||
const migration =
|
||||
@@ -225,14 +227,17 @@ export function validateGeneratedReleaseRewrite(
|
||||
? undefined
|
||||
: validateGeneratedCopy(value.migration, "migration", value.id, 500);
|
||||
|
||||
if (breaking && !migration) {
|
||||
if (changeType === "breaking" && !migration) {
|
||||
throw new Error(
|
||||
`Breaking AI rewrite ${value.id} must have a single-line migration`,
|
||||
);
|
||||
}
|
||||
if (!breaking && migration !== undefined) {
|
||||
|
||||
// Users act on this line, so it may only quote code a maintainer approved.
|
||||
const quotedCode = inlineCodeValues(migration ?? "");
|
||||
if (quotedCode.some((code) => !changesetDescription?.includes(code))) {
|
||||
throw new Error(
|
||||
`Non-breaking AI rewrite ${value.id} cannot have a migration`,
|
||||
`AI rewrite ${value.id} migration quotes code that is not in the changeset`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,5 +14,9 @@ Keep every title to one clear sentence and one line. Start with a past-tense
|
||||
verb. Wrap code identifiers in backticks. Do not include links, HTML, images,
|
||||
bold or italic emphasis, `@mentions`, PR numbers, or author attribution.
|
||||
|
||||
Return every input ID exactly once. Use `migration: null` for non-breaking
|
||||
changes and a single-line migration action for breaking changes.
|
||||
Return every input ID exactly once. Write a single-line `migration` for a
|
||||
breaking change, and for any other change only when the description says
|
||||
upgrading alone is not enough, such as when users must change code or
|
||||
configuration or run a database migration. Start it with an imperative verb or
|
||||
an "If you ..." clause, and quote only code that appears in the description.
|
||||
Otherwise use `migration: null`.
|
||||
|
||||
@@ -8,11 +8,15 @@ Review every rewrite against its matching context. Approve it only when it:
|
||||
|
||||
- preserves the direction and user-visible meaning of the change
|
||||
- makes no claim unsupported by the title or changeset description
|
||||
- keeps API names, compatibility conditions, security guarantees, and
|
||||
migration requirements that users need to act on the change
|
||||
- keeps API names, compatibility conditions, security guarantees, migration
|
||||
requirements, the affected case, and the restored version that users need
|
||||
to judge or act on the change
|
||||
- is clear, grammatical, and concise
|
||||
- follows the requested change type and contains migration guidance only for a
|
||||
breaking change
|
||||
- has migration guidance for a breaking change, and for any other change only
|
||||
when the description says upgrading alone is not enough, such as when users
|
||||
must change code or configuration or run a database migration
|
||||
- addresses migration guidance only to the affected users and quotes only code
|
||||
that appears in the description
|
||||
|
||||
Do not reject copy merely because it omits internal implementation details.
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ the final Markdown deterministically.
|
||||
- Start with a past-tense verb such as "Fixed", "Added", or "Improved"
|
||||
- Keep each title to one sentence and one line
|
||||
- Describe the user-visible impact, not the internal implementation
|
||||
- Keep the affected case, the security check, or the restored version when the
|
||||
description states it, because most users read only the title
|
||||
- Wrap code identifiers in backticks, but not general concepts
|
||||
- Do not include PR numbers or author attribution in titles
|
||||
- Do not include links, HTML, images, bold or italic emphasis, or `@mentions`
|
||||
@@ -31,13 +33,25 @@ the final Markdown deterministically.
|
||||
- If a title remains unclear, preserve its factual meaning without inventing
|
||||
behavior that is not present in the supplied context
|
||||
|
||||
For a change whose `changeType` is `breaking`, also write a single-line
|
||||
`migration` explaining what users must change. Do not add `migration` to any
|
||||
other change type.
|
||||
Write a single-line `migration` for a `breaking` change, and for any other
|
||||
change whose description says upgrading alone is not enough, such as when
|
||||
users must change code or configuration or run a database migration. Use
|
||||
`migration: null` for every other change. It renders as "Action required", so
|
||||
start with an imperative verb, or with an "If you ..." clause when only some
|
||||
users are affected. Quote only code that appears in the changeset description,
|
||||
because users run what this line tells them to.
|
||||
|
||||
For example, a fix whose description tells users to add the domain to
|
||||
`trustedOrigins`:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Fixed magic link verification to reject links opened from an untrusted origin",
|
||||
"migration": "If you open magic links from another domain, add it to `trustedOrigins`."
|
||||
}
|
||||
```
|
||||
|
||||
## Output rules
|
||||
|
||||
- Include every input change ID exactly once
|
||||
- Do not add unknown change IDs
|
||||
- Use `migration: null` for non-breaking changes
|
||||
- Use a migration string for breaking changes
|
||||
|
||||
@@ -21,7 +21,7 @@ const maxBatchEntries = 30;
|
||||
const maxBatchCharacters = 60_000;
|
||||
const maxContextCharacters = 500_000;
|
||||
const maxOutputTokensPerBatch = 32_000;
|
||||
const maxReviewOutputTokensPerBatch = 8_000;
|
||||
const maxReviewOutputTokensPerBatch = 32_000;
|
||||
const defaultFallbackReason = "The rewrite was not approved by review.";
|
||||
const invalidCopyFallbackReason =
|
||||
"The rewrite did not pass deterministic copy validation.";
|
||||
@@ -136,7 +136,7 @@ function validationFeedback(
|
||||
if (!entry)
|
||||
throw new Error(`Release rewrite context ${rewrite.id} is missing`);
|
||||
try {
|
||||
validateGeneratedReleaseRewrite(rewrite, entry.changeType === "breaking");
|
||||
validateGeneratedReleaseRewrite(rewrite, entry);
|
||||
return null;
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -71,7 +71,7 @@ const releaseRewriteSchema = z.strictObject({
|
||||
.max(500)
|
||||
.regex(/^[^\r\n]+$/)
|
||||
.nullable()
|
||||
.describe("A migration action for breaking changes, otherwise null"),
|
||||
.describe("An action users must take, otherwise null"),
|
||||
});
|
||||
|
||||
export const releaseRewritesSchema = z.strictObject({
|
||||
|
||||
@@ -27,6 +27,7 @@ interface CommandResult {
|
||||
function renderReleaseRewrites(
|
||||
rewrites: JSONValue,
|
||||
breaking = false,
|
||||
changesetDescription = "Avoid duplicate session requests.",
|
||||
): CommandResult {
|
||||
const directory = mkdtempSync(
|
||||
resolve(tmpdir(), "better-auth-release-notes-"),
|
||||
@@ -56,7 +57,7 @@ function renderReleaseRewrites(
|
||||
id: "pr-10769:better-auth",
|
||||
rewriteKey: "pr-10769",
|
||||
title: "fix(client): avoid duplicate requests",
|
||||
changesetDescription: "Avoid duplicate session requests.",
|
||||
changesetDescription,
|
||||
prNumber: 10769,
|
||||
author: "bytaesu",
|
||||
domain: "core",
|
||||
@@ -144,7 +145,7 @@ describe("AI release-note rewrites", () => {
|
||||
);
|
||||
expect(output).not.toContain("Read https://example.com before upgrading");
|
||||
expect(output).toContain(
|
||||
"> **Migration:** Update custom session refresh integrations.",
|
||||
"> **Action required:** Update custom session refresh integrations.",
|
||||
);
|
||||
expect(output).toContain(
|
||||
"- Changed session refresh behavior Injected paragraph",
|
||||
@@ -243,21 +244,84 @@ describe("AI release-note rewrites", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses deterministic copy when a non-breaking rewrite has a migration", () => {
|
||||
it("renders migration copy for a non-breaking change that asks users to act", () => {
|
||||
const result = renderReleaseRewrites(
|
||||
{
|
||||
rewrites: [
|
||||
{
|
||||
id: "pr-10769",
|
||||
title: "Fixed duplicate requests",
|
||||
migration: "Set `clientId` on the provider.",
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
"Set `clientId` on the provider.",
|
||||
);
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.output).toContain("- Fixed duplicate requests");
|
||||
expect(result.output).toContain(
|
||||
"> **Action required:** Set `clientId` on the provider.",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses deterministic copy when a migration quotes code missing from the changeset", () => {
|
||||
const result = renderReleaseRewrites({
|
||||
rewrites: [
|
||||
{
|
||||
id: "pr-10769",
|
||||
title: "Fixed duplicate requests",
|
||||
migration: "Change an unrelated setting",
|
||||
migration: "Run `npx unknown-package` before upgrading.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.output).toContain("fix(client): avoid duplicate requests");
|
||||
expect(result.output).not.toContain("unknown-package");
|
||||
expect(result.stderr).toContain(
|
||||
"Non-breaking AI rewrite pr-10769 cannot have a migration",
|
||||
"AI rewrite pr-10769 migration quotes code that is not in the changeset",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses deterministic copy when a migration hides code in doubled backticks", () => {
|
||||
const result = renderReleaseRewrites({
|
||||
rewrites: [
|
||||
{
|
||||
id: "pr-10769",
|
||||
title: "Fixed duplicate requests",
|
||||
migration: "Run ``npx unknown-package`` before upgrading.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.output).not.toContain("unknown-package");
|
||||
expect(result.stderr).toContain(
|
||||
"AI rewrite pr-10769 migration quotes code that is not in the changeset",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses deterministic copy when an escaped backtick shifts a code span", () => {
|
||||
const result = renderReleaseRewrites(
|
||||
{
|
||||
rewrites: [
|
||||
{
|
||||
id: "pr-10769",
|
||||
title: "Fixed duplicate requests",
|
||||
migration: "Run \\`clientId`npx unknown-package`clientId` now.",
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
"Set `clientId` on the provider.",
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.output).not.toContain("unknown-package");
|
||||
expect(result.stderr).toContain(
|
||||
"AI rewrite pr-10769 migration quotes code that is not in the changeset",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -298,7 +362,7 @@ describe("AI release-note rewrites", () => {
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.output).toContain(
|
||||
"> **Migration:** Update custom session refresh integrations.",
|
||||
"> **Action required:** Update custom session refresh integrations.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -483,8 +483,8 @@ describe("release-note rewriting", () => {
|
||||
rewrites: [
|
||||
{
|
||||
id,
|
||||
title: "Preserved safe output",
|
||||
migration: "Change an unrelated setting.",
|
||||
title: "Preserved [safe output](https://example.com)",
|
||||
migration: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
Generated
+21
-21
@@ -1693,8 +1693,8 @@ importers:
|
||||
specifier: 22.0.1
|
||||
version: 22.0.1
|
||||
ai:
|
||||
specifier: 7.0.112
|
||||
version: 7.0.112(zod@4.5.4)
|
||||
specifier: 7.0.143
|
||||
version: 7.0.143(zod@4.5.4)
|
||||
conventional-changelog-conventionalcommits:
|
||||
specifier: 10.4.0
|
||||
version: 10.4.0
|
||||
@@ -1951,8 +1951,8 @@ packages:
|
||||
peerDependencies:
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/gateway@4.0.90':
|
||||
resolution: {integrity: sha512-mtmiicpMapQU8K0ynNXCRcO55GVyBv6kjbwsD15RMxJKZUUPR99HfQ7/BsdRLdjSQcL7YcCauMLbguw6J+JFjQ==}
|
||||
'@ai-sdk/gateway@4.0.116':
|
||||
resolution: {integrity: sha512-YK2As2GpxmyHi5w6R4FLepiLcs6MbIMxh6nyTMgHaoHKoM5aWEXvKDe/JqdMQ1ivOy3EPJiEBgRI677YTGxfHQ==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
zod: 4.5.4
|
||||
@@ -1975,8 +1975,8 @@ packages:
|
||||
peerDependencies:
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/provider-utils@5.0.46':
|
||||
resolution: {integrity: sha512-XYpiJbd3jW12HC4yzqNmUuPbZ7ooKXVxQjnXCC3O/VzzV2OvRWD6lDFUYYPLs/ZJgTcWWUkmHL/vIyvnxyplVA==}
|
||||
'@ai-sdk/provider-utils@5.0.62':
|
||||
resolution: {integrity: sha512-//6sHa6QNQdM2KHOg5pxp1UMFQB4U8cqdw/kSm9BMr0DLeaFgG3TXZfxhpwRZmxgBpOrzMVZ/gwLXH309+Ortw==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
zod: 4.5.4
|
||||
@@ -1985,8 +1985,8 @@ packages:
|
||||
resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/provider@4.0.18':
|
||||
resolution: {integrity: sha512-+GZJIgz1jk86pwEbb3f1BD2bdoSKyWE4Jg4YUc7NMnMozbWemSKYZcw2F4nMaO5qwsL5A8RmioAKW81YktRpIQ==}
|
||||
'@ai-sdk/provider@4.0.29':
|
||||
resolution: {integrity: sha512-nV7UpLADtKn9f0xfnxVZAeFEKuNkScJv5rOpHL7qeWe2EwEEBa3pd6P0QVK6D6coCJ0E8FyCgoM50AffTYrzDQ==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@ai-sdk/react@3.0.210':
|
||||
@@ -8019,8 +8019,8 @@ packages:
|
||||
peerDependencies:
|
||||
zod: 4.5.4
|
||||
|
||||
ai@7.0.112:
|
||||
resolution: {integrity: sha512-JDIaxfynFQoV/MPh8AUeHIpuFAUjA2uPBXd6Aa8oZBHtiQ58gJ+Q3NHsQUgavIHsA734oCIr3y9QWkhlgCEA0g==}
|
||||
ai@7.0.143:
|
||||
resolution: {integrity: sha512-y9QNgCK9QRHRCcHEUNoU2vemJ+q2zFX5M9T7xHCMZutfcWCqZPckkAfDbp+KHwqZDiJs5AqoKazj64cb20m5PA==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
zod: 4.5.4
|
||||
@@ -15928,10 +15928,10 @@ snapshots:
|
||||
'@vercel/oidc': 3.2.0
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/gateway@4.0.90(zod@4.5.4)':
|
||||
'@ai-sdk/gateway@4.0.116(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 4.0.18
|
||||
'@ai-sdk/provider-utils': 5.0.46(zod@4.5.4)
|
||||
'@ai-sdk/provider': 4.0.29
|
||||
'@ai-sdk/provider-utils': 5.0.62(zod@4.5.4)
|
||||
'@vercel/oidc': 3.2.0
|
||||
zod: 4.5.4
|
||||
|
||||
@@ -15954,20 +15954,20 @@ snapshots:
|
||||
eventsource-parser: 3.1.0
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/provider-utils@5.0.46(zod@4.5.4)':
|
||||
'@ai-sdk/provider-utils@5.0.62(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 4.0.18
|
||||
'@ai-sdk/provider': 4.0.29
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@workflow/serde': 4.1.0
|
||||
eventsource-parser: 3.1.0
|
||||
undici: 7.29.0
|
||||
undici: 7.30.0
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/provider@3.0.10':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/provider@4.0.18':
|
||||
'@ai-sdk/provider@4.0.29':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
@@ -24025,11 +24025,11 @@ snapshots:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
zod: 4.5.4
|
||||
|
||||
ai@7.0.112(zod@4.5.4):
|
||||
ai@7.0.143(zod@4.5.4):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 4.0.90(zod@4.5.4)
|
||||
'@ai-sdk/provider': 4.0.18
|
||||
'@ai-sdk/provider-utils': 5.0.46(zod@4.5.4)
|
||||
'@ai-sdk/gateway': 4.0.116(zod@4.5.4)
|
||||
'@ai-sdk/provider': 4.0.29
|
||||
'@ai-sdk/provider-utils': 5.0.62(zod@4.5.4)
|
||||
zod: 4.5.4
|
||||
|
||||
ajv-formats@3.0.1(ajv@8.18.0):
|
||||
|
||||
Reference in New Issue
Block a user