@fern-api/replay 0.11.0 → 0.12.0

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/index.js CHANGED
@@ -439,7 +439,7 @@ var FERN_SUPPORT_NAMES = ["fern-support", "Fern Support"];
439
439
  function isGenerationCommit(commit) {
440
440
  const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
441
441
  const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
442
- const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
442
+ const hasGenerationMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.startsWith("[fern-autoversion]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || // Squash merge of a Fern-generated PR uses the PR title as commit message.
443
443
  // The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
444
444
  // GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
445
445
  commit.message.startsWith("SDK Generation");
@@ -2126,21 +2126,66 @@ var ReplayService = class {
2126
2126
  this.applicator = new ReplayApplicator(git, this.lockManager, outputDir);
2127
2127
  this.committer = new ReplayCommitter(git, outputDir);
2128
2128
  }
2129
- async runReplay(options) {
2129
+ /**
2130
+ * Phase 1 of the two-phase replay flow. Runs preGenerationRebase (when applicable),
2131
+ * detects new patches, and creates the `[fern-generated]` commit. Leaves HEAD at
2132
+ * the generation commit (or unchanged for dry-run).
2133
+ *
2134
+ * Does NOT apply patches — the returned handle must be passed to
2135
+ * `applyPreparedReplay()` to complete the run. No disk writes to the lockfile
2136
+ * happen here; lockfile persistence is deferred to phase 2.
2137
+ *
2138
+ * Callers may land additional commits on HEAD between `prepareReplay` and
2139
+ * `applyPreparedReplay` (e.g., `[fern-autoversion]`).
2140
+ */
2141
+ async prepareReplay(options) {
2130
2142
  this.warnings = [];
2143
+ this.detector.warnings.length = 0;
2131
2144
  if (options?.skipApplication) {
2132
- return this.handleSkipApplication(options);
2145
+ return this._prepareSkipApplication(options);
2133
2146
  }
2134
2147
  const flow = this.determineFlow();
2135
2148
  switch (flow) {
2136
2149
  case "first-generation":
2137
- return this.handleFirstGeneration(options);
2150
+ return this._prepareFirstGeneration(options);
2138
2151
  case "no-patches":
2139
- return this.handleNoPatchesRegeneration(options);
2152
+ return this._prepareNoPatchesRegeneration(options);
2140
2153
  case "normal-regeneration":
2141
- return this.handleNormalRegeneration(options);
2154
+ return this._prepareNormalRegeneration(options);
2142
2155
  }
2143
2156
  }
2157
+ /**
2158
+ * Phase 2 of the two-phase replay flow. Applies patches, rebases them against
2159
+ * the generation, saves the lockfile, and commits `[fern-replay]` (or stages
2160
+ * under `stageOnly`). Operates on HEAD at call time — mid-phase commits are
2161
+ * respected.
2162
+ *
2163
+ * For terminal handles (dry-run, first-gen has no patch work, skip-app does
2164
+ * its own post-commit logic), this returns the precomputed report.
2165
+ */
2166
+ async applyPreparedReplay(prep, options) {
2167
+ if (prep._prepared.terminal) {
2168
+ return prep._prepared.preparedReport;
2169
+ }
2170
+ switch (prep._prepared.flow) {
2171
+ case "first-generation":
2172
+ return this._applyFirstGeneration(prep);
2173
+ case "skip-application":
2174
+ return this._applySkipApplication(prep, options);
2175
+ case "no-patches":
2176
+ return this._applyNoPatchesRegeneration(prep, options);
2177
+ case "normal-regeneration":
2178
+ return this._applyNormalRegeneration(prep, options);
2179
+ }
2180
+ }
2181
+ /**
2182
+ * Backwards-compatible atomic entry point. Composes `prepareReplay` + `applyPreparedReplay`.
2183
+ * Behavior is byte-identical to the pre-split implementation for all existing callers.
2184
+ */
2185
+ async runReplay(options) {
2186
+ const prep = await this.prepareReplay(options);
2187
+ return this.applyPreparedReplay(prep, options);
2188
+ }
2144
2189
  /**
2145
2190
  * Sync the lockfile after a divergent PR was squash-merged.
2146
2191
  * Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
@@ -2214,15 +2259,35 @@ var ReplayService = class {
2214
2259
  throw error;
2215
2260
  }
2216
2261
  }
2217
- async handleFirstGeneration(options) {
2262
+ firstGenerationReport() {
2263
+ return {
2264
+ flow: "first-generation",
2265
+ patchesDetected: 0,
2266
+ patchesApplied: 0,
2267
+ patchesWithConflicts: 0,
2268
+ patchesSkipped: 0,
2269
+ conflicts: []
2270
+ };
2271
+ }
2272
+ async _prepareFirstGeneration(options) {
2218
2273
  if (options?.dryRun) {
2274
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
2219
2275
  return {
2220
2276
  flow: "first-generation",
2221
- patchesDetected: 0,
2222
- patchesApplied: 0,
2223
- patchesWithConflicts: 0,
2224
- patchesSkipped: 0,
2225
- conflicts: []
2277
+ previousGenerationSha: null,
2278
+ currentGenerationSha: headSha,
2279
+ baseBranchHead: options.baseBranchHead ?? null,
2280
+ _prepared: {
2281
+ flow: "first-generation",
2282
+ terminal: true,
2283
+ preparedReport: this.firstGenerationReport(),
2284
+ patchesToApply: [],
2285
+ newPatchIds: /* @__PURE__ */ new Set(),
2286
+ detectorWarnings: [],
2287
+ revertedCount: 0,
2288
+ genSha: headSha,
2289
+ optionsSnapshot: options
2290
+ }
2226
2291
  };
2227
2292
  }
