@fern-api/generator-cli 0.9.42 → 0.9.44

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.
Files changed (3) hide show
  1. package/dist/api.js +20 -0
  2. package/dist/cli.js +120 -24
  3. package/package.json +1 -1
package/dist/api.js CHANGED
@@ -6168,6 +6168,26 @@ var ClonedRepository = class _ClonedRepository {
6168
6168
  const result = await this.git.raw(["rev-parse", `${commitSha}^{tree}`]);
6169
6169
  return result.trim();
6170
6170
  }
6171
+ async getCommitMessage(commitSha) {
6172
+ await this.git.cwd(this.clonePath);
6173
+ const result = await this.git.raw(["log", "-1", "--format=%B", commitSha]);
6174
+ return result.replace(/\n$/, "");
6175
+ }
6176
+ async getCommitParents(commitSha) {
6177
+ await this.git.cwd(this.clonePath);
6178
+ const result = await this.git.raw(["rev-list", "--parents", "-n", "1", commitSha]);
6179
+ const parts = result.trim().split(/\s+/);
6180
+ return parts.slice(1);
6181
+ }
6182
+ /**
6183
+ * Returns the SHAs of commits reachable from HEAD but not from any remote-tracking ref,
6184
+ * ordered oldest-first. These are the commits created locally that have not been pushed.
6185
+ */
6186
+ async getLocalOnlyCommits() {
6187
+ await this.git.cwd(this.clonePath);
6188
+ const result = await this.git.raw(["rev-list", "--reverse", "HEAD", "--not", "--remotes=origin"]);
6189
+ return result.trim().split("\n").filter((line) => line.length > 0);
6190
+ }
6171
6191
  /**
6172
6192
  * Returns true if the working tree has uncommitted changes (staged or unstaged).
6173
6193
  */
package/dist/cli.js CHANGED
@@ -32254,6 +32254,26 @@ var ClonedRepository = class _ClonedRepository {
32254
32254
  const result = await this.git.raw(["rev-parse", `${commitSha}^{tree}`]);
32255
32255
  return result.trim();
32256
32256
  }
32257
+ async getCommitMessage(commitSha) {
32258
+ await this.git.cwd(this.clonePath);
32259
+ const result = await this.git.raw(["log", "-1", "--format=%B", commitSha]);
32260
+ return result.replace(/\n$/, "");
32261
+ }
32262
+ async getCommitParents(commitSha) {
32263
+ await this.git.cwd(this.clonePath);
32264
+ const result = await this.git.raw(["rev-list", "--parents", "-n", "1", commitSha]);
32265
+ const parts = result.trim().split(/\s+/);
32266
+ return parts.slice(1);
32267
+ }
32268
+ /**
32269
+ * Returns the SHAs of commits reachable from HEAD but not from any remote-tracking ref,
32270
+ * ordered oldest-first. These are the commits created locally that have not been pushed.
32271
+ */
32272
+ async getLocalOnlyCommits() {
32273
+ await this.git.cwd(this.clonePath);
32274
+ const result = await this.git.raw(["rev-list", "--reverse", "HEAD", "--not", "--remotes=origin"]);
32275
+ return result.trim().split("\n").filter((line) => line.length > 0);
32276
+ }
32257
32277
  /**
32258
32278
  * Returns true if the working tree has uncommitted changes (staged or unstaged).
32259
32279
  */
@@ -40448,7 +40468,7 @@ var consolePipelineLogger = {
40448
40468
  };
40449
40469
 
40450
40470
  // src/pipeline/PostGenerationPipeline.ts
40451
- var import_child_process6 = require("child_process");
40471
+ var import_child_process7 = require("child_process");
40452
40472
 
40453
40473
  // src/pipeline/github/constants.ts
40454
40474
  var FERN_BOT_NAME2 = "fern-api";
