@fern-api/replay 0.15.2 → 0.16.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/cli.cjs +90 -125
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +90 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -37
- package/dist/index.d.ts +24 -37
- package/dist/index.js +90 -125
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -13596,9 +13596,22 @@ function isGenerationCommit(commit) {
|
|
|
13596
13596
|
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.
|
|
13597
13597
|
// The default PR title is "SDK Generation" (from GithubStep's commitMessage default).
|
|
13598
13598
|
// GitHub appends "(#N)" for the PR number, e.g. "SDK Generation (#70)".
|
|
13599
|
-
commit.message.startsWith("SDK Generation")
|
|
13599
|
+
commit.message.startsWith("SDK Generation") || // Merge commit (GitHub's "Create a merge commit" option) of a fern-bot
|
|
13600
|
+
// regen PR. First-parent walking lands on this commit, not on the
|
|
13601
|
+
// generation commit on the side branch, so the classifier must treat
|
|
13602
|
+
// it as a generation boundary. GitHub's default subject is
|
|
13603
|
+
// "Merge pull request #N from <owner>/<branch>"; fern-bot branches are
|
|
13604
|
+
// named "<owner>/fern-bot/<...>".
|
|
13605
|
+
/^Merge pull request #\d+ from [^/]+\/fern-bot\//.test(commit.message);
|
|
13600
13606
|
return isBotAuthor || hasGenerationMarker;
|
|
13601
13607
|
}
|
|
13608
|
+
function isGenerationBoundary(commit) {
|
|
13609
|
+
const isFernSupport = FERN_SUPPORT_NAMES.includes(commit.authorName);
|
|
13610
|
+
const isBotAuthor = !isFernSupport && (commit.authorLogin === FERN_BOT_LOGIN || commit.authorEmail === FERN_BOT_EMAIL || commit.authorName === FERN_BOT_NAME);
|
|
13611
|
+
const isAutoversion = commit.message.startsWith("[fern-autoversion]");
|
|
13612
|
+
const hasBoundaryMarker = commit.message.startsWith("[fern-generated]") || commit.message.startsWith("[fern-replay]") || commit.message.includes("Generated by Fern") || commit.message.includes("\u{1F916} Generated with Fern") || commit.message.startsWith("SDK Generation");
|
|
13613
|
+
return isBotAuthor && !isAutoversion || hasBoundaryMarker;
|
|
13614
|
+
}
|
|
13602
13615
|
function isReplayCommit(commit) {
|
|
13603
13616
|
return commit.message.startsWith("[fern-replay]");
|
|
13604
13617
|
}
|
|
@@ -13672,8 +13685,48 @@ var ReplayDetector = class {
|
|
|
13672
13685
|
this.lockManager = lockManager;
|
|
13673
13686
|
this.sdkOutputDir = sdkOutputDir;
|
|
13674
13687
|
}
|
|
13688
|
+
/**
|
|
13689
|
+
* Derive the previous-generation SHA from git history instead of trusting
|
|
13690
|
+
* `lock.current_generation`.
|
|
13691
|
+
*
|
|
13692
|
+
* Walks `git log HEAD --first-parent` and returns the SHA of the most recent
|
|
13693
|
+
* commit that `isGenerationBoundary` classifies as a customization-baseline
|
|
13694
|
+
* boundary, or null if no such commit is reachable from HEAD on the mainline.
|
|
13695
|
+
*
|
|
13696
|
+
* `isGenerationBoundary` is intentionally narrower than `isGenerationCommit`:
|
|
13697
|
+
* it excludes `[fern-autoversion]` (lightweight bumps that don't reset the
|
|
13698
|
+
* baseline) and the GitHub merge-commit subject for fern-bot regen PRs
|
|
13699
|
+
* (which sits on first-parent but where the customer fix-up commits live
|
|
13700
|
+
* on second-parent — stopping at the merge would hide them).
|
|
13701
|
+
*
|
|
13702
|
+
* See docs/adr/0001-derived-scan-boundary.md.
|
|
13703
|
+
*/
|
|
13704
|
+
async findPreviousGenerationFromHistory() {
|
|
13705
|
+
let log;
|
|
13706
|
+
try {
|
|
13707
|
+
log = await this.git.exec([
|
|
13708
|
+
"log",
|
|
13709
|
+
"--first-parent",
|
|
13710
|
+
"--format=%H%x00%an%x00%ae%x00%s",
|
|
13711
|
+
"HEAD"
|
|
13712
|
+
]);
|
|
13713
|
+
} catch {
|
|
13714
|
+
return null;
|
|
13715
|
+
}
|
|
13716
|
+
for (const commit of this.parseGitLog(log)) {
|
|
13717
|
+
if (isGenerationBoundary(commit)) {
|
|
13718
|
+
return commit.sha;
|
|
13719
|
+
}
|
|
13720
|
+
}
|
|
13721
|
+
return null;
|
|
13722
|
+
}
|
|
13675
13723
|
async detectNewPatches() {
|
|
13676
13724
|
const lock = this.lockManager.read();
|
|
13725
|
+
const derivedSha = await this.findPreviousGenerationFromHistory();
|
|
13726
|
+
if (derivedSha !== null) {
|
|
13727
|
+
const derivedRecord = await this.synthesizeGenerationRecord(derivedSha);
|
|
13728
|
+
return this.detectPatchesInRange(derivedSha, derivedRecord, lock);
|
|
13729
|
+
}
|
|
13677
13730
|
const lastGen = this.getLastGeneration(lock);
|
|
13678
13731
|
if (!lastGen) {
|
|
13679
13732
|
return { patches: [], revertedPatchIds: [] };
|
|
@@ -13681,7 +13734,7 @@ var ReplayDetector = class {
|
|
|
13681
13734
|
const exists2 = await this.git.commitExists(lastGen.commit_sha);
|
|
13682
13735
|
if (!exists2) {
|
|
13683
13736
|
this.warnings.push(
|
|
13684
|
-
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to
|
|
13737
|
+
`Generation commit ${lastGen.commit_sha.slice(0, 7)} not found in git history. Falling back to tree-diff detection (likely a shallow clone).`
|
|
13685
13738
|
);
|
|
13686
13739
|
return this.detectPatchesViaTreeDiff(
|
|
13687
13740
|
lastGen,
|
|
@@ -13689,44 +13742,8 @@ var ReplayDetector = class {
|
|
|
13689
13742
|
true
|
|
13690
13743
|
);
|
|
13691
13744
|
}
|
|
13692
|
-
const isAncestor = await this.git.isAncestor(lastGen.commit_sha, "HEAD");
|
|
13693
|
-
if (!isAncestor) {
|
|
13694
|
-
return this.detectPatchesViaMergeBase(lastGen, lock);
|
|
13695
|
-
}
|
|
13696
13745
|
return this.detectPatchesInRange(lastGen.commit_sha, lastGen, lock);
|
|
13697
13746
|
}
|
|
13698
|
-
/**
|
|
13699
|
-
* Non-linear-history detection via `merge-base(prevGen, HEAD)`.
|
|
13700
|
-
*
|
|
13701
|
-
* After a squash-merge of a Fern-bot PR, `lastGen.commit_sha` (the previous
|
|
13702
|
-
* `[fern-generated]`) is orphaned but still in git's object database. The
|
|
13703
|
-
* merge-base is the commit on main from which the bot's branch was created —
|
|
13704
|
-
* a reachable ancestor of HEAD. Walking that range and classifying each
|
|
13705
|
-
* commit via `isGenerationCommit` mirrors the linear path and eliminates
|
|
13706
|
-
* the bug class where pipeline-driven changes (autoversion, replay,
|
|
13707
|
-
* generation) get bundled as customer customizations.
|
|
13708
|
-
*
|
|
13709
|
-
* Falls through to the legacy composite path when no common ancestor exists
|
|
13710
|
-
* (truly disjoint history — orphan branches with no shared root).
|
|
13711
|
-
*/
|
|
13712
|
-
async detectPatchesViaMergeBase(lastGen, lock) {
|
|
13713
|
-
let mergeBase = "";
|
|
13714
|
-
try {
|
|
13715
|
-
mergeBase = (await this.git.exec(["merge-base", lastGen.commit_sha, "HEAD"])).trim();
|
|
13716
|
-
} catch {
|
|
13717
|
-
}
|
|
13718
|
-
if (!mergeBase) {
|
|
13719
|
-
this.warnings.push(
|
|
13720
|
-
`No common ancestor between previous generation ${lastGen.commit_sha.slice(0, 7)} and HEAD. Falling back to composite tree-diff detection.`
|
|
13721
|
-
);
|
|
13722
|
-
return this.detectPatchesViaTreeDiff(
|
|
13723
|
-
lastGen,
|
|
13724
|
-
/* commitKnownMissing */
|
|
13725
|
-
false
|
|
13726
|
-
);
|
|
13727
|
-
}
|
|
13728
|
-
return this.detectPatchesInRange(mergeBase, lastGen, lock);
|
|
13729
|
-
}
|
|
13730
13747
|
/**
|
|
13731
13748
|
* Walk `${rangeStart}..HEAD`, classify each commit, emit one
|
|
13732
13749
|
* `StoredPatch` per customer commit. Body shared by the linear path
|
|
@@ -14162,6 +14179,26 @@ var ReplayDetector = class {
|
|
|
14162
14179
|
getLastGeneration(lock) {
|
|
14163
14180
|
return lock.generations.find((g) => g.commit_sha === lock.current_generation);
|
|
14164
14181
|
}
|
|
14182
|
+
/**
|
|
14183
|
+
* Build a transient `GenerationRecord` from a SHA derived by walking git
|
|
14184
|
+
* history. Only `commit_sha` (and `tree_hash` when needed downstream) is
|
|
14185
|
+
* load-bearing here; the rest are filled with defensible placeholders.
|
|
14186
|
+
* This record is consumed by `detectPatchesInRange` and is never persisted.
|
|
14187
|
+
*/
|
|
14188
|
+
async synthesizeGenerationRecord(sha) {
|
|
14189
|
+
let tree_hash = "";
|
|
14190
|
+
try {
|
|
14191
|
+
tree_hash = await this.git.getTreeHash(sha);
|
|
14192
|
+
} catch {
|
|
14193
|
+
}
|
|
14194
|
+
return {
|
|
14195
|
+
commit_sha: sha,
|
|
14196
|
+
tree_hash,
|
|
14197
|
+
timestamp: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
14198
|
+
cli_version: "",
|
|
14199
|
+
generator_versions: {}
|
|
14200
|
+
};
|
|
14201
|
+
}
|
|
14165
14202
|
};
|
|
14166
14203
|
|
|
14167
14204
|
// src/ReplayService.ts
|
|
@@ -16882,8 +16919,7 @@ Patches absorbed by generator (${buckets.absorbed.length}):`;
|
|
|
16882
16919
|
tree_hash: treeHash,
|
|
16883
16920
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16884
16921
|
cli_version: options?.cliVersion ?? "unknown",
|
|
16885
|
-
generator_versions: options?.generatorVersions ?? {}
|
|
16886
|
-
base_branch_head: options?.baseBranchHead
|
|
16922
|
+
generator_versions: options?.generatorVersions ?? {}
|
|
16887
16923
|
};
|
|
16888
16924
|
}
|
|
16889
16925
|
async stageAll() {
|
|
@@ -17262,7 +17298,6 @@ var FirstGenerationFlow = class {
|
|
|
17262
17298
|
flow: "first-generation",
|
|
17263
17299
|
previousGenerationSha: null,
|
|
17264
17300
|
currentGenerationSha: headSha,
|
|
17265
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
17266
17301
|
_prepared: {
|
|
17267
17302
|
kind: "terminal",
|
|
17268
17303
|
flow: "first-generation",
|
|
@@ -17272,8 +17307,7 @@ var FirstGenerationFlow = class {
|
|
|
17272
17307
|
}
|
|
17273
17308
|
const commitOpts = options ? {
|
|
17274
17309
|
cliVersion: options.cliVersion ?? "unknown",
|
|
17275
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
17276
|
-
baseBranchHead: options.baseBranchHead
|
|
17310
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
17277
17311
|
} : void 0;
|
|
17278
17312
|
await this.service.committer.commitGeneration("Initial SDK generation", commitOpts);
|
|
17279
17313
|
const genRecord = await this.service.committer.createGenerationRecord(commitOpts);
|
|
@@ -17282,7 +17316,6 @@ var FirstGenerationFlow = class {
|
|
|
17282
17316
|
flow: "first-generation",
|
|
17283
17317
|
previousGenerationSha: null,
|
|
17284
17318
|
currentGenerationSha: genRecord.commit_sha,
|
|
17285
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
17286
17319
|
_prepared: {
|
|
17287
17320
|
kind: "active",
|
|
17288
17321
|
flow: "first-generation",
|
|
@@ -17319,8 +17352,7 @@ var SkipApplicationFlow = class {
|
|
|
17319
17352
|
async prepare(options) {
|
|
17320
17353
|
const commitOpts = options ? {
|
|
17321
17354
|
cliVersion: options.cliVersion ?? "unknown",
|
|
17322
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
17323
|
-
baseBranchHead: options.baseBranchHead
|
|
17355
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
17324
17356
|
} : void 0;
|
|
17325
17357
|
await this.service.committer.commitGeneration("Update SDK (replay skipped)", commitOpts);
|
|
17326
17358
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -17342,7 +17374,6 @@ var SkipApplicationFlow = class {
|
|
|
17342
17374
|
flow: "skip-application",
|
|
17343
17375
|
previousGenerationSha,
|
|
17344
17376
|
currentGenerationSha: genRecord.commit_sha,
|
|
17345
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
17346
17377
|
_prepared: {
|
|
17347
17378
|
kind: "active",
|
|
17348
17379
|
flow: "skip-application",
|
|
@@ -17403,7 +17434,6 @@ var NoPatchesFlow = class {
|
|
|
17403
17434
|
flow: "no-patches",
|
|
17404
17435
|
previousGenerationSha,
|
|
17405
17436
|
currentGenerationSha: headSha,
|
|
17406
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
17407
17437
|
_prepared: {
|
|
17408
17438
|
kind: "terminal",
|
|
17409
17439
|
flow: "no-patches",
|
|
@@ -17435,8 +17465,7 @@ var NoPatchesFlow = class {
|
|
|
17435
17465
|
}
|
|
17436
17466
|
const commitOpts = options ? {
|
|
17437
17467
|
cliVersion: options.cliVersion ?? "unknown",
|
|
17438
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
17439
|
-
baseBranchHead: options.baseBranchHead
|
|
17468
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
17440
17469
|
} : void 0;
|
|
17441
17470
|
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
17442
17471
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -17446,7 +17475,6 @@ var NoPatchesFlow = class {
|
|
|
17446
17475
|
flow: "no-patches",
|
|
17447
17476
|
previousGenerationSha,
|
|
17448
17477
|
currentGenerationSha: genRecord.commit_sha,
|
|
17449
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
17450
17478
|
_prepared: {
|
|
17451
17479
|
kind: "active",
|
|
17452
17480
|
flow: "no-patches",
|
|
@@ -17536,7 +17564,6 @@ var NormalRegenerationFlow = class {
|
|
|
17536
17564
|
flow: "normal-regeneration",
|
|
17537
17565
|
previousGenerationSha,
|
|
17538
17566
|
currentGenerationSha: headSha,
|
|
17539
|
-
baseBranchHead: options.baseBranchHead ?? null,
|
|
17540
17567
|
_prepared: {
|
|
17541
17568
|
kind: "terminal",
|
|
17542
17569
|
flow: "normal-regeneration",
|
|
@@ -17554,14 +17581,16 @@ var NormalRegenerationFlow = class {
|
|
|
17554
17581
|
existingPatches = this.service.lockManager.getPatches();
|
|
17555
17582
|
const seenHashes = /* @__PURE__ */ new Map();
|
|
17556
17583
|
for (const p of existingPatches) {
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
|
|
17563
|
-
|
|
17564
|
-
|
|
17584
|
+
if (!p.original_commit || p.original_commit === "") {
|
|
17585
|
+
const priorPatchId = seenHashes.get(p.content_hash);
|
|
17586
|
+
if (priorPatchId !== void 0) {
|
|
17587
|
+
this.service.lockManager.removePatch(p.id);
|
|
17588
|
+
this.service.warnings.push(
|
|
17589
|
+
`Consolidated patch ${p.id} into prior patch ${priorPatchId}: byte-identical patch_content. Lockfile collapsed to avoid duplicate-apply corruption on next regen.`
|
|
17590
|
+
);
|
|
17591
|
+
} else {
|
|
17592
|
+
seenHashes.set(p.content_hash, p.id);
|
|
17593
|
+
}
|
|
17565
17594
|
}
|
|
17566
17595
|
}
|
|
17567
17596
|
existingPatches = this.service.lockManager.getPatches();
|
|
@@ -17590,8 +17619,7 @@ var NormalRegenerationFlow = class {
|
|
|
17590
17619
|
const allPatches = [...existingPatches, ...newPatches];
|
|
17591
17620
|
const commitOpts = options ? {
|
|
17592
17621
|
cliVersion: options.cliVersion ?? "unknown",
|
|
17593
|
-
generatorVersions: options.generatorVersions ?? {}
|
|
17594
|
-
baseBranchHead: options.baseBranchHead
|
|
17622
|
+
generatorVersions: options.generatorVersions ?? {}
|
|
17595
17623
|
} : void 0;
|
|
17596
17624
|
await this.service.committer.commitGeneration("Update SDK", commitOpts);
|
|
17597
17625
|
await this.service.cleanupStaleConflictMarkers();
|
|
@@ -17601,7 +17629,6 @@ var NormalRegenerationFlow = class {
|
|
|
17601
17629
|
flow: "normal-regeneration",
|
|
17602
17630
|
previousGenerationSha,
|
|
17603
17631
|
currentGenerationSha: genRecord.commit_sha,
|
|
17604
|
-
baseBranchHead: options?.baseBranchHead ?? null,
|
|
17605
17632
|
_prepared: {
|
|
17606
17633
|
kind: "active",
|
|
17607
17634
|
flow: "normal-regeneration",
|
|
@@ -17794,68 +17821,6 @@ var ReplayService = class {
|
|
|
17794
17821
|
const prep = await this.prepareReplay(options);
|
|
17795
17822
|
return this.applyPreparedReplay(prep, options);
|
|
17796
17823
|
}
|
|
17797
|
-
/**
|
|
17798
|
-
* Sync the lockfile after a divergent PR was squash-merged.
|
|
17799
|
-
* Call this BEFORE runReplay() when the CLI detects a merged divergent PR.
|
|
17800
|
-
*
|
|
17801
|
-
* After updating the generation record, re-detects customer patches via
|
|
17802
|
-
* tree diff between the generation tag (pure generation tree) and HEAD
|
|
17803
|
-
* (which includes customer customizations after squash merge). This ensures
|
|
17804
|
-
* patches survive the squash merge → regenerate cycle even when the lockfile
|
|
17805
|
-
* was restored from the base branch during conflict PR creation.
|
|
17806
|
-
*/
|
|
17807
|
-
async syncFromDivergentMerge(generationCommitSha, options) {
|
|
17808
|
-
const treeHash = await this.git.getTreeHash(generationCommitSha);
|
|
17809
|
-
const timestamp = (await this.git.exec(["log", "-1", "--format=%aI", generationCommitSha])).trim();
|
|
17810
|
-
const record = {
|
|
17811
|
-
commit_sha: generationCommitSha,
|
|
17812
|
-
tree_hash: treeHash,
|
|
17813
|
-
timestamp,
|
|
17814
|
-
cli_version: options?.cliVersion ?? "unknown",
|
|
17815
|
-
generator_versions: options?.generatorVersions ?? {},
|
|
17816
|
-
base_branch_head: options?.baseBranchHead
|
|
17817
|
-
};
|
|
17818
|
-
let resolvedPatches;
|
|
17819
|
-
if (!this.lockManager.exists()) {
|
|
17820
|
-
this.lockManager.initializeInMemory(record);
|
|
17821
|
-
} else {
|
|
17822
|
-
this.lockManager.read();
|
|
17823
|
-
const unresolvedPatches = [
|
|
17824
|
-
...this.lockManager.getUnresolvedPatches(),
|
|
17825
|
-
...this.lockManager.getResolvingPatches()
|
|
17826
|
-
];
|
|
17827
|
-
resolvedPatches = this.lockManager.getPatches().filter((p) => p.status == null);
|
|
17828
|
-
this.lockManager.addGeneration(record);
|
|
17829
|
-
this.lockManager.clearPatches();
|
|
17830
|
-
for (const patch of unresolvedPatches) {
|
|
17831
|
-
this.lockManager.addPatch(patch);
|
|
17832
|
-
}
|
|
17833
|
-
}
|
|
17834
|
-
try {
|
|
17835
|
-
const { patches: redetectedPatches } = await this.detector.detectNewPatches();
|
|
17836
|
-
if (redetectedPatches.length > 0) {
|
|
17837
|
-
const redetectedFiles = new Set(redetectedPatches.flatMap((p) => p.files));
|
|
17838
|
-
const currentPatches = this.lockManager.getPatches();
|
|
17839
|
-
for (const patch of currentPatches) {
|
|
17840
|
-
if (patch.status != null && patch.files.some((f) => redetectedFiles.has(f))) {
|
|
17841
|
-
this.lockManager.removePatch(patch.id);
|
|
17842
|
-
}
|
|
17843
|
-
}
|
|
17844
|
-
for (const patch of redetectedPatches) {
|
|
17845
|
-
this.lockManager.addPatch(patch);
|
|
17846
|
-
}
|
|
17847
|
-
}
|
|
17848
|
-
} catch (error) {
|
|
17849
|
-
for (const patch of resolvedPatches ?? []) {
|
|
17850
|
-
this.lockManager.addPatch(patch);
|
|
17851
|
-
}
|
|
17852
|
-
this.detector.warnings.push(
|
|
17853
|
-
`Patch re-detection failed after divergent merge sync. ${(resolvedPatches ?? []).length} previously resolved patch(es) preserved. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
17854
|
-
);
|
|
17855
|
-
}
|
|
17856
|
-
this.lockManager.save();
|
|
17857
|
-
this.detector.warnings.push(...this.lockManager.warnings);
|
|
17858
|
-
}
|
|
17859
17824
|
determineFlow() {
|
|
17860
17825
|
try {
|
|
17861
17826
|
const lock = this.lockManager.read();
|