2228
2293
  const commitOpts = options ? {
@@ -2232,22 +2297,36 @@ var ReplayService = class {
2232
2297
  } : void 0;
2233
2298
  await this.committer.commitGeneration("Initial SDK generation", commitOpts);
2234
2299
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
2235
- this.lockManager.initialize(genRecord);
2300
+ this.lockManager.initializeInMemory(genRecord);
2236
2301
  return {
2237
2302
  flow: "first-generation",
2238
- patchesDetected: 0,
2239
- patchesApplied: 0,
2240
- patchesWithConflicts: 0,
2241
- patchesSkipped: 0,
2242
- conflicts: []
2303
+ previousGenerationSha: null,
2304
+ currentGenerationSha: genRecord.commit_sha,
2305
+ baseBranchHead: options?.baseBranchHead ?? null,
2306
+ _prepared: {
2307
+ flow: "first-generation",
2308
+ terminal: false,
2309
+ patchesToApply: [],
2310
+ newPatchIds: /* @__PURE__ */ new Set(),
2311
+ detectorWarnings: [],
2312
+ revertedCount: 0,
2313
+ genSha: genRecord.commit_sha,
2314
+ optionsSnapshot: options
2315
+ }
2243
2316
  };
2244
2317
  }
2318
+ async _applyFirstGeneration(_prep) {
2319
+ this.lockManager.save();
2320
+ return this.firstGenerationReport();
2321
+ }
2245
2322
  /**
2246
- * Skip-application mode: commit the generation and update the lockfile
2247
- * but don't detect or apply patches. Sets a marker so the next normal
2248
- * run skips revert detection in preGenerationRebase().
2323
+ * Skip-application mode phase 1: commit the generation and update the
2324
+ * in-memory lockfile, but don't detect or apply patches. Sets a marker
2325
+ * so the next normal run skips revert detection in preGenerationRebase().
2326
+ *
2327
+ * Disk save is deferred to `_applySkipApplication`.
2249
2328
  */
