@fern-api/generator-cli 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -549,6 +549,16 @@ interface GithubStepConfig {
549
549
  previousGenerationSha: string;
550
550
  currentGenerationSha: string;
551
551
  };
552
+ /** When true: separate PRs per generation, no-diff skip, automerge support, run_id in body */
553
+ automationMode?: boolean;
554
+ /** Enable GitHub automerge on the PR (only effective when automationMode && !hasBreakingChanges) */
555
+ autoMerge?: boolean;
556
+ /** Pre-computed: version bump is MAJOR (from --version AUTO AI analysis) */
557
+ hasBreakingChanges?: boolean;
558
+ /** Human-readable breaking changes summary for PR body (from autoVersioningPrDescription on MAJOR) */
559
+ breakingChangesSummary?: string;
560
+ /** FERN_RUN_ID for cross-repo correlation (set by GitHub Action) */
561
+ runId?: string;
552
562
  }
553
563
  interface PipelineResult {
554
564
  success: boolean;
@@ -593,6 +603,10 @@ interface GithubStepResult extends StepResult {
593
603
  prNumber?: number;
594
604
  updatedExistingPr?: boolean;
595
605
  generationBaseTagSha?: string;
606
+ /** True when generation produced no changes vs base branch — PR skipped */
607
+ skippedNoDiff?: boolean;
608
+ /** True when automerge was enabled on the PR */
609
+ autoMergeEnabled?: boolean;
596
610
  }
597
611
  interface StepResult {
598
612
  executed: boolean;
package/dist/api.js CHANGED
@@ -6022,6 +6022,24 @@ var ClonedRepository = class _ClonedRepository {
6022
6022
  const result = await this.git.raw(["rev-parse", `${commitSha}^{tree}`]);
6023
6023
  return result.trim();
6024
6024
  }
6025
+ /**
6026
+ * Returns true if the working tree has uncommitted changes (staged or unstaged).
6027
+ */
6028
+ async hasChanges() {
6029
+ await this.git.cwd(this.clonePath);
6030
+ const status2 = await this.git.status();
6031
+ return !status2.isClean();
6032
+ }
6033
+ /**
6034
+ * Compares the tree hash of HEAD against a ref (e.g. `origin/main`).
6035
+ * Returns true if the trees are identical (no content difference).
6036
+ * Useful for detecting no-diff after committing — if trees match, the commit is a no-op.
6037
+ */
6038
+ async treeHashEquals(ref) {
6039
+ const headTree = await this.getHeadTreeHash();
6040
+ const refTree = await this.getCommitTreeHash(ref);
6041
+ return headTree === refTree;
6042
+ }
6025
6043
  // This tag is a moving pointer (fern-generation-base--<generator>) that tracks the latest
6026
6044
  // clean generation SHA. --force is required because: (1) the tag is intentionally overwritten
6027
6045
  // each generation, and (2) --force-with-lease doesn't work for tags since git tag -f overwrites
@@ -10311,31 +10329,32 @@ var GithubStep = class extends BaseStep {
10311
10329
  const baseBranch = this.config.branch ?? await repository.getDefaultBranch();
10312
10330
  const octokit = new import_rest2.Octokit({ auth: this.config.token });
10313
10331
  const { owner, repo } = parseRepository(this.config.uri);
10314
- const existingPR = await findExistingUpdatablePR(octokit, owner, repo, baseBranch, this.logger);
10315
10332
  let prBranch;
10316
10333
  let isUpdatingExistingPR = false;
10317
10334
  let generationBaseSha;
10335
+ let existingPR;
10336
+ if (!this.config.automationMode) {
10337
+ existingPR = await findExistingUpdatablePR(octokit, owner, repo, baseBranch, this.logger);
10338
+ }
10318
10339
  if (existingPR != null) {
10319
10340
  this.logger.info(
10320
10341
  `Found existing updatable PR #${existingPR.number}, will update branch ${existingPR.headBranch}`
10321
10342
  );
10322
10343
  prBranch = existingPR.headBranch;
10323
10344
  isUpdatingExistingPR = true;
10324
- if (skipCommit) {
10325
- generationBaseSha = await createReplayBranch(
10326
- repository,
10327
- prBranch,
10328
- this.config.commitMessage,
10329
- replayConflictInfo,
10330
- this.logger
10331
- );
10332
- } else {
10333
- await repository.checkoutRemoteBranch(prBranch);
10334
- }
10335
10345
  } else {
10336
- this.logger.debug(`No existing updatable PR found, creating new branch ${newPrBranch}`);
10346
+ this.logger.debug(
10347
+ this.config.automationMode ? `Automation mode: creating new branch ${newPrBranch}` : `No existing updatable PR found, creating new branch ${newPrBranch}`
10348
+ );
10337
10349
  prBranch = newPrBranch;
10338
- if (skipCommit) {
10350
+ }
10351
+ const branchAction = resolveBranchAction({
10352
+ automationMode: this.config.automationMode === true,
10353
+ skipCommit,
10354
+ existingPR
10355
+ });
10356
+ switch (branchAction) {
10357
+ case "replay-branch":
10339
10358
  generationBaseSha = await createReplayBranch(
10340
10359
  repository,
10341
10360
  prBranch,
@@ -10343,8 +10362,16 @@ var GithubStep = class extends BaseStep {
10343
10362
  replayConflictInfo,
10344
10363
  this.logger
10345
10364
  );
10346
- } else {
10347
- await repository.checkout(prBranch);
10365
+ break;
10366
+ case "create-from-head":
10367
+ await repository.createBranchFromHead(prBranch);
10368
+ break;
10369
+ case "checkout-remote":
10370
+ await repository.checkoutRemoteBranch(prBranch);
10371
+ break;
10372
+ default: {
10373
+ const _exhaustive = branchAction;
10374
+ throw new Error(`Unexpected branch action: ${String(_exhaustive)}`);
10348
10375
  }
10349
10376
  }
10350
10377
  if (!skipCommit) {
@@ -10354,6 +10381,13 @@ var GithubStep = class extends BaseStep {
10354
10381
  await repository.commitAllChanges(finalCommitMessage2);
10355
10382
  this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
10356
10383
  }
10384
+ if (this.config.automationMode) {
10385
+ const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
10386
+ if (noDiff) {
10387
+ this.logger.info("No changes detected after generation \u2014 skipping PR creation");
10388
+ return { executed: true, success: true, skippedNoDiff: true };
10389
+ }
10390
+ }
10357
10391
  const result = {
10358
10392
  executed: true,
10359
10393
  success: true,
@@ -10388,7 +10422,8 @@ var GithubStep = class extends BaseStep {
10388
10422
  this.config.versionBumpReason
10389
10423
  );
10390
10424
  const replaySection = formatReplayPrBody(replayResult, { branchName: prBranch, repoUri: this.config.uri });
10391
- const enrichedBody = replaySection != null ? prBody + "\n\n---\n\n" + replaySection : prBody;
10425
+ let enrichedBody = replaySection != null ? prBody + "\n\n---\n\n" + replaySection : prBody;
10426
+ enrichedBody = enrichPrBodyForAutomation(enrichedBody, this.config);
10392
10427
  if (isUpdatingExistingPR && existingPR != null) {
10393
10428
  this.logger.info(`Updated existing pull request: ${existingPR.htmlUrl}`);
10394
10429
  result.prUrl = existingPR.htmlUrl;
@@ -10419,6 +10454,23 @@ var GithubStep = class extends BaseStep {
10419
10454
  this.logger.info(`Created pull request: ${pullRequest.html_url}`);
10420
10455
  result.prUrl = pullRequest.html_url;
10421
10456
  result.prNumber = pullRequest.number;
10457
+ if (shouldEnableAutomerge(this.config) && pullRequest.node_id) {
10458
+ try {
10459
+ await octokit.graphql(
10460
+ `mutation($pullRequestId: ID!) {
10461
+ enablePullRequestAutoMerge(input: {
10462
+ pullRequestId: $pullRequestId,
10463
+ mergeMethod: SQUASH
10464
+ }) { clientMutationId }
10465
+ }`,
10466
+ { pullRequestId: pullRequest.node_id }
10467
+ );
10468
+ this.logger.info(`Enabled automerge on PR #${pullRequest.number}`);
10469
+ result.autoMergeEnabled = true;
10470
+ } catch (autoMergeError) {
10471
+ this.logger.warn(`Could not enable automerge: ${extractErrorMessage(autoMergeError)}`);
10472
+ }
10473
+ }
10422
10474
  } catch (error) {
10423
10475
  const message = extractErrorMessage(error);
10424
10476
  if (message.includes("A pull request already exists for")) {
@@ -10431,6 +10483,7 @@ var GithubStep = class extends BaseStep {
10431
10483
  return result;
10432
10484
  }
10433
10485
  async executePushMode(repository) {
10486
+ const baseBranch = this.config.branch ?? await repository.getDefaultBranch();
10434
10487
  if (this.config.branch != null) {
10435
10488
  this.logger.debug(`Checking out branch ${this.config.branch}`);
10436
10489
  await repository.checkout(this.config.branch);
@@ -10440,6 +10493,13 @@ var GithubStep = class extends BaseStep {
10440
10493
  const finalCommitMessage = this.config.commitMessage ?? "SDK Generation";
10441
10494
  await repository.commitAllChanges(finalCommitMessage);
10442
10495
  this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
10496
+ if (this.config.automationMode) {
10497
+ const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
10498
+ if (noDiff) {
10499
+ this.logger.info("No changes detected after generation \u2014 skipping push");
10500
+ return { executed: true, success: true, skippedNoDiff: true };
10501
+ }
10502
+ }
10443
10503
  const result = {
10444
10504
  executed: true,
10445
10505
  success: true
@@ -10484,6 +10544,42 @@ var GithubStep = class extends BaseStep {
10484
10544
  return void 0;
10485
10545
  }
10486
10546
  };
10547
+ function enrichPrBodyForAutomation(body, config) {
10548
+ if (!config.automationMode) {
10549
+ return body;
10550
+ }
10551
+ let result = body;
10552
+ if (config.hasBreakingChanges && config.breakingChangesSummary) {
10553
+ result += "\n\n---\n\n## \u26A0\uFE0F Breaking Changes\n\nThis release contains breaking changes that require manual review:\n\n" + config.breakingChangesSummary;
10554
+ }
10555
+ if (config.runId) {
10556
+ result += `
10557
+
10558
+ ---
10559
+
10560
+ _Fern Run ID: \`${config.runId}\`_`;
10561
+ }
10562
+ return result;
10563
+ }
10564
+ function shouldEnableAutomerge(config) {
10565
+ return config.automationMode === true && config.autoMerge === true && config.hasBreakingChanges !== true;
10566
+ }
10567
+ function resolveBranchAction({
10568
+ automationMode,
10569
+ skipCommit,
10570
+ existingPR
10571
+ }) {
10572
+ if (skipCommit) {
10573
+ return "replay-branch";
10574
+ }
10575
+ if (automationMode) {
10576
+ return "create-from-head";
10577
+ }
10578
+ if (existingPR != null) {
10579
+ return "checkout-remote";
10580
+ }
10581
+ return "create-from-head";
10582
+ }
10487
10583
 
10488
10584
  // src/replay/replay-run.ts
10489
10585
  var import_replay = require("@fern-api/replay");
package/dist/cli.js CHANGED
@@ -20098,7 +20098,7 @@ var init_diff3 = __esm({
20098
20098
  }
20099
20099
  });
20100
20100
 
20101
- // ../../node_modules/.pnpm/@fern-api+replay@0.10.2/node_modules/@fern-api/replay/dist/index.js
20101
+ // ../../node_modules/.pnpm/@fern-api+replay@0.10.3/node_modules/@fern-api/replay/dist/index.js
20102
20102
  var dist_exports = {};
20103
20103
  __export(dist_exports, {
20104
20104
  FERN_BOT_EMAIL: () => FERN_BOT_EMAIL2,
@@ -21063,7 +21063,7 @@ function status(outputDir) {
21063
21063
  }
21064
21064
  var import_fs6, import_path8, import_yaml, import_crypto, import_promises4, import_os, import_path9, import_fs7, import_path10, import_crypto2, import_fs8, import_path11, import_yaml2, import_crypto3, import_fs9, import_path12, import_fs10, __defProp3, __getOwnPropNames3, __esm3, __export3, GitClient_exports, GitClient, init_GitClient, HybridReconstruction_exports, init_HybridReconstruction, FERN_BOT_NAME2, FERN_BOT_EMAIL2, FERN_BOT_LOGIN2, FERN_SUPPORT_NAMES, LOCKFILE_HEADER, LockfileNotFoundError, LockfileManager, INFRASTRUCTURE_FILES, ReplayDetector, CONFLICT_OPENER, CONFLICT_SEPARATOR, CONFLICT_CLOSER, BINARY_EXTENSIONS, ReplayApplicator, ReplayCommitter, ReplayService, FernignoreMigrator, REPLAY_FERNIGNORE_ENTRIES, GITATTRIBUTES_ENTRIES, EMPTY_RESULT;
21065
21065
  var init_dist3 = __esm({
21066
- "../../node_modules/.pnpm/@fern-api+replay@0.10.2/node_modules/@fern-api/replay/dist/index.js"() {
21066
+ "../../node_modules/.pnpm/@fern-api+replay@0.10.3/node_modules/@fern-api/replay/dist/index.js"() {
21067
21067
  "use strict";
21068
21068
  init_esm4();
21069
21069
  import_fs6 = require("fs");
@@ -25063,6 +25063,24 @@ var ClonedRepository = class _ClonedRepository {
25063
25063
  const result = await this.git.raw(["rev-parse", `${commitSha}^{tree}`]);
25064
25064
  return result.trim();
25065
25065
  }
25066
+ /**
25067
+ * Returns true if the working tree has uncommitted changes (staged or unstaged).
25068
+ */
25069
+ async hasChanges() {
25070
+ await this.git.cwd(this.clonePath);
25071
+ const status2 = await this.git.status();
25072
+ return !status2.isClean();
25073
+ }
25074
+ /**
25075
+ * Compares the tree hash of HEAD against a ref (e.g. `origin/main`).
25076
+ * Returns true if the trees are identical (no content difference).
25077
+ * Useful for detecting no-diff after committing — if trees match, the commit is a no-op.
25078
+ */
25079
+ async treeHashEquals(ref) {
25080
+ const headTree = await this.getHeadTreeHash();
25081
+ const refTree = await this.getCommitTreeHash(ref);
25082
+ return headTree === refTree;
25083
+ }
25066
25084
  // This tag is a moving pointer (fern-generation-base--<generator>) that tracks the latest
25067
25085
  // clean generation SHA. --force is required because: (1) the tag is intentionally overwritten
25068
25086
  // each generation, and (2) --force-with-lease doesn't work for tags since git tag -f overwrites
@@ -33135,31 +33153,32 @@ var GithubStep = class extends BaseStep {
33135
33153
  const baseBranch = this.config.branch ?? await repository.getDefaultBranch();
33136
33154
  const octokit = new Octokit2({ auth: this.config.token });
33137
33155
  const { owner, repo } = parseRepository(this.config.uri);
33138
- const existingPR = await findExistingUpdatablePR(octokit, owner, repo, baseBranch, this.logger);
33139
33156
  let prBranch;
33140
33157
  let isUpdatingExistingPR = false;
33141
33158
  let generationBaseSha;
33159
+ let existingPR;
33160
+ if (!this.config.automationMode) {
33161
+ existingPR = await findExistingUpdatablePR(octokit, owner, repo, baseBranch, this.logger);
33162
+ }
33142
33163
  if (existingPR != null) {
33143
33164
  this.logger.info(
33144
33165
  `Found existing updatable PR #${existingPR.number}, will update branch ${existingPR.headBranch}`
33145
33166
  );
33146
33167
  prBranch = existingPR.headBranch;
33147
33168
  isUpdatingExistingPR = true;
33148
- if (skipCommit) {
33149
- generationBaseSha = await createReplayBranch(
33150
- repository,
33151
- prBranch,
33152
- this.config.commitMessage,
33153
- replayConflictInfo,
33154
- this.logger
33155
- );
33156
- } else {
33157
- await repository.checkoutRemoteBranch(prBranch);
33158
- }
33159
33169
  } else {
33160
- this.logger.debug(`No existing updatable PR found, creating new branch ${newPrBranch}`);
33170
+ this.logger.debug(
33171
+ this.config.automationMode ? `Automation mode: creating new branch ${newPrBranch}` : `No existing updatable PR found, creating new branch ${newPrBranch}`
33172
+ );
33161
33173
  prBranch = newPrBranch;
33162
- if (skipCommit) {
33174
+ }
33175
+ const branchAction = resolveBranchAction({
33176
+ automationMode: this.config.automationMode === true,
33177
+ skipCommit,
33178
+ existingPR
33179
+ });
33180
+ switch (branchAction) {
33181
+ case "replay-branch":
33163
33182
  generationBaseSha = await createReplayBranch(
33164
33183
  repository,
33165
33184
  prBranch,
@@ -33167,8 +33186,16 @@ var GithubStep = class extends BaseStep {
33167
33186
  replayConflictInfo,
33168
33187
  this.logger
33169
33188
  );
33170
- } else {
33171
- await repository.checkout(prBranch);
33189
+ break;
33190
+ case "create-from-head":
33191
+ await repository.createBranchFromHead(prBranch);
33192
+ break;
33193
+ case "checkout-remote":
33194
+ await repository.checkoutRemoteBranch(prBranch);
33195
+ break;
33196
+ default: {
33197
+ const _exhaustive = branchAction;
33198
+ throw new Error(`Unexpected branch action: ${String(_exhaustive)}`);
33172
33199
  }
33173
33200
  }
33174
33201
  if (!skipCommit) {
@@ -33178,6 +33205,13 @@ var GithubStep = class extends BaseStep {
33178
33205
  await repository.commitAllChanges(finalCommitMessage2);
33179
33206
  this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
33180
33207
  }
33208
+ if (this.config.automationMode) {
33209
+ const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
33210
+ if (noDiff) {
33211
+ this.logger.info("No changes detected after generation \u2014 skipping PR creation");
33212
+ return { executed: true, success: true, skippedNoDiff: true };
33213
+ }
33214
+ }
33181
33215
  const result = {
33182
33216
  executed: true,
33183
33217
  success: true,
@@ -33212,7 +33246,8 @@ var GithubStep = class extends BaseStep {
33212
33246
  this.config.versionBumpReason
33213
33247
  );
33214
33248
  const replaySection = formatReplayPrBody(replayResult, { branchName: prBranch, repoUri: this.config.uri });
33215
- const enrichedBody = replaySection != null ? prBody + "\n\n---\n\n" + replaySection : prBody;
33249
+ let enrichedBody = replaySection != null ? prBody + "\n\n---\n\n" + replaySection : prBody;
33250
+ enrichedBody = enrichPrBodyForAutomation(enrichedBody, this.config);
33216
33251
  if (isUpdatingExistingPR && existingPR != null) {
33217
33252
  this.logger.info(`Updated existing pull request: ${existingPR.htmlUrl}`);
33218
33253
  result.prUrl = existingPR.htmlUrl;
@@ -33243,6 +33278,23 @@ var GithubStep = class extends BaseStep {
33243
33278
  this.logger.info(`Created pull request: ${pullRequest.html_url}`);
33244
33279
  result.prUrl = pullRequest.html_url;
33245
33280
  result.prNumber = pullRequest.number;
33281
+ if (shouldEnableAutomerge(this.config) && pullRequest.node_id) {
33282
+ try {
33283
+ await octokit.graphql(
33284
+ `mutation($pullRequestId: ID!) {
33285
+ enablePullRequestAutoMerge(input: {
33286
+ pullRequestId: $pullRequestId,
33287
+ mergeMethod: SQUASH
33288
+ }) { clientMutationId }
33289
+ }`,
33290
+ { pullRequestId: pullRequest.node_id }
33291
+ );
33292
+ this.logger.info(`Enabled automerge on PR #${pullRequest.number}`);
33293
+ result.autoMergeEnabled = true;
33294
+ } catch (autoMergeError) {
33295
+ this.logger.warn(`Could not enable automerge: ${extractErrorMessage(autoMergeError)}`);
33296
+ }
33297
+ }
33246
33298
  } catch (error) {
33247
33299
  const message = extractErrorMessage(error);
33248
33300
  if (message.includes("A pull request already exists for")) {
@@ -33255,6 +33307,7 @@ var GithubStep = class extends BaseStep {
33255
33307
  return result;
33256
33308
  }
33257
33309
  async executePushMode(repository) {
33310
+ const baseBranch = this.config.branch ?? await repository.getDefaultBranch();
33258
33311
  if (this.config.branch != null) {
33259
33312
  this.logger.debug(`Checking out branch ${this.config.branch}`);
33260
33313
  await repository.checkout(this.config.branch);
@@ -33264,6 +33317,13 @@ var GithubStep = class extends BaseStep {
33264
33317
  const finalCommitMessage = this.config.commitMessage ?? "SDK Generation";
33265
33318
  await repository.commitAllChanges(finalCommitMessage);
33266
33319
  this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
33320
+ if (this.config.automationMode) {
33321
+ const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
33322
+ if (noDiff) {
33323
+ this.logger.info("No changes detected after generation \u2014 skipping push");
33324
+ return { executed: true, success: true, skippedNoDiff: true };
33325
+ }
33326
+ }
33267
33327
  const result = {
33268
33328
  executed: true,
33269
33329
  success: true
@@ -33308,6 +33368,42 @@ var GithubStep = class extends BaseStep {
33308
33368
  return void 0;
33309
33369
  }
33310
33370
  };
33371
+ function enrichPrBodyForAutomation(body, config) {
33372
+ if (!config.automationMode) {
33373
+ return body;
33374
+ }
33375
+ let result = body;
33376
+ if (config.hasBreakingChanges && config.breakingChangesSummary) {
33377
+ result += "\n\n---\n\n## \u26A0\uFE0F Breaking Changes\n\nThis release contains breaking changes that require manual review:\n\n" + config.breakingChangesSummary;
33378
+ }
33379
+ if (config.runId) {
33380
+ result += `
33381
+
33382
+ ---
33383
+
33384
+ _Fern Run ID: \`${config.runId}\`_`;
33385
+ }
33386
+ return result;
33387
+ }
33388
+ function shouldEnableAutomerge(config) {
33389
+ return config.automationMode === true && config.autoMerge === true && config.hasBreakingChanges !== true;
33390
+ }
33391
+ function resolveBranchAction({
33392
+ automationMode,
33393
+ skipCommit,
33394
+ existingPR
33395
+ }) {
33396
+ if (skipCommit) {
33397
+ return "replay-branch";
33398
+ }
33399
+ if (automationMode) {
33400
+ return "create-from-head";
33401
+ }
33402
+ if (existingPR != null) {
33403
+ return "checkout-remote";
33404
+ }
33405
+ return "create-from-head";
33406
+ }
33311
33407
 
33312
33408
  // src/replay/replay-run.ts
33313
33409
  init_dist3();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fern-api/generator-cli",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fern-api/fern.git",
@@ -22,7 +22,7 @@
22
22
  "**"
23
23
  ],
24
24
  "dependencies": {
25
- "@fern-api/replay": "0.10.2",
25
+ "@fern-api/replay": "0.10.3",
26
26
  "@octokit/rest": "^22.0.1",
27
27
  "es-toolkit": "^1.45.1",
28
28
  "tmp-promise": "^3.0.3"