Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- summary: |
Add `fern automations upgrade` command that wraps `fern upgrade` and
`fern generator upgrade` into a single invocation with structured JSON
output (`--json`). Designed for consumption by the fern-upgrade GitHub
Action.
type: feat
- summary: |
Replace brittle hardcoded changelog URL map in `upgradeGenerator.ts` with
a regex-based derivation that supports all current and future generators.
type: fix
100 changes: 100 additions & 0 deletions packages/cli/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { addGeneratorCommands, addGetOrganizationCommand } from "./cliV2.js";
import { addGeneratorToWorkspaces } from "./commands/add-generator/addGeneratorToWorkspaces.js";
import { executeAutomationsGenerate } from "./commands/automations/generate/executeAutomationsGenerate.js";
import { listPreviewGroups } from "./commands/automations/listPreviewGroups.js";
import { executeAutomationsUpgrade } from "./commands/automations/upgrade/executeAutomationsUpgrade.js";
import { diff } from "./commands/diff/diff.js";
import { previewDocsWorkspace } from "./commands/docs-dev/devDocsWorkspace.js";
import { docsDiff } from "./commands/docs-diff/docsDiff.js";
Expand Down Expand Up @@ -2334,6 +2335,7 @@ function addAutomationsCommand(cli: Argv<GlobalCliOptions>, cliContext: CliConte
cli.command("automations", false, (yargs) => {
addAutomationsGenerateCommand(yargs, cliContext);
addAutomationsPreviewCommand(yargs, cliContext);
addAutomationsUpgradeCommand(yargs, cliContext);
return yargs.demandCommand();
});
}
Expand Down Expand Up @@ -2607,6 +2609,104 @@ function addAutomationsGenerateCommand(cli: Argv<GlobalCliOptions>, cliContext:
);
}

/**
* `fern automations upgrade`
*
* Wraps `fern upgrade` (CLI version) and `fern generator upgrade` (generator
* versions) into a single command that outputs structured JSON to stdout.
*
* Designed for consumption by the fern-upgrade GitHub Action, replacing the
* previous snapshot.js / diff.js approach with a single CLI invocation.
*
* Flags:
* --include-major Include major version bumps for generators (default: false).
* --json Output structured JSON to stdout (for machine consumption).
*
* JSON output format (--json):
* {
* "cli": { "from": "4.66.0", "to": "4.96.0", "upgraded": true },
* "generators": [
* {
* "name": "fernapi/fern-typescript-sdk",
* "group": "ts-sdk",
* "api": "api",
* "from": "3.63.4",
* "to": "3.65.5",
* "changelog": "https://buildwithfern.com/learn/sdks/generators/typescript/changelog",
* "migrationsApplied": 1
* }
* ],
* "skippedMajor": [{ "name": "...", "current": "0.28.0", "latest": "1.37.0" }],
* "alreadyUpToDate": [{ "name": "...", "version": "3.65.5" }]
* }
*
* Example GitHub Actions usage:
* - run: |
* UPGRADE_JSON=$(fern automations upgrade --json --include-major)
* echo "upgrade-json=$UPGRADE_JSON" >> "$GITHUB_OUTPUT"
* env:
* FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
*/
function addAutomationsUpgradeCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
cli.command(
"upgrade",
false, // hidden
(yargs) =>
yargs
.option("include-major", {
boolean: true,
default: true,
description:
"Whether to include major version upgrades for generators. " +
"When true (default), all upgrades including major versions are applied."
})
.option("json", {
boolean: true,
default: false,
description: "Output results as JSON to stdout (for machine consumption)."
}),
async (argv) => {
cliContext.instrumentPostHogEvent({
command: "fern automations upgrade"
});

const result = await executeAutomationsUpgrade({
cliContext,
options: {
includeMajor: argv["include-major"]
}
});

if (argv.json) {
cliContext.writeJsonToStdout(result);
} else {
// Human-readable summary
const { cli: cliResult, generators, skippedMajor, alreadyUpToDate } = result;
if (cliResult.upgraded) {
cliContext.logger.info(`CLI: ${cliResult.from} -> ${cliResult.to}`);
} else {
cliContext.logger.info(`CLI: ${cliResult.from} (already up to date)`);
}
if (generators.length > 0) {
cliContext.logger.info(`Generators upgraded: ${generators.length}`);
for (const gen of generators) {
cliContext.logger.info(` ${gen.name}: ${gen.from} -> ${gen.to}`);
}
}
if (alreadyUpToDate.length > 0) {
cliContext.logger.info(`Generators already up to date: ${alreadyUpToDate.length}`);
}
if (skippedMajor.length > 0) {
cliContext.logger.info(`Major upgrades available (skipped): ${skippedMajor.length}`);
for (const skip of skippedMajor) {
cliContext.logger.info(` ${skip.name}: ${skip.current} -> ${skip.latest}`);
}
}
}
}
);
}

function addSdkPreviewCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContext) {
cli.command(
"preview",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { describe, expect, it } from "vitest";

import { getChangelogUrl } from "../executeAutomationsUpgrade.js";

describe("getChangelogUrl", () => {
it("derives typescript changelog URL from SDK generator name", () => {
expect(getChangelogUrl("fernapi/fern-typescript-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/typescript/changelog"
);
});

it("derives typescript changelog URL from node SDK variant", () => {
expect(getChangelogUrl("fernapi/fern-typescript-node-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/typescript/changelog"
);
});

it("derives python changelog URL", () => {
expect(getChangelogUrl("fernapi/fern-python-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/python/changelog"
);
});

it("derives go changelog URL", () => {
expect(getChangelogUrl("fernapi/fern-go-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/go/changelog"
);
});

it("derives java changelog URL", () => {
expect(getChangelogUrl("fernapi/fern-java-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/java/changelog"
);
});

it("derives csharp changelog URL", () => {
expect(getChangelogUrl("fernapi/fern-csharp-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/csharp/changelog"
);
});

it("derives ruby changelog URL from v2 variant", () => {
expect(getChangelogUrl("fernapi/fern-ruby-sdk-v2")).toBe(
"https://buildwithfern.com/learn/sdks/generators/ruby/changelog"
);
});

it("derives php changelog URL", () => {
expect(getChangelogUrl("fernapi/fern-php-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/php/changelog"
);
});

it("derives swift changelog URL", () => {
expect(getChangelogUrl("fernapi/fern-swift-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/swift/changelog"
);
});

it("derives rust changelog URL", () => {
expect(getChangelogUrl("fernapi/fern-rust-sdk")).toBe(
"https://buildwithfern.com/learn/sdks/generators/rust/changelog"
);
});

it("returns undefined for unrecognized generator names", () => {
expect(getChangelogUrl("some-other-generator")).toBeUndefined();
});

it("returns undefined for empty string", () => {
expect(getChangelogUrl("")).toBeUndefined();
});

it("returns undefined for generator without fernapi prefix", () => {
expect(getChangelogUrl("custom-org/fern-typescript-sdk")).toBeUndefined();
});

it("handles model generators (non-SDK)", () => {
expect(getChangelogUrl("fernapi/fern-java-model")).toBe(
"https://buildwithfern.com/learn/sdks/generators/java/changelog"
);
});

it("handles server generators", () => {
expect(getChangelogUrl("fernapi/fern-python-server")).toBe(
"https://buildwithfern.com/learn/sdks/generators/python/changelog"
);
});
});

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.
const exampleResult = {
cli: { from: "4.66.0", to: "4.96.0", upgraded: true },
generators: [
{
name: "fernapi/fern-typescript-sdk",
group: "ts-sdk",
api: "api",
from: "3.63.4",
to: "3.65.5",
changelog: "https://buildwithfern.com/learn/sdks/generators/typescript/changelog",
migrationsApplied: 1
}
],
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" }]
};

// Verify all top-level keys exist
expect(exampleResult).toHaveProperty("cli");
expect(exampleResult).toHaveProperty("generators");
expect(exampleResult).toHaveProperty("skippedMajor");
expect(exampleResult).toHaveProperty("alreadyUpToDate");

// 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");
expect(gen).toHaveProperty("group");
expect(gen).toHaveProperty("api");
expect(gen).toHaveProperty("from");
expect(gen).toHaveProperty("to");
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");
});

it("supports null api field for single-API projects", () => {
const entry = {
name: "fernapi/fern-typescript-sdk",
group: "ts-sdk",
api: null,
from: "3.63.4",
to: "3.65.5",
changelog: "https://buildwithfern.com/learn/sdks/generators/typescript/changelog",
migrationsApplied: 0
};
expect(entry.api).toBeNull();
});
});
Loading