@@ -42461,6 +42481,7 @@ ${prBody}`;
42461
42481
 
42462
42482
  // src/pipeline/github/pushSignedCommit.ts
42463
42483
  var MAX_CONCURRENT_PUSH_RETRIES = 3;
42484
+ var MAX_SIGNED_CHAIN_LENGTH = 20;
42464
42485
  async function pushSignedCommit({
42465
42486
  repository,
42466
42487
  octokit,
@@ -42475,25 +42496,19 @@ async function pushSignedCommit({
42475
42496
  const tempRef = `refs/temp/fern-${Date.now()}`;
42476
42497
  let tempRefPushed = false;
42477
42498
  try {
42478
- let [localHeadSha, treeSha, message, parents] = await Promise.all([
42479
- repository.getHeadSha(),
42480
- repository.getHeadTreeHash(),
42481
- repository.getHeadCommitMessage(),
42482
- repository.getHeadParents()
42483
- ]);
42499
+ let localHeadSha = await repository.getHeadSha();
42484
42500
  await repository.pushObjectToRef(localHeadSha, tempRef);
42485
42501
  tempRefPushed = true;
42486
42502
  for (let attempt2 = 0; attempt2 < MAX_CONCURRENT_PUSH_RETRIES; attempt2++) {
42487
- const identityFields = author != null ? { author, committer: author } : {};
42488
- const { data: signedCommit } = await octokit.git.createCommit({
42503
+ const signedSha = await signLocalCommitChain({
42504
+ repository,
42505
+ octokit,
42489
42506
  owner,
42490
42507
  repo,
42491
- message,
42492
- tree: treeSha,
42493
- parents,
42494
- ...identityFields
42508
+ localHeadSha,
42509
+ author,
42510
+ logger
42495
42511
  });
42496
- const signedSha = signedCommit.sha;
42497
42512
  try {
42498
42513
  await upsertBranchRef({ octokit, owner, repo, branch, sha: signedSha, force });
42499
42514
  logger.debug(`Updated refs/heads/${branch} to signed commit ${signedSha}`);
@@ -42508,12 +42523,7 @@ async function pushSignedCommit({
42508
42523
  `Non-fast-forward on refs/heads/${branch} (attempt ${attempt2 + 1}/${MAX_CONCURRENT_PUSH_RETRIES}); rebasing locally and retrying.`
42509
42524
  );
42510
42525
  await repository.pullWithRebase(branch);
42511
- [localHeadSha, treeSha, message, parents] = await Promise.all([
42512
- repository.getHeadSha(),
42513
- repository.getHeadTreeHash(),
42514
- repository.getHeadCommitMessage(),
42515
- repository.getHeadParents()
42516
- ]);
42526
+ localHeadSha = await repository.getHeadSha();
42517
42527
  await repository.pushObjectToRef(localHeadSha, tempRef, { force: true });
42518
42528
  } else {
42519
42529
  throw err;
@@ -42533,6 +42543,49 @@ async function pushSignedCommit({
42533
42543
  }
42534
42544
  }
42535
42545
  }
42546
+ async function signLocalCommitChain({
42547
+ repository,
42548
+ octokit,
42549
+ owner,
42550
+ repo,
42551
+ localHeadSha,
42552
+ author,
42553
+ logger
42554
+ }) {
42555
+ let chain = await repository.getLocalOnlyCommits();
42556
+ if (chain.length === 0 || chain[chain.length - 1] !== localHeadSha) {
42557
+ chain = [localHeadSha];
42558
+ } else if (chain.length > MAX_SIGNED_CHAIN_LENGTH) {
42559
+ logger.warn(
42560
+ `Local commit chain has ${chain.length} commits (limit ${MAX_SIGNED_CHAIN_LENGTH}); signing HEAD only.`
42561
+ );
42562
+ chain = [localHeadSha];
42563
+ }
42564
+ const identityFields = author != null ? { author, committer: author } : {};
42565
+ const signedShaByLocalSha = /* @__PURE__ */ new Map();
42566
+ let signedSha = localHeadSha;
42567
+ for (const sha of chain) {
42568
+ const [treeSha, message, parents] = await Promise.all([
42569
+ repository.getCommitTreeHash(sha),
42570
+ repository.getCommitMessage(sha),
42571
+ repository.getCommitParents(sha)
42572
+ ]);
42573
+ const { data: signedCommit } = await octokit.git.createCommit({
42574
+ owner,
42575
+ repo,
42576
+ message,
42577
+ tree: treeSha,
42578
+ parents: parents.map((parent) => signedShaByLocalSha.get(parent) ?? parent),
42579
+ ...identityFields
42580
+ });
42581
+ signedShaByLocalSha.set(sha, signedCommit.sha);
42582
+ signedSha = signedCommit.sha;
42583
+ }
42584
+ if (chain.length > 1) {
42585
+ logger.debug(`Signed ${chain.length} local commits via the GitHub API`);
42586
+ }
42587
+ return signedSha;
42588
+ }
42536
42589
  async function upsertBranchRef({
42537
42590
  octokit,
42538
42591
  owner,
@@ -43119,6 +43172,7 @@ function resolveBranchAction({
43119
43172
  }
43120
43173
 
43121
43174
  // src/pipeline/steps/ReplayStep.ts
43175
+ var import_child_process6 = require("child_process");
43122
43176
  var ReplayStep = class extends BaseStep {
43123
43177
  constructor(outputDir, logger, config, cliVersion, generatorVersions) {
43124
43178
  super(outputDir, logger);
@@ -43159,17 +43213,21 @@ var ReplayStep = class extends BaseStep {
43159
43213
  patchesWithConflicts: 0
43160
43214
  };
43161
43215
  }
43216
+ const stageOnly = this.config.stageOnly ?? false;
43162
43217
  const result = prepared != null ? await replayApply(prepared, {
43163
- stageOnly: this.config.stageOnly ?? false,
43218
+ stageOnly,
43164
43219
  logger: this.logger
43165
43220
  }) : await replayRun({
43166
43221
  outputDir: this.outputDir,
43167
43222
  cliVersion: this.cliVersion,
43168
43223
  generatorVersions: this.generatorVersions,
43169
- stageOnly: this.config.stageOnly ?? false,
43224
+ stageOnly,
43170
43225
  skipApplication: this.config.skipApplication,
43171
43226
  logger: this.logger
43172
43227
  });
43228
+ if (!stageOnly) {
43229
+ this.commitLockfileIfUncommitted();
43230
+ }
43173
43231
  if (result.failureReason != null) {
43174
43232
  return {
43175
43233
  executed: true,
@@ -43233,6 +43291,44 @@ var ReplayStep = class extends BaseStep {
43233
43291
  warnings: report.warnings
43234
43292
  };
43235
43293
  }
43294
+ /**
43295
+ * Commits `.fern/replay.lock` when replay saved the lockfile to disk but
43296
+ * didn't create a `[fern-replay]` commit (e.g. no-patches flow with zero
43297
+ * new patches). Without this, `skipCommit = true` in GithubStep drops the
43298
+ * lockfile advance, freezing `previousGenerationSha` across runs.
43299
+ */
43300
+ commitLockfileIfUncommitted() {
43301
+ try {
43302
+ const status2 = (0, import_child_process6.execFileSync)("git", ["status", "--porcelain", "--", ".fern/replay.lock"], {
43303
+ cwd: this.outputDir,
43304
+ encoding: "utf-8",
43305
+ stdio: ["pipe", "pipe", "pipe"]
43306
+ }).trim();
43307
+ if (status2.length === 0) {
43308
+ return;
43309
+ }
43310
+ (0, import_child_process6.execFileSync)("git", ["add", "--", ".fern/replay.lock"], {
43311
+ cwd: this.outputDir,
43312
+ stdio: "pipe"
43313
+ });
43314
+ (0, import_child_process6.execFileSync)(
43315
+ "git",
43316
+ [
43317
+ "-c",
43318
+ `user.name=${FERN_BOT_NAME2}`,
43319
+ "-c",
43320
+ `user.email=${FERN_BOT_EMAIL2}`,
43321
+ "commit",
43322
+ "-m",
43323
+ "[fern-replay] advance lockfile"
43324
+ ],
43325
+ { cwd: this.outputDir, stdio: "pipe" }
43326
+ );
43327
+ this.logger.debug("ReplayStep: committed uncommitted lockfile advance.");
43328
+ } catch (error) {
43329
+ this.logger.debug("ReplayStep: failed to commit lockfile advance: " + String(error));
43330
+ }
43331
+ }
43236
43332
  };
43237
43333
 
43238
43334
  // ../cli/generation/local-generation/docker-utils/lib/runDocker.js
@@ -43567,8 +43663,8 @@ var PostGenerationPipeline = class {
43567
43663
  async run() {
43568
43664
  if (this.config.github?.enabled) {
43569
43665
  try {
43570
- (0, import_child_process6.execFileSync)("git", ["config", "user.name", FERN_BOT_NAME2], { cwd: this.config.outputDir });
43571
- (0, import_child_process6.execFileSync)("git", ["config", "user.email", FERN_BOT_EMAIL2], { cwd: this.config.outputDir });
43666
+ (0, import_child_process7.execFileSync)("git", ["config", "user.name", FERN_BOT_NAME2], { cwd: this.config.outputDir });
43667
+ (0, import_child_process7.execFileSync)("git", ["config", "user.email", FERN_BOT_EMAIL2], { cwd: this.config.outputDir });
43572
43668
  } catch {
43573
43669
  }
43574
43670
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fern-api/generator-cli",
3
- "version": "0.9.42",
3
+ "version": "0.9.44",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fern-api/fern.git",