@fern-api/generator-cli 0.9.5 → 0.9.7
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 +14 -0
- package/dist/api.js +113 -17
- package/dist/cli.js +168 -22
- package/package.json +2 -2
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(
|
|
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
|
-
|
|
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
|
-
|
|
10347
|
-
|
|
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
|
-
|
|
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.
|
|
20101
|
+
// ../../node_modules/.pnpm/@fern-api+replay@0.10.4/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.
|
|
21066
|
+
"../../node_modules/.pnpm/@fern-api+replay@0.10.4/node_modules/@fern-api/replay/dist/index.js"() {
|
|
21067
21067
|
"use strict";
|
|
21068
21068
|
init_esm4();
|
|
21069
21069
|
import_fs6 = require("fs");
|
|
@@ -21427,6 +21427,7 @@ var init_dist3 = __esm({
|
|
|
21427
21427
|
}
|
|
21428
21428
|
const log = await this.git.exec([
|
|
21429
21429
|
"log",
|
|
21430
|
+
"--first-parent",
|
|
21430
21431
|
"--format=%H%x00%an%x00%ae%x00%s",
|
|
21431
21432
|
`${lastGen.commit_sha}..HEAD`,
|
|
21432
21433
|
"--",
|
|
@@ -21442,11 +21443,20 @@ var init_dist3 = __esm({
|
|
|
21442
21443
|
if (isGenerationCommit(commit)) {
|
|
21443
21444
|
continue;
|
|
21444
21445
|
}
|
|
21445
|
-
|
|
21446
|
-
if (parents.length > 1) {
|
|
21446
|
+
if (lock.patches.find((p) => p.original_commit === commit.sha)) {
|
|
21447
21447
|
continue;
|
|
21448
21448
|
}
|
|
21449
|
-
|
|
21449
|
+
const parents = await this.git.getCommitParents(commit.sha);
|
|
21450
|
+
if (parents.length > 1) {
|
|
21451
|
+
const compositePatch = await this.createMergeCompositePatch({
|
|
21452
|
+
mergeCommit: commit,
|
|
21453
|
+
firstParent: parents[0],
|
|
21454
|
+
lastGen,
|
|
21455
|
+
lock
|
|
21456
|
+
});
|
|
21457
|
+
if (compositePatch) {
|
|
21458
|
+
newPatches.push(compositePatch);
|
|
21459
|
+
}
|
|
21450
21460
|
continue;
|
|
21451
21461
|
}
|
|
21452
21462
|
let patchContent;
|
|
@@ -21545,6 +21555,46 @@ var init_dist3 = __esm({
|
|
|
21545
21555
|
const normalized = patchContent.split("\n").filter((line) => !line.startsWith("From ") && !line.startsWith("index ") && !line.startsWith("Date: ")).join("\n");
|
|
21546
21556
|
return `sha256:${(0, import_crypto.createHash)("sha256").update(normalized).digest("hex")}`;
|
|
21547
21557
|
}
|
|
21558
|
+
/**
|
|
21559
|
+
* Create a single composite patch for a merge commit by diffing the merge
|
|
21560
|
+
* commit against its first parent. This captures the net change of the
|
|
21561
|
+
* entire merged branch as one patch, eliminating accumulator chaining and
|
|
21562
|
+
* zero-net-change ghost conflicts.
|
|
21563
|
+
*/
|
|
21564
|
+
async createMergeCompositePatch(input) {
|
|
21565
|
+
const { mergeCommit, firstParent, lastGen, lock } = input;
|
|
21566
|
+
const forgottenHashes = new Set(lock.forgotten_hashes ?? []);
|
|
21567
|
+
const filesOutput = await this.git.exec([
|
|
21568
|
+
"diff",
|
|
21569
|
+
"--name-only",
|
|
21570
|
+
firstParent,
|
|
21571
|
+
mergeCommit.sha
|
|
21572
|
+
]);
|
|
21573
|
+
const files = filesOutput.trim().split("\n").filter(Boolean).filter((f2) => !INFRASTRUCTURE_FILES.has(f2)).filter((f2) => !f2.startsWith(".fern/"));
|
|
21574
|
+
if (files.length === 0) return null;
|
|
21575
|
+
const diff = await this.git.exec([
|
|
21576
|
+
"diff",
|
|
21577
|
+
firstParent,
|
|
21578
|
+
mergeCommit.sha,
|
|
21579
|
+
"--",
|
|
21580
|
+
...files
|
|
21581
|
+
]);
|
|
21582
|
+
if (!diff.trim()) return null;
|
|
21583
|
+
const contentHash = this.computeContentHash(diff);
|
|
21584
|
+
if (lock.patches.some((p) => p.content_hash === contentHash) || forgottenHashes.has(contentHash)) {
|
|
21585
|
+
return null;
|
|
21586
|
+
}
|
|
21587
|
+
return {
|
|
21588
|
+
id: `patch-merge-${mergeCommit.sha.slice(0, 8)}`,
|
|
21589
|
+
content_hash: contentHash,
|
|
21590
|
+
original_commit: mergeCommit.sha,
|
|
21591
|
+
original_message: mergeCommit.message,
|
|
21592
|
+
original_author: `${mergeCommit.authorName} <${mergeCommit.authorEmail}>`,
|
|
21593
|
+
base_generation: lastGen.commit_sha,
|
|
21594
|
+
files,
|
|
21595
|
+
patch_content: diff
|
|
21596
|
+
};
|
|
21597
|
+
}
|
|
21548
21598
|
/**
|
|
21549
21599
|
* Detect patches via tree diff for non-linear history. Returns a composite patch.
|
|
21550
21600
|
* Revert reconciliation is skipped here because tree-diff produces a single composite
|
|
@@ -25063,6 +25113,24 @@ var ClonedRepository = class _ClonedRepository {
|
|
|
25063
25113
|
const result = await this.git.raw(["rev-parse", `${commitSha}^{tree}`]);
|
|
25064
25114
|
return result.trim();
|
|
25065
25115
|
}
|
|
25116
|
+
/**
|
|
25117
|
+
* Returns true if the working tree has uncommitted changes (staged or unstaged).
|
|
25118
|
+
*/
|
|
25119
|
+
async hasChanges() {
|
|
25120
|
+
await this.git.cwd(this.clonePath);
|
|
25121
|
+
const status2 = await this.git.status();
|
|
25122
|
+
return !status2.isClean();
|
|
25123
|
+
}
|
|
25124
|
+
/**
|
|
25125
|
+
* Compares the tree hash of HEAD against a ref (e.g. `origin/main`).
|
|
25126
|
+
* Returns true if the trees are identical (no content difference).
|
|
25127
|
+
* Useful for detecting no-diff after committing — if trees match, the commit is a no-op.
|
|
25128
|
+
*/
|
|
25129
|
+
async treeHashEquals(ref) {
|
|
25130
|
+
const headTree = await this.getHeadTreeHash();
|
|
25131
|
+
const refTree = await this.getCommitTreeHash(ref);
|
|
25132
|
+
return headTree === refTree;
|
|
25133
|
+
}
|
|
25066
25134
|
// This tag is a moving pointer (fern-generation-base--<generator>) that tracks the latest
|
|
25067
25135
|
// clean generation SHA. --force is required because: (1) the tag is intentionally overwritten
|
|
25068
25136
|
// each generation, and (2) --force-with-lease doesn't work for tags since git tag -f overwrites
|
|
@@ -33135,31 +33203,32 @@ var GithubStep = class extends BaseStep {
|
|
|
33135
33203
|
const baseBranch = this.config.branch ?? await repository.getDefaultBranch();
|
|
33136
33204
|
const octokit = new Octokit2({ auth: this.config.token });
|
|
33137
33205
|
const { owner, repo } = parseRepository(this.config.uri);
|
|
33138
|
-
const existingPR = await findExistingUpdatablePR(octokit, owner, repo, baseBranch, this.logger);
|
|
33139
33206
|
let prBranch;
|
|
33140
33207
|
let isUpdatingExistingPR = false;
|
|
33141
33208
|
let generationBaseSha;
|
|
33209
|
+
let existingPR;
|
|
33210
|
+
if (!this.config.automationMode) {
|
|
33211
|
+
existingPR = await findExistingUpdatablePR(octokit, owner, repo, baseBranch, this.logger);
|
|
33212
|
+
}
|
|
33142
33213
|
if (existingPR != null) {
|
|
33143
33214
|
this.logger.info(
|
|
33144
33215
|
`Found existing updatable PR #${existingPR.number}, will update branch ${existingPR.headBranch}`
|
|
33145
33216
|
);
|
|
33146
33217
|
prBranch = existingPR.headBranch;
|
|
33147
33218
|
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
33219
|
} else {
|
|
33160
|
-
this.logger.debug(
|
|
33220
|
+
this.logger.debug(
|
|
33221
|
+
this.config.automationMode ? `Automation mode: creating new branch ${newPrBranch}` : `No existing updatable PR found, creating new branch ${newPrBranch}`
|
|
33222
|
+
);
|
|
33161
33223
|
prBranch = newPrBranch;
|
|
33162
|
-
|
|
33224
|
+
}
|
|
33225
|
+
const branchAction = resolveBranchAction({
|
|
33226
|
+
automationMode: this.config.automationMode === true,
|
|
33227
|
+
skipCommit,
|
|
33228
|
+
existingPR
|
|
33229
|
+
});
|
|
33230
|
+
switch (branchAction) {
|
|
33231
|
+
case "replay-branch":
|
|
33163
33232
|
generationBaseSha = await createReplayBranch(
|
|
33164
33233
|
repository,
|
|
33165
33234
|
prBranch,
|
|
@@ -33167,8 +33236,16 @@ var GithubStep = class extends BaseStep {
|
|
|
33167
33236
|
replayConflictInfo,
|
|
33168
33237
|
this.logger
|
|
33169
33238
|
);
|
|
33170
|
-
|
|
33171
|
-
|
|
33239
|
+
break;
|
|
33240
|
+
case "create-from-head":
|
|
33241
|
+
await repository.createBranchFromHead(prBranch);
|
|
33242
|
+
break;
|
|
33243
|
+
case "checkout-remote":
|
|
33244
|
+
await repository.checkoutRemoteBranch(prBranch);
|
|
33245
|
+
break;
|
|
33246
|
+
default: {
|
|
33247
|
+
const _exhaustive = branchAction;
|
|
33248
|
+
throw new Error(`Unexpected branch action: ${String(_exhaustive)}`);
|
|
33172
33249
|
}
|
|
33173
33250
|
}
|
|
33174
33251
|
if (!skipCommit) {
|
|
@@ -33178,6 +33255,13 @@ var GithubStep = class extends BaseStep {
|
|
|
33178
33255
|
await repository.commitAllChanges(finalCommitMessage2);
|
|
33179
33256
|
this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
|
|
33180
33257
|
}
|
|
33258
|
+
if (this.config.automationMode) {
|
|
33259
|
+
const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
|
|
33260
|
+
if (noDiff) {
|
|
33261
|
+
this.logger.info("No changes detected after generation \u2014 skipping PR creation");
|
|
33262
|
+
return { executed: true, success: true, skippedNoDiff: true };
|
|
33263
|
+
}
|
|
33264
|
+
}
|
|
33181
33265
|
const result = {
|
|
33182
33266
|
executed: true,
|
|
33183
33267
|
success: true,
|
|
@@ -33212,7 +33296,8 @@ var GithubStep = class extends BaseStep {
|
|
|
33212
33296
|
this.config.versionBumpReason
|
|
33213
33297
|
);
|
|
33214
33298
|
const replaySection = formatReplayPrBody(replayResult, { branchName: prBranch, repoUri: this.config.uri });
|
|
33215
|
-
|
|
33299
|
+
let enrichedBody = replaySection != null ? prBody + "\n\n---\n\n" + replaySection : prBody;
|
|
33300
|
+
enrichedBody = enrichPrBodyForAutomation(enrichedBody, this.config);
|
|
33216
33301
|
if (isUpdatingExistingPR && existingPR != null) {
|
|
33217
33302
|
this.logger.info(`Updated existing pull request: ${existingPR.htmlUrl}`);
|
|
33218
33303
|
result.prUrl = existingPR.htmlUrl;
|
|
@@ -33243,6 +33328,23 @@ var GithubStep = class extends BaseStep {
|
|
|
33243
33328
|
this.logger.info(`Created pull request: ${pullRequest.html_url}`);
|
|
33244
33329
|
result.prUrl = pullRequest.html_url;
|
|
33245
33330
|
result.prNumber = pullRequest.number;
|
|
33331
|
+
if (shouldEnableAutomerge(this.config) && pullRequest.node_id) {
|
|
33332
|
+
try {
|
|
33333
|
+
await octokit.graphql(
|
|
33334
|
+
`mutation($pullRequestId: ID!) {
|
|
33335
|
+
enablePullRequestAutoMerge(input: {
|
|
33336
|
+
pullRequestId: $pullRequestId,
|
|
33337
|
+
mergeMethod: SQUASH
|
|
33338
|
+
}) { clientMutationId }
|
|
33339
|
+
}`,
|
|
33340
|
+
{ pullRequestId: pullRequest.node_id }
|
|
33341
|
+
);
|
|
33342
|
+
this.logger.info(`Enabled automerge on PR #${pullRequest.number}`);
|
|
33343
|
+
result.autoMergeEnabled = true;
|
|
33344
|
+
} catch (autoMergeError) {
|
|
33345
|
+
this.logger.warn(`Could not enable automerge: ${extractErrorMessage(autoMergeError)}`);
|
|
33346
|
+
}
|
|
33347
|
+
}
|
|
33246
33348
|
} catch (error) {
|
|
33247
33349
|
const message = extractErrorMessage(error);
|
|
33248
33350
|
if (message.includes("A pull request already exists for")) {
|
|
@@ -33255,6 +33357,7 @@ var GithubStep = class extends BaseStep {
|
|
|
33255
33357
|
return result;
|
|
33256
33358
|
}
|
|
33257
33359
|
async executePushMode(repository) {
|
|
33360
|
+
const baseBranch = this.config.branch ?? await repository.getDefaultBranch();
|
|
33258
33361
|
if (this.config.branch != null) {
|
|
33259
33362
|
this.logger.debug(`Checking out branch ${this.config.branch}`);
|
|
33260
33363
|
await repository.checkout(this.config.branch);
|
|
@@ -33264,6 +33367,13 @@ var GithubStep = class extends BaseStep {
|
|
|
33264
33367
|
const finalCommitMessage = this.config.commitMessage ?? "SDK Generation";
|
|
33265
33368
|
await repository.commitAllChanges(finalCommitMessage);
|
|
33266
33369
|
this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
|
|
33370
|
+
if (this.config.automationMode) {
|
|
33371
|
+
const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
|
|
33372
|
+
if (noDiff) {
|
|
33373
|
+
this.logger.info("No changes detected after generation \u2014 skipping push");
|
|
33374
|
+
return { executed: true, success: true, skippedNoDiff: true };
|
|
33375
|
+
}
|
|
33376
|
+
}
|
|
33267
33377
|
const result = {
|
|
33268
33378
|
executed: true,
|
|
33269
33379
|
success: true
|
|
@@ -33308,6 +33418,42 @@ var GithubStep = class extends BaseStep {
|
|
|
33308
33418
|
return void 0;
|
|
33309
33419
|
}
|
|
33310
33420
|
};
|
|
33421
|
+
function enrichPrBodyForAutomation(body, config) {
|
|
33422
|
+
if (!config.automationMode) {
|
|
33423
|
+
return body;
|
|
33424
|
+
}
|
|
33425
|
+
let result = body;
|
|
33426
|
+
if (config.hasBreakingChanges && config.breakingChangesSummary) {
|
|
33427
|
+
result += "\n\n---\n\n## \u26A0\uFE0F Breaking Changes\n\nThis release contains breaking changes that require manual review:\n\n" + config.breakingChangesSummary;
|
|
33428
|
+
}
|
|
33429
|
+
if (config.runId) {
|
|
33430
|
+
result += `
|
|
33431
|
+
|
|
33432
|
+
---
|
|
33433
|
+
|
|
33434
|
+
_Fern Run ID: \`${config.runId}\`_`;
|
|
33435
|
+
}
|
|
33436
|
+
return result;
|
|
33437
|
+
}
|
|
33438
|
+
function shouldEnableAutomerge(config) {
|
|
33439
|
+
return config.automationMode === true && config.autoMerge === true && config.hasBreakingChanges !== true;
|
|
33440
|
+
}
|
|
33441
|
+
function resolveBranchAction({
|
|
33442
|
+
automationMode,
|
|
33443
|
+
skipCommit,
|
|
33444
|
+
existingPR
|
|
33445
|
+
}) {
|
|
33446
|
+
if (skipCommit) {
|
|
33447
|
+
return "replay-branch";
|
|
33448
|
+
}
|
|
33449
|
+
if (automationMode) {
|
|
33450
|
+
return "create-from-head";
|
|
33451
|
+
}
|
|
33452
|
+
if (existingPR != null) {
|
|
33453
|
+
return "checkout-remote";
|
|
33454
|
+
}
|
|
33455
|
+
return "create-from-head";
|
|
33456
|
+
}
|
|
33311
33457
|
|
|
33312
33458
|
// src/replay/replay-run.ts
|
|
33313
33459
|
init_dist3();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fern-api/generator-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
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.
|
|
25
|
+
"@fern-api/replay": "0.10.4",
|
|
26
26
|
"@octokit/rest": "^22.0.1",
|
|
27
27
|
"es-toolkit": "^1.45.1",
|
|
28
28
|
"tmp-promise": "^3.0.3"
|