2250
- async handleSkipApplication(options) {
2329
+ async _prepareSkipApplication(options) {
2251
2330
  const commitOpts = options ? {
2252
2331
  cliVersion: options.cliVersion ?? "unknown",
2253
2332
  generatorVersions: options.generatorVersions ?? {},
@@ -2256,8 +2335,10 @@ var ReplayService = class {
2256
2335
  await this.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
2257
2336
  await this.cleanupStaleConflictMarkers();
2258
2337
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
2338
+ let previousGenerationSha = null;
2259
2339
  try {
2260
- this.lockManager.read();
2340
+ const lock = this.lockManager.read();
2341
+ previousGenerationSha = lock.current_generation ?? null;
2261
2342
  this.lockManager.addGeneration(genRecord);
2262
2343
  } catch (error) {
2263
2344
  if (error instanceof LockfileNotFoundError) {
@@ -2267,6 +2348,24 @@ var ReplayService = class {
2267
2348
  }
2268
2349
  }
2269
2350
  this.lockManager.setReplaySkippedAt((/* @__PURE__ */ new Date()).toISOString());
2351
+ return {
2352
+ flow: "skip-application",
2353
+ previousGenerationSha,
2354
+ currentGenerationSha: genRecord.commit_sha,
2355
+ baseBranchHead: options?.baseBranchHead ?? null,
2356
+ _prepared: {
2357
+ flow: "skip-application",
2358
+ terminal: false,
2359
+ patchesToApply: [],
2360
+ newPatchIds: /* @__PURE__ */ new Set(),
2361
+ detectorWarnings: [],
2362
+ revertedCount: 0,
2363
+ genSha: genRecord.commit_sha,
2364
+ optionsSnapshot: options
2365
+ }
2366
+ };
2367
+ }
2368
+ async _applySkipApplication(_prep, options) {
2270
2369
  this.lockManager.save();
2271
2370
  const credentialWarnings = [...this.lockManager.warnings];
2272
2371
  if (!options?.stageOnly) {
@@ -2284,12 +2383,14 @@ var ReplayService = class {
2284
2383
  warnings: credentialWarnings.length > 0 ? credentialWarnings : void 0
2285
2384
  };
2286
2385
  }
2287
- async handleNoPatchesRegeneration(options) {
2386
+ async _prepareNoPatchesRegeneration(options) {
2387
+ const preLock = this.lockManager.read();
2388
+ const previousGenerationSha = preLock.current_generation ?? null;
2288
2389
  let { patches: newPatches, revertedPatchIds } = await this.detector.detectNewPatches();
2289
2390
  const detectorWarnings = [...this.detector.warnings];
2290
2391
  if (options?.dryRun) {
2291
2392
  const dryRunWarnings = [...detectorWarnings, ...this.warnings];
2292
- return {
2393
+ const preparedReport = {
2293
2394
  flow: "no-patches",
2294
2395
  patchesDetected: newPatches.length,
2295
2396
  patchesApplied: 0,
@@ -2300,9 +2401,26 @@ var ReplayService = class {
2300
2401
  wouldApply: newPatches,
2301
2402
  warnings: dryRunWarnings.length > 0 ? dryRunWarnings : void 0
2302
2403
  };
2404
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
2405
+ return {
2406
+ flow: "no-patches",
2407
+ previousGenerationSha,
2408
+ currentGenerationSha: headSha,
2409
+ baseBranchHead: options.baseBranchHead ?? null,
2410
+ _prepared: {
2411
+ flow: "no-patches",
2412
+ terminal: true,
2413
+ preparedReport,
2414
+ patchesToApply: [],
2415
+ newPatchIds: /* @__PURE__ */ new Set(),
2416
+ detectorWarnings,
2417
+ revertedCount: revertedPatchIds.length,
2418
+ genSha: headSha,
2419
+ optionsSnapshot: options
2420
+ }
2421
+ };
2303
2422
  }
2304
2423
  {
2305
- const preLock = this.lockManager.read();
2306
2424
  const preCurrentGen = preLock.current_generation;
2307
2425
  const uniqueFiles = [...new Set(newPatches.flatMap((p) => p.files))];
2308
2426
  const absorbedFiles = /* @__PURE__ */ new Set();
@@ -2333,6 +2451,27 @@ var ReplayService = class {
2333
2451
  await this.cleanupStaleConflictMarkers();
2334
2452
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
2335
2453
  this.lockManager.addGeneration(genRecord);
2454
+ return {
2455
+ flow: "no-patches",
2456
+ previousGenerationSha,
2457
+ currentGenerationSha: genRecord.commit_sha,
2458
+ baseBranchHead: options?.baseBranchHead ?? null,
2459
+ _prepared: {
2460
+ flow: "no-patches",
2461
+ terminal: false,
2462
+ patchesToApply: newPatches,
2463
+ newPatchIds: new Set(newPatches.map((p) => p.id)),
2464
+ detectorWarnings,
2465
+ revertedCount: revertedPatchIds.length,
2466
+ genSha: genRecord.commit_sha,
2467
+ optionsSnapshot: options
2468
+ }
2469
+ };
2470
+ }
2471
+ async _applyNoPatchesRegeneration(prep, options) {
2472
+ const newPatches = prep._prepared.patchesToApply;
2473
+ const detectorWarnings = prep._prepared.detectorWarnings;
2474
+ const genSha = prep._prepared.genSha;
2336
2475
  let results = [];
2337
2476
  if (newPatches.length > 0) {
2338
2477
  results = await this.applicator.applyPatches(newPatches);
@@ -2344,7 +2483,7 @@ var ReplayService = class {
2344
2483
  }
2345
2484
  }
2346
2485
  }
2347
- const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
2486
+ const rebaseCounts = await this.rebasePatches(results, genSha);
2348
2487
  for (const patch of newPatches) {
2349
2488
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
2350
2489
  this.lockManager.addPatch(patch);
@@ -2361,15 +2500,26 @@ var ReplayService = class {
2361
2500
  }
2362
2501
  }
2363
2502
  const warnings = [...detectorWarnings, ...this.warnings];
2364
- return this.buildReport("no-patches", newPatches, results, options, warnings, rebaseCounts, void 0, revertedPatchIds.length);
2503
+ return this.buildReport(
2504
+ "no-patches",
2505
+ newPatches,
2506
+ results,
2507
+ prep._prepared.optionsSnapshot,
2508
+ warnings,
2509
+ rebaseCounts,
2510
+ void 0,
2511
+ prep._prepared.revertedCount
2512
+ );
2365
2513
  }
2366
- async handleNormalRegeneration(options) {
2514
+ async _prepareNormalRegeneration(options) {
2515
+ const preLock = this.lockManager.read();
2516
+ const previousGenerationSha = preLock.current_generation ?? null;
2367
2517
  if (options?.dryRun) {
2368
2518
  const existingPatches2 = this.lockManager.getPatches();
2369
2519
  const { patches: newPatches2, revertedPatchIds: dryRunReverted } = await this.detector.detectNewPatches();
2370
- const warnings2 = [...this.detector.warnings, ...this.warnings];
2520
+ const warnings = [...this.detector.warnings, ...this.warnings];
2371
2521
  const allPatches2 = [...existingPatches2, ...newPatches2];
2372
- return {
2522
+ const preparedReport = {
2373
2523
  flow: "normal-regeneration",
2374
2524
  patchesDetected: allPatches2.length,
2375
2525
  patchesApplied: 0,
@@ -2378,7 +2528,25 @@ var ReplayService = class {
2378
2528
  patchesReverted: dryRunReverted.length,
2379
2529
  conflicts: [],
2380
2530
  wouldApply: allPatches2,
2381
- warnings: warnings2.length > 0 ? warnings2 : void 0
2531
+ warnings: warnings.length > 0 ? warnings : void 0
2532
+ };
2533
+ const headSha = await this.git.exec(["rev-parse", "HEAD"]).then((s) => s.trim()).catch(() => "");
2534
+ return {
2535
+ flow: "normal-regeneration",
2536
+ previousGenerationSha,
2537
+ currentGenerationSha: headSha,
2538
+ baseBranchHead: options.baseBranchHead ?? null,
2539
+ _prepared: {
2540
+ flow: "normal-regeneration",
2541
+ terminal: true,
2542
+ preparedReport,
2543
+ patchesToApply: [],
2544
+ newPatchIds: /* @__PURE__ */ new Set(),
2545
+ detectorWarnings: [...this.detector.warnings],
2546
+ revertedCount: dryRunReverted.length,
2547
+ genSha: headSha,
2548
+ optionsSnapshot: options
2549
+ }
2382
2550
  };
2383
2551
  }
2384
2552
  let existingPatches = this.lockManager.getPatches();
@@ -2430,6 +2598,29 @@ var ReplayService = class {
2430
2598
  await this.cleanupStaleConflictMarkers();
2431
2599
  const genRecord = await this.committer.createGenerationRecord(commitOpts);
2432
2600
  this.lockManager.addGeneration(genRecord);
2601
+ return {
2602
+ flow: "normal-regeneration",
2603
+ previousGenerationSha,
2604
+ currentGenerationSha: genRecord.commit_sha,
2605
+ baseBranchHead: options?.baseBranchHead ?? null,
2606
+ _prepared: {
2607
+ flow: "normal-regeneration",
2608
+ terminal: false,
2609
+ patchesToApply: allPatches,
2610
+ newPatchIds: new Set(newPatches.map((p) => p.id)),
2611
+ preRebaseCounts,
2612
+ detectorWarnings,
2613
+ revertedCount: revertedPatchIds.length,
2614
+ genSha: genRecord.commit_sha,
2615
+ optionsSnapshot: options
2616
+ }
2617
+ };
2618
+ }
2619
+ async _applyNormalRegeneration(prep, options) {
2620
+ const allPatches = prep._prepared.patchesToApply;
2621
+ const newPatchIds = prep._prepared.newPatchIds;
2622
+ const detectorWarnings = prep._prepared.detectorWarnings;
2623
+ const genSha = prep._prepared.genSha;
2433
2624
  const results = await this.applicator.applyPatches(allPatches);
2434
2625
  this._lastApplyResults = results;
2435
2626
  this.revertConflictingFiles(results);
@@ -2438,7 +2629,8 @@ var ReplayService = class {
2438
2629
  result.patch.status = "unresolved";
2439
2630
  }
2440
2631
  }
2441
- const rebaseCounts = await this.rebasePatches(results, genRecord.commit_sha);
2632
+ const rebaseCounts = await this.rebasePatches(results, genSha);
2633
+ const newPatches = allPatches.filter((p) => newPatchIds.has(p.id));
2442
2634
  for (const patch of newPatches) {
2443
2635
  if (!rebaseCounts.absorbedPatchIds.has(patch.id)) {
2444
2636
  this.lockManager.addPatch(patch);
@@ -2465,11 +2657,11 @@ var ReplayService = class {
2465
2657
  "normal-regeneration",
2466
2658
  allPatches,
2467
2659
  results,
2468
- options,
2660
+ prep._prepared.optionsSnapshot,
2469
2661
  warnings,
2470
2662
  rebaseCounts,
2471
- preRebaseCounts,
2472
- revertedPatchIds.length
2663
+ prep._prepared.preRebaseCounts,
2664
+ prep._prepared.revertedCount
2473
2665
  );
2474
2666
  }
2475
2667
  /**