@fern-api/generator-cli 0.9.44 → 0.9.46
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.js +243 -14
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -41018,7 +41018,8 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
41018
41018
|
contentLines.push(line);
|
|
41019
41019
|
}
|
|
41020
41020
|
}
|
|
41021
|
-
let processedContent = this.
|
|
41021
|
+
let processedContent = this.removeGoModulePathSuffixPairs(contentLines);
|
|
41022
|
+
processedContent = this.removeVersionChangePairs(processedContent, mappedMagicVersion);
|
|
41022
41023
|
processedContent = this.removeRemainingMagicVersionLines(processedContent, mappedMagicVersion);
|
|
41023
41024
|
let hasChanges = false;
|
|
41024
41025
|
for (const line of processedContent) {
|
|
@@ -41154,21 +41155,85 @@ var AutoVersioningService = class _AutoVersioningService {
|
|
|
41154
41155
|
* so non-Go diffs like API URL version changes are not affected.
|
|
41155
41156
|
*/
|
|
41156
41157
|
isGoModulePathSuffixChange(minusLine, plusLine) {
|
|
41157
|
-
const
|
|
41158
|
-
const
|
|
41159
|
-
|
|
41160
|
-
const goModuleDirectivePattern = /^\s*module\s+(?:github\.com|golang\.org|google\.golang\.org|gopkg\.in)\/\S+/;
|
|
41161
|
-
const hasGoModulePath = goModulePathPattern.test(minusContent) || goModulePathPattern.test(plusContent) || goModuleDirectivePattern.test(minusContent.trim()) || goModuleDirectivePattern.test(plusContent.trim());
|
|
41162
|
-
if (!hasGoModulePath) {
|
|
41158
|
+
const minus = this.analyzeGoModulePathLine(minusLine);
|
|
41159
|
+
const plus = this.analyzeGoModulePathLine(plusLine);
|
|
41160
|
+
if (minus == null || plus == null) {
|
|
41163
41161
|
return false;
|
|
41164
41162
|
}
|
|
41165
|
-
|
|
41166
|
-
const minusWithoutSuffix = minusContent.replace(suffixPattern, "");
|
|
41167
|
-
const plusWithoutSuffix = plusContent.replace(suffixPattern, "");
|
|
41168
|
-
if (minusWithoutSuffix !== plusWithoutSuffix) {
|
|
41163
|
+
if (minus.strippedContent !== plus.strippedContent) {
|
|
41169
41164
|
return false;
|
|
41170
41165
|
}
|
|
41171
|
-
return
|
|
41166
|
+
return minus.hasSuffix || plus.hasSuffix;
|
|
41167
|
+
}
|
|
41168
|
+
/**
|
|
41169
|
+
* Removes Go module path `/vN` suffix-only churn from a diff section, regardless of how the
|
|
41170
|
+
* deletion and addition lines are laid out. `removeVersionChangePairs` only removes a deletion
|
|
41171
|
+
* when it can pair it with a *nearby* addition and keeps every line in between — so a diff that
|
|
41172
|
+
* groups all deletions before all additions (as Go import blocks do) leaks the suffix churn for
|
|
41173
|
+
* every line except the first. That churn then reaches the AI diff analyzer, which sees the
|
|
41174
|
+
* previously published `/vN` module path changing on every import and misclassifies a
|
|
41175
|
+
* no-op regeneration as a breaking change (spurious major bump).
|
|
41176
|
+
*
|
|
41177
|
+
* This pass matches each suffix-bearing deletion with any suffix-only-equivalent addition in
|
|
41178
|
+
* the section (by suffix-stripped content) and drops both, so only genuine content changes to
|
|
41179
|
+
* Go module path lines survive.
|
|
41180
|
+
*/
|
|
41181
|
+
removeGoModulePathSuffixPairs(lines) {
|
|
41182
|
+
const additions = [];
|
|
41183
|
+
for (let index = 0; index < lines.length; index++) {
|
|
41184
|
+
const line = lines[index];
|
|
41185
|
+
if (line == null || !this.isAdditionLine(line)) {
|
|
41186
|
+
continue;
|
|
41187
|
+
}
|
|
41188
|
+
const info = this.analyzeGoModulePathLine(line);
|
|
41189
|
+
if (info != null) {
|
|
41190
|
+
additions.push({ index, strippedContent: info.strippedContent });
|
|
41191
|
+
}
|
|
41192
|
+
}
|
|
41193
|
+
const removed = /* @__PURE__ */ new Set();
|
|
41194
|
+
const usedAdditions = /* @__PURE__ */ new Set();
|
|
41195
|
+
for (let index = 0; index < lines.length; index++) {
|
|
41196
|
+
const line = lines[index];
|
|
41197
|
+
if (line == null || !this.isDeletionLine(line)) {
|
|
41198
|
+
continue;
|
|
41199
|
+
}
|
|
41200
|
+
const info = this.analyzeGoModulePathLine(line);
|
|
41201
|
+
if (info == null || !info.hasSuffix) {
|
|
41202
|
+
continue;
|
|
41203
|
+
}
|
|
41204
|
+
const match2 = additions.find(
|
|
41205
|
+
(addition) => !usedAdditions.has(addition.index) && addition.strippedContent === info.strippedContent
|
|
41206
|
+
);
|
|
41207
|
+
if (match2 != null) {
|
|
41208
|
+
usedAdditions.add(match2.index);
|
|
41209
|
+
removed.add(index);
|
|
41210
|
+
removed.add(match2.index);
|
|
41211
|
+
}
|
|
41212
|
+
}
|
|
41213
|
+
if (removed.size === 0) {
|
|
41214
|
+
return lines;
|
|
41215
|
+
}
|
|
41216
|
+
return lines.filter((_2, index) => !removed.has(index));
|
|
41217
|
+
}
|
|
41218
|
+
/**
|
|
41219
|
+
* If a diff line (with its leading +/- stripped) references a Go module path — either a quoted
|
|
41220
|
+
* import path or a `module` directive — returns its content with any `/vN` module-version
|
|
41221
|
+
* suffixes removed, plus whether a suffix was present. Returns undefined for non-Go lines so
|
|
41222
|
+
* unrelated diffs (e.g. API URL version segments) are never touched.
|
|
41223
|
+
*/
|
|
41224
|
+
analyzeGoModulePathLine(line) {
|
|
41225
|
+
const content = line.substring(1);
|
|
41226
|
+
const goModulePathPattern = /"[^"]*(?:github\.com|golang\.org|google\.golang\.org|gopkg\.in)\/[^"]*"/;
|
|
41227
|
+
const goModuleDirectivePattern = /^\s*module\s+(?:github\.com|golang\.org|google\.golang\.org|gopkg\.in)\/\S+/;
|
|
41228
|
+
const isGoModulePathLine = goModulePathPattern.test(content) || goModuleDirectivePattern.test(content.trim());
|
|
41229
|
+
if (!isGoModulePathLine) {
|
|
41230
|
+
return void 0;
|
|
41231
|
+
}
|
|
41232
|
+
const suffixPattern = /\/v\d+(?=\/|"|$)/g;
|
|
41233
|
+
return {
|
|
41234
|
+
strippedContent: content.replace(suffixPattern, ""),
|
|
41235
|
+
hasSuffix: suffixPattern.test(content)
|
|
41236
|
+
};
|
|
41172
41237
|
}
|
|
41173
41238
|
/**
|
|
41174
41239
|
* Gets the latest semantic version from git tags in the given repository.
|
|
@@ -41591,7 +41656,8 @@ var AutoVersionStep = class extends BaseStep {
|
|
|
41591
41656
|
}
|
|
41592
41657
|
async handleNormalFlow(params) {
|
|
41593
41658
|
const { prepared, service, language, mappedMagicVersion, previousGenerationSha } = params;
|
|
41594
|
-
const
|
|
41659
|
+
const diffBase = this.resolveReachableGenerationBase(previousGenerationSha, prepared.currentGenerationSha);
|
|
41660
|
+
const rawDiff = diffBase != null ? this.safeGitDiff(diffBase, prepared.currentGenerationSha) : "";
|
|
41595
41661
|
const previousVersion = await this.resolvePreviousVersion({
|
|
41596
41662
|
service,
|
|
41597
41663
|
rawDiff,
|
|
@@ -42014,6 +42080,97 @@ var AutoVersionStep = class extends BaseStep {
|
|
|
42014
42080
|
maxBuffer: 256 * 1024 * 1024
|
|
42015
42081
|
});
|
|
42016
42082
|
}
|
|
42083
|
+
/**
|
|
42084
|
+
* Resolves a reachable base commit for the autoversion diff.
|
|
42085
|
+
*
|
|
42086
|
+
* `recordedSha` (the previous generation from replay.lock) is preferred when it exists
|
|
42087
|
+
* in this clone. When it doesn't — e.g. the signed-commit push recreated the
|
|
42088
|
+
* [fern-generated] commit under a new SHA, or the branch was squash-merged — we re-anchor
|
|
42089
|
+
* on the most recent reachable [fern-generated] commit in history. Returns null when no
|
|
42090
|
+
* generation baseline is reachable at all, in which case the caller resolves the previous
|
|
42091
|
+
* version from metadata/git-tags and still rewrites the placeholder.
|
|
42092
|
+
*/
|
|
42093
|
+
resolveReachableGenerationBase(recordedSha, currentGenerationSha) {
|
|
42094
|
+
if (this.commitExists(recordedSha)) {
|
|
42095
|
+
return recordedSha;
|
|
42096
|
+
}
|
|
42097
|
+
this.logger.warn(
|
|
42098
|
+
`AutoVersionStep: recorded previous generation ${recordedSha.slice(0, 7)} is unreachable in this clone; deriving the baseline from history.`
|
|
42099
|
+
);
|
|
42100
|
+
const derived = this.findPreviousGenerationFromHistory(currentGenerationSha);
|
|
42101
|
+
if (derived != null) {
|
|
42102
|
+
this.logger.info(`AutoVersionStep: re-anchored autoversion baseline on ${derived.slice(0, 7)}.`);
|
|
42103
|
+
return derived;
|
|
42104
|
+
}
|
|
42105
|
+
this.logger.warn(
|
|
42106
|
+
"AutoVersionStep: no reachable [fern-generated] baseline found; falling back to metadata/git-tags."
|
|
42107
|
+
);
|
|
42108
|
+
return null;
|
|
42109
|
+
}
|
|
42110
|
+
commitExists(sha) {
|
|
42111
|
+
if (!/^[0-9a-f]{7,40}$/.test(sha)) {
|
|
42112
|
+
return false;
|
|
42113
|
+
}
|
|
42114
|
+
try {
|
|
42115
|
+
(0, import_child_process4.execFileSync)("git", ["cat-file", "-e", `${sha}^{commit}`], {
|
|
42116
|
+
cwd: this.outputDir,
|
|
42117
|
+
stdio: "pipe"
|
|
42118
|
+
});
|
|
42119
|
+
return true;
|
|
42120
|
+
} catch {
|
|
42121
|
+
return false;
|
|
42122
|
+
}
|
|
42123
|
+
}
|
|
42124
|
+
/**
|
|
42125
|
+
* Walks first-parent history from the current generation commit and returns the most
|
|
42126
|
+
* recent prior [fern-generated] commit — the reachable equivalent of replay.lock's
|
|
42127
|
+
* recorded baseline. Mirrors @fern-api/replay's `findPreviousGenerationFromHistory`.
|
|
42128
|
+
*/
|
|
42129
|
+
findPreviousGenerationFromHistory(currentGenerationSha) {
|
|
42130
|
+
const start = this.commitExists(currentGenerationSha) ? currentGenerationSha : "HEAD";
|
|
42131
|
+
let log;
|
|
42132
|
+
try {
|
|
42133
|
+
log = (0, import_child_process4.execFileSync)("git", ["log", "--first-parent", "--format=%H%x00%s", start], {
|
|
42134
|
+
cwd: this.outputDir,
|
|
42135
|
+
encoding: "utf-8",
|
|
42136
|
+
stdio: "pipe",
|
|
42137
|
+
maxBuffer: 64 * 1024 * 1024
|
|
42138
|
+
});
|
|
42139
|
+
} catch (error) {
|
|
42140
|
+
this.logger.debug(`AutoVersionStep: git log history walk failed (${String(error)}); no baseline derived.`);
|
|
42141
|
+
return null;
|
|
42142
|
+
}
|
|
42143
|
+
for (const line of log.split("\n")) {
|
|
42144
|
+
if (line.trim().length === 0) {
|
|
42145
|
+
continue;
|
|
42146
|
+
}
|
|
42147
|
+
const [sha, subject = ""] = line.split("\0");
|
|
42148
|
+
if (sha == null || sha === currentGenerationSha) {
|
|
42149
|
+
continue;
|
|
42150
|
+
}
|
|
42151
|
+
if (subject.startsWith("[fern-generated]")) {
|
|
42152
|
+
return sha;
|
|
42153
|
+
}
|
|
42154
|
+
}
|
|
42155
|
+
return null;
|
|
42156
|
+
}
|
|
42157
|
+
/**
|
|
42158
|
+
* `gitDiff` that degrades to an empty diff instead of throwing. Both endpoints are
|
|
42159
|
+
* expected to be reachable (the base is resolved via resolveReachableGenerationBase and
|
|
42160
|
+
* `to` is this run's freshly-created HEAD), but we never want a `git diff` failure to
|
|
42161
|
+
* abort autoversion and leave the magic placeholder in the shipped SDK — the empty diff
|
|
42162
|
+
* routes version resolution to baseVersion/metadata/git-tags, which still rewrites it.
|
|
42163
|
+
*/
|
|
42164
|
+
safeGitDiff(from, to) {
|
|
42165
|
+
try {
|
|
42166
|
+
return this.gitDiff(from, to);
|
|
42167
|
+
} catch (error) {
|
|
42168
|
+
this.logger.warn(
|
|
42169
|
+
`AutoVersionStep: git diff ${from.slice(0, 7)}..${to.slice(0, 7)} failed (${error instanceof Error ? error.message : String(error)}); proceeding with an empty diff.`
|
|
42170
|
+
);
|
|
42171
|
+
return "";
|
|
42172
|
+
}
|
|
42173
|
+
}
|
|
42017
42174
|
gitDiff(from, to) {
|
|
42018
42175
|
return (0, import_child_process4.execFileSync)("git", ["diff", from, to, "--", ".", ":(exclude).fern/metadata.json"], {
|
|
42019
42176
|
cwd: this.outputDir,
|
|
@@ -42846,11 +43003,16 @@ var GithubStep = class extends BaseStep {
|
|
|
42846
43003
|
throw new Error(`Unexpected branch action: ${String(_exhaustive)}`);
|
|
42847
43004
|
}
|
|
42848
43005
|
}
|
|
43006
|
+
const createdChangelog = await this.ensureChangelogFile(resolved);
|
|
42849
43007
|
if (!skipCommit) {
|
|
42850
43008
|
await this.ensureFernignore();
|
|
42851
43009
|
this.logger.debug("Committing changes...");
|
|
42852
43010
|
await repository.commitAllChanges(resolved.commitMessage);
|
|
42853
43011
|
this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
|
|
43012
|
+
} else if (createdChangelog) {
|
|
43013
|
+
this.logger.debug("Committing reconstructed changelog.md on top of the replay commit...");
|
|
43014
|
+
await repository.add("changelog.md");
|
|
43015
|
+
await repository.commit(resolved.commitMessage);
|
|
42854
43016
|
}
|
|
42855
43017
|
if (shouldCheckNoDiff(this.config)) {
|
|
42856
43018
|
const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
|
|
@@ -42891,7 +43053,20 @@ var GithubStep = class extends BaseStep {
|
|
|
42891
43053
|
}
|
|
42892
43054
|
}
|
|
42893
43055
|
const headSha = await repository.getHeadSha();
|
|
42894
|
-
const
|
|
43056
|
+
const changelogCommitted = (await repository.listTrackedFiles()).includes("changelog.md");
|
|
43057
|
+
const changelogUrl = resolveChangelogUrl({
|
|
43058
|
+
changelogEntry: resolved.changelogEntry,
|
|
43059
|
+
changelogCommitted,
|
|
43060
|
+
remote,
|
|
43061
|
+
owner,
|
|
43062
|
+
repo,
|
|
43063
|
+
headSha
|
|
43064
|
+
});
|
|
43065
|
+
if (resolved.changelogEntry && !changelogCommitted) {
|
|
43066
|
+
this.logger.warn(
|
|
43067
|
+
'changelog.md is not present in the committed tree; omitting the "See full changelog" link.'
|
|
43068
|
+
);
|
|
43069
|
+
}
|
|
42895
43070
|
const { prTitle, prBody } = parseCommitMessageForPR(
|
|
42896
43071
|
resolved.commitMessage,
|
|
42897
43072
|
resolved.changelogEntry,
|
|
@@ -42969,6 +43144,7 @@ var GithubStep = class extends BaseStep {
|
|
|
42969
43144
|
this.logger.debug(`Checking out branch ${this.config.branch}`);
|
|
42970
43145
|
await repository.checkout(this.config.branch);
|
|
42971
43146
|
}
|
|
43147
|
+
await this.ensureChangelogFile(resolved);
|
|
42972
43148
|
await this.ensureFernignore();
|
|
42973
43149
|
this.logger.debug("Committing changes...");
|
|
42974
43150
|
await repository.commitAllChanges(resolved.commitMessage);
|
|
@@ -43010,6 +43186,7 @@ var GithubStep = class extends BaseStep {
|
|
|
43010
43186
|
this.logger.debug(`Checking out branch ${this.config.branch}`);
|
|
43011
43187
|
await repository.checkout(this.config.branch);
|
|
43012
43188
|
}
|
|
43189
|
+
await this.ensureChangelogFile(resolved);
|
|
43013
43190
|
await this.ensureFernignore();
|
|
43014
43191
|
this.logger.debug("Committing changes...");
|
|
43015
43192
|
await repository.commitAllChanges(resolved.commitMessage);
|
|
@@ -43087,6 +43264,33 @@ var GithubStep = class extends BaseStep {
|
|
|
43087
43264
|
await (0, import_promises16.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
|
|
43088
43265
|
}
|
|
43089
43266
|
}
|
|
43267
|
+
/**
|
|
43268
|
+
* Guarantees `changelog.md` is present in the output directory whenever a changelog entry
|
|
43269
|
+
* exists for this run, so the file lands in the committed tree that GithubStep pushes and the
|
|
43270
|
+
* "See full changelog" link resolves. AutoVersionStep normally writes this file via
|
|
43271
|
+
* `prependChangelogEntry`; this is a safety net for paths where that write never reached the
|
|
43272
|
+
* directory GithubStep commits (e.g. non-replay self-hosted generation), which previously left
|
|
43273
|
+
* the link pointing at a non-existent blob (404).
|
|
43274
|
+
*
|
|
43275
|
+
* Returns `true` when it created the file, `false` when the file already existed (or there is
|
|
43276
|
+
* no changelog entry to write).
|
|
43277
|
+
*/
|
|
43278
|
+
async ensureChangelogFile(resolved) {
|
|
43279
|
+
const entry = resolved.changelogEntry?.trim();
|
|
43280
|
+
if (entry == null || entry.length === 0) {
|
|
43281
|
+
return false;
|
|
43282
|
+
}
|
|
43283
|
+
const changelogPath = (0, import_path24.join)(this.outputDir, "changelog.md");
|
|
43284
|
+
try {
|
|
43285
|
+
await (0, import_promises16.access)(changelogPath);
|
|
43286
|
+
return false;
|
|
43287
|
+
} catch {
|
|
43288
|
+
const version2 = resolved.newVersion ?? resolved.previousVersion;
|
|
43289
|
+
await (0, import_promises16.writeFile)(changelogPath, buildChangelogFileContents(entry, version2), "utf-8");
|
|
43290
|
+
this.logger.debug(`Wrote changelog.md${version2 != null ? ` for ${version2}` : ""}.`);
|
|
43291
|
+
return true;
|
|
43292
|
+
}
|
|
43293
|
+
}
|
|
43090
43294
|
deriveSkipCommit(replayResult) {
|
|
43091
43295
|
if (replayResult == null) {
|
|
43092
43296
|
return false;
|
|
@@ -43170,6 +43374,31 @@ function resolveBranchAction({
|
|
|
43170
43374
|
}
|
|
43171
43375
|
return "create-from-head";
|
|
43172
43376
|
}
|
|
43377
|
+
function buildChangelogFileContents(entry, version2) {
|
|
43378
|
+
const trimmed2 = entry.trim();
|
|
43379
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
43380
|
+
const header = version2 != null ? `## [${version2}] - ${now}
|
|
43381
|
+
` : `## ${now}
|
|
43382
|
+
`;
|
|
43383
|
+
return `# Changelog
|
|
43384
|
+
|
|
43385
|
+
${header}${trimmed2}
|
|
43386
|
+
|
|
43387
|
+
`;
|
|
43388
|
+
}
|
|
43389
|
+
function resolveChangelogUrl({
|
|
43390
|
+
changelogEntry,
|
|
43391
|
+
changelogCommitted,
|
|
43392
|
+
remote,
|
|
43393
|
+
owner,
|
|
43394
|
+
repo,
|
|
43395
|
+
headSha
|
|
43396
|
+
}) {
|
|
43397
|
+
if (changelogEntry == null || changelogEntry.trim().length === 0 || !changelogCommitted) {
|
|
43398
|
+
return void 0;
|
|
43399
|
+
}
|
|
43400
|
+
return `https://${remote}/${owner}/${repo}/blob/${headSha}/changelog.md`;
|
|
43401
|
+
}
|
|
43173
43402
|
|
|
43174
43403
|
// src/pipeline/steps/ReplayStep.ts
|
|
43175
43404
|
var import_child_process6 = require("child_process");
|