diff --git a/packages/cli/cli/changes/unreleased/add-pr-formatting-to-automations-upgrade.yml b/packages/cli/cli/changes/unreleased/add-pr-formatting-to-automations-upgrade.yml new file mode 100644 index 00000000000..dc582bc51b5 --- /dev/null +++ b/packages/cli/cli/changes/unreleased/add-pr-formatting-to-automations-upgrade.yml @@ -0,0 +1,5 @@ +- summary: | + Add PR title, body, and commit message to `fern automations upgrade --json` output. + The new `pr` field in the JSON output allows GitHub Actions consumers to use + CLI-generated PR formatting directly, eliminating duplicated presentation logic. + type: feat diff --git a/packages/cli/cli/src/cli.ts b/packages/cli/cli/src/cli.ts index b2d2c9aa784..a14152909ef 100644 --- a/packages/cli/cli/src/cli.ts +++ b/packages/cli/cli/src/cli.ts @@ -2618,7 +2618,12 @@ function addAutomationsGenerateCommand(cli: Argv, cliContext: * } * ], * "skippedMajor": [{ "name": "...", "current": "0.28.0", "latest": "1.37.0" }], - * "alreadyUpToDate": [{ "name": "...", "version": "3.65.5" }] + * "alreadyUpToDate": [{ "name": "...", "version": "3.65.5" }], + * "pr": { + * "title": "chore(fern): upgrade CLI 4.66.0 → 4.96.0 and 1 generator", + * "body": "## Fern Upgrade\n...", + * "commitMessage": "chore: upgrade fern cli 4.66.0 -> 4.96.0, typescript-sdk 3.63.4 -> 3.65.5" + * } * } * * Example GitHub Actions usage: diff --git a/packages/cli/cli/src/commands/automations/upgrade/__test__/executeAutomationsUpgrade.test.ts b/packages/cli/cli/src/commands/automations/upgrade/__test__/executeAutomationsUpgrade.test.ts index ca1232b4770..7a37190889e 100644 --- a/packages/cli/cli/src/commands/automations/upgrade/__test__/executeAutomationsUpgrade.test.ts +++ b/packages/cli/cli/src/commands/automations/upgrade/__test__/executeAutomationsUpgrade.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from "vitest"; -import { getChangelogUrl } from "../executeAutomationsUpgrade.js"; +import { + buildCommitMessage, + buildPrBody, + buildPrTitle, + getChangelogUrl, + getShortGeneratorName +} from "../executeAutomationsUpgrade.js"; describe("getChangelogUrl", () => { it("derives typescript changelog URL from SDK generator name", () => { @@ -88,10 +94,106 @@ describe("getChangelogUrl", () => { }); }); +describe("getShortGeneratorName", () => { + it("strips fernapi/fern- prefix", () => { + expect(getShortGeneratorName("fernapi/fern-typescript-sdk")).toBe("typescript-sdk"); + }); + + it("strips prefix from variant names", () => { + expect(getShortGeneratorName("fernapi/fern-ruby-sdk-v2")).toBe("ruby-sdk-v2"); + }); + + it("leaves non-fernapi names unchanged", () => { + expect(getShortGeneratorName("custom-org/my-generator")).toBe("custom-org/my-generator"); + }); +}); + +const CLI_UPGRADED = { from: "4.66.0", to: "5.7.3", upgraded: true }; +const CLI_NOT_UPGRADED = { from: "5.7.3", to: "5.7.3", upgraded: false }; + +const GENERATOR_TS = { + name: "fernapi/fern-typescript-sdk", + group: "ts-sdk", + api: "api" as string | null, + from: "3.63.4", + to: "3.65.5", + changelog: "https://buildwithfern.com/learn/sdks/generators/typescript/changelog", + migrationsApplied: 1 +}; + +const GENERATOR_GO = { + name: "fernapi/fern-go-sdk", + group: "go-sdk", + api: null as string | null, + from: "0.28.0", + to: "1.39.0", + changelog: "https://buildwithfern.com/learn/sdks/generators/go/changelog", + migrationsApplied: 0 +}; + +describe("buildPrTitle", () => { + it("includes CLI and generator count", () => { + expect(buildPrTitle({ cli: CLI_UPGRADED, generators: [GENERATOR_TS, GENERATOR_GO] })).toBe( + "chore(fern): upgrade CLI 4.66.0 → 5.7.3 and 2 generators" + ); + }); + + it("singular generator", () => { + expect(buildPrTitle({ cli: CLI_NOT_UPGRADED, generators: [GENERATOR_TS] })).toBe( + "chore(fern): upgrade 1 generator" + ); + }); + + it("CLI only", () => { + expect(buildPrTitle({ cli: CLI_UPGRADED, generators: [] })).toBe("chore(fern): upgrade CLI 4.66.0 → 5.7.3"); + }); +}); + +describe("buildPrBody", () => { + it("includes CLI section and generator table", () => { + const body = buildPrBody({ cli: CLI_UPGRADED, generators: [GENERATOR_TS] }); + expect(body).toContain("## Fern Upgrade"); + expect(body).toContain("### CLI"); + expect(body).toContain("`4.66.0` → `5.7.3`"); + expect(body).toContain("### Generators"); + expect(body).toContain("| fernapi/fern-typescript-sdk | 3.63.4 | 3.65.5 |"); + expect(body).toContain("[View](https://buildwithfern.com/learn/sdks/generators/typescript/changelog)"); + expect(body).toContain("fern-upgrade"); + }); + + it("uses em-dash for missing changelog", () => { + const gen = { ...GENERATOR_TS, changelog: undefined }; + const body = buildPrBody({ cli: CLI_NOT_UPGRADED, generators: [gen] }); + expect(body).toContain("| — |"); + }); + + it("omits CLI section when not upgraded", () => { + const body = buildPrBody({ cli: CLI_NOT_UPGRADED, generators: [GENERATOR_TS] }); + expect(body).not.toContain("### CLI"); + }); + + it("omits generator section when no generators", () => { + const body = buildPrBody({ cli: CLI_UPGRADED, generators: [] }); + expect(body).not.toContain("### Generators"); + }); +}); + +describe("buildCommitMessage", () => { + it("includes cli and short generator names", () => { + expect(buildCommitMessage({ cli: CLI_UPGRADED, generators: [GENERATOR_TS] })).toBe( + "chore: upgrade fern cli 4.66.0 -> 5.7.3, typescript-sdk 3.63.4 -> 3.65.5" + ); + }); + + it("generators only", () => { + expect(buildCommitMessage({ cli: CLI_NOT_UPGRADED, generators: [GENERATOR_TS, GENERATOR_GO] })).toBe( + "chore: upgrade fern typescript-sdk 3.63.4 -> 3.65.5, go-sdk 0.28.0 -> 1.39.0" + ); + }); +}); + describe("AutomationsUpgradeResult schema", () => { - it("documents the expected JSON output shape", () => { - // This test documents the contract between CLI and GHA consumers. - // If the schema changes, this test should be updated alongside the GHA. + it("documents the expected JSON output shape including pr field", () => { const exampleResult = { cli: { from: "4.66.0", to: "4.96.0", upgraded: true }, generators: [ @@ -106,21 +208,24 @@ describe("AutomationsUpgradeResult schema", () => { } ], skippedMajor: [{ name: "fernapi/fern-ruby-sdk-v2", current: "0.3.0", latest: "1.0.0" }], - alreadyUpToDate: [{ name: "fernapi/fern-go-sdk", version: "1.37.0" }] + alreadyUpToDate: [{ name: "fernapi/fern-go-sdk", version: "1.37.0" }], + pr: { + title: "chore(fern): upgrade CLI 4.66.0 → 4.96.0 and 1 generator", + body: "## Fern Upgrade\n...", + commitMessage: "chore: upgrade fern cli 4.66.0 -> 4.96.0, typescript-sdk 3.63.4 -> 3.65.5" + } }; - // Verify all top-level keys exist expect(exampleResult).toHaveProperty("cli"); expect(exampleResult).toHaveProperty("generators"); expect(exampleResult).toHaveProperty("skippedMajor"); expect(exampleResult).toHaveProperty("alreadyUpToDate"); + expect(exampleResult).toHaveProperty("pr"); - // Verify cli shape expect(exampleResult.cli).toHaveProperty("from"); expect(exampleResult.cli).toHaveProperty("to"); expect(exampleResult.cli).toHaveProperty("upgraded"); - // Verify generator entry shape const gen = exampleResult.generators[0]; expect(gen).toBeDefined(); expect(gen).toHaveProperty("name"); @@ -131,18 +236,31 @@ describe("AutomationsUpgradeResult schema", () => { expect(gen).toHaveProperty("changelog"); expect(gen).toHaveProperty("migrationsApplied"); - // Verify skippedMajor entry shape const skip = exampleResult.skippedMajor[0]; expect(skip).toBeDefined(); expect(skip).toHaveProperty("name"); expect(skip).toHaveProperty("current"); expect(skip).toHaveProperty("latest"); - // Verify alreadyUpToDate entry shape const upToDate = exampleResult.alreadyUpToDate[0]; expect(upToDate).toBeDefined(); expect(upToDate).toHaveProperty("name"); expect(upToDate).toHaveProperty("version"); + + expect(exampleResult.pr).toHaveProperty("title"); + expect(exampleResult.pr).toHaveProperty("body"); + expect(exampleResult.pr).toHaveProperty("commitMessage"); + }); + + it("pr is null when nothing changed", () => { + const result = { + cli: { from: "5.7.3", to: "5.7.3", upgraded: false }, + generators: [], + skippedMajor: [], + alreadyUpToDate: [{ name: "fernapi/fern-go-sdk", version: "1.37.0" }], + pr: null + }; + expect(result.pr).toBeNull(); }); it("supports null api field for single-API projects", () => { diff --git a/packages/cli/cli/src/commands/automations/upgrade/executeAutomationsUpgrade.ts b/packages/cli/cli/src/commands/automations/upgrade/executeAutomationsUpgrade.ts index 84848c35f6e..6d51d05f531 100644 --- a/packages/cli/cli/src/commands/automations/upgrade/executeAutomationsUpgrade.ts +++ b/packages/cli/cli/src/commands/automations/upgrade/executeAutomationsUpgrade.ts @@ -61,11 +61,18 @@ interface AlreadyUpToDateEntry { version: string; } +interface PrSuggestion { + title: string; + body: string; + commitMessage: string; +} + export interface AutomationsUpgradeResult { cli: CliUpgradeResult; generators: GeneratorUpgradeEntry[]; skippedMajor: SkippedMajorEntry[]; alreadyUpToDate: AlreadyUpToDateEntry[]; + pr: PrSuggestion | null; } /** @@ -108,7 +115,12 @@ async function getCurrentCliVersion(cliContext: CliContext): Promise { * } * ], * "skippedMajor": [{ "name": "...", "current": "...", "latest": "..." }], - * "alreadyUpToDate": [{ "name": "...", "version": "..." }] + * "alreadyUpToDate": [{ "name": "...", "version": "..." }], + * "pr": { + * "title": "chore(fern): upgrade CLI 4.66.0 → 4.96.0 and 2 generators", + * "body": "## Fern Upgrade\n...", + * "commitMessage": "chore: upgrade fern cli 4.66.0 -> 4.96.0, ..." + * } * } */ export async function executeAutomationsUpgrade({ @@ -155,15 +167,21 @@ export async function executeAutomationsUpgrade({ }); // 4. Build the structured result + const cliResult: CliUpgradeResult = { + from: cliVersionBefore, + to: cliVersionAfter, + upgraded: cliUpgraded + }; + + const hasChanges = cliUpgraded || generatorResults.generators.length > 0; + const pr = hasChanges ? buildPrSuggestion({ cli: cliResult, generators: generatorResults.generators }) : null; + const result: AutomationsUpgradeResult = { - cli: { - from: cliVersionBefore, - to: cliVersionAfter, - upgraded: cliUpgraded - }, + cli: cliResult, generators: generatorResults.generators, skippedMajor: generatorResults.skippedMajor, - alreadyUpToDate: generatorResults.alreadyUpToDate + alreadyUpToDate: generatorResults.alreadyUpToDate, + pr }; return result; @@ -256,3 +274,111 @@ async function upgradeGeneratorsForAllWorkspaces({ return { generators, skippedMajor, alreadyUpToDate }; } + +// --------------------------------------------------------------------------- +// PR formatting helpers +// --------------------------------------------------------------------------- + +/** + * Strips the Docker org prefix from a generator name. + * "fernapi/fern-typescript-sdk" → "typescript-sdk" + */ +export function getShortGeneratorName(name: string): string { + return name.replace(/^fernapi\/fern-/, ""); +} + +/** + * Builds a suggested PR title from the upgrade results. + */ +export function buildPrTitle({ + cli, + generators +}: { + cli: CliUpgradeResult; + generators: readonly GeneratorUpgradeEntry[]; +}): string { + const parts: string[] = []; + + if (cli.upgraded) { + parts.push(`CLI ${cli.from} → ${cli.to}`); + } + + if (generators.length > 0) { + parts.push(`${generators.length} generator${generators.length === 1 ? "" : "s"}`); + } + + return `chore(fern): upgrade ${parts.join(" and ")}`; +} + +/** + * Builds a suggested PR body (markdown) from the upgrade results. + */ +export function buildPrBody({ + cli, + generators +}: { + cli: CliUpgradeResult; + generators: readonly GeneratorUpgradeEntry[]; +}): string { + const sections: string[] = ["## Fern Upgrade\n"]; + + if (cli.upgraded) { + sections.push(`### CLI\n- \`${cli.from}\` → \`${cli.to}\`\n`); + } + + if (generators.length > 0) { + sections.push("### Generators"); + sections.push("| Generator | From | To | Changelog |"); + sections.push("|-----------|------|----|-----------|"); + + for (const g of generators) { + const link = g.changelog != null ? `[View](${g.changelog})` : "—"; + sections.push(`| ${g.name} | ${g.from} | ${g.to} | ${link} |`); + } + sections.push(""); + } + + sections.push( + "---\n🤖 This PR was automatically created by " + + "[fern-upgrade](https://github.com/fern-api/actions/tree/main/upgrade)" + ); + + return sections.join("\n"); +} + +/** + * Builds a one-line commit message summarising all version changes. + */ +export function buildCommitMessage({ + cli, + generators +}: { + cli: CliUpgradeResult; + generators: readonly GeneratorUpgradeEntry[]; +}): string { + const parts: string[] = []; + + if (cli.upgraded) { + parts.push(`cli ${cli.from} -> ${cli.to}`); + } + + for (const g of generators) { + parts.push(`${getShortGeneratorName(g.name)} ${g.from} -> ${g.to}`); + } + + return `chore: upgrade fern ${parts.join(", ")}`; +} + +function buildPrSuggestion({ + cli, + generators +}: { + cli: CliUpgradeResult; + generators: readonly GeneratorUpgradeEntry[]; +}): PrSuggestion { + return { + title: buildPrTitle({ cli, generators }), + body: buildPrBody({ cli, generators }), + commitMessage: buildCommitMessage({ cli, generators }) + }; +}