@fern-api/generator-cli 0.9.10 → 0.9.13
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 +178 -30
- package/dist/api.js +2106 -423
- package/dist/cli.js +10916 -4636
- package/package.json +4 -2
package/dist/api.js
CHANGED
|
@@ -793,12 +793,12 @@ var require_src2 = __commonJS({
|
|
|
793
793
|
function check(path3, isFile, isDirectory) {
|
|
794
794
|
log(`checking %s`, path3);
|
|
795
795
|
try {
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
796
|
+
const stat2 = fs_1.statSync(path3);
|
|
797
|
+
if (stat2.isFile() && isFile) {
|
|
798
798
|
log(`[OK] path represents a file`);
|
|
799
799
|
return true;
|
|
800
800
|
}
|
|
801
|
-
if (
|
|
801
|
+
if (stat2.isDirectory() && isDirectory) {
|
|
802
802
|
log(`[OK] path represents a directory`);
|
|
803
803
|
return true;
|
|
804
804
|
}
|
|
@@ -881,6 +881,7 @@ var require_dist2 = __commonJS({
|
|
|
881
881
|
var api_exports2 = {};
|
|
882
882
|
__export(api_exports2, {
|
|
883
883
|
PostGenerationPipeline: () => PostGenerationPipeline,
|
|
884
|
+
ReplaySubmitError: () => ReplaySubmitError,
|
|
884
885
|
consolePipelineLogger: () => consolePipelineLogger,
|
|
885
886
|
formatBootstrapSummary: () => formatBootstrapSummary,
|
|
886
887
|
formatReplayPrBody: () => formatReplayPrBody,
|
|
@@ -892,11 +893,13 @@ __export(api_exports2, {
|
|
|
892
893
|
githubPush: () => githubPush,
|
|
893
894
|
githubRelease: () => githubRelease,
|
|
894
895
|
logReplaySummary: () => logReplaySummary,
|
|
896
|
+
parseOwnerRepo: () => parseOwnerRepo,
|
|
895
897
|
replayForget: () => replayForget,
|
|
896
898
|
replayInit: () => replayInit,
|
|
897
899
|
replayResolve: () => replayResolve,
|
|
898
900
|
replayRun: () => replayRun,
|
|
899
|
-
replayStatus: () => replayStatus
|
|
901
|
+
replayStatus: () => replayStatus,
|
|
902
|
+
submitReplayInit: () => submitReplayInit
|
|
900
903
|
});
|
|
901
904
|
module.exports = __toCommonJS(api_exports2);
|
|
902
905
|
|
|
@@ -6017,6 +6020,46 @@ var ClonedRepository = class _ClonedRepository {
|
|
|
6017
6020
|
const result = await this.git.raw(["rev-parse", "HEAD^{tree}"]);
|
|
6018
6021
|
return result.trim();
|
|
6019
6022
|
}
|
|
6023
|
+
async getHeadCommitMessage() {
|
|
6024
|
+
await this.git.cwd(this.clonePath);
|
|
6025
|
+
const result = await this.git.raw(["log", "-1", "--format=%B", "HEAD"]);
|
|
6026
|
+
return result.replace(/\n$/, "");
|
|
6027
|
+
}
|
|
6028
|
+
async getHeadParents() {
|
|
6029
|
+
await this.git.cwd(this.clonePath);
|
|
6030
|
+
const result = await this.git.raw(["rev-list", "--parents", "-n", "1", "HEAD"]);
|
|
6031
|
+
const parts = result.trim().split(/\s+/);
|
|
6032
|
+
return parts.slice(1);
|
|
6033
|
+
}
|
|
6034
|
+
/**
|
|
6035
|
+
* Pushes a specific object (commit SHA) to a remote ref.
|
|
6036
|
+
* Used to upload tree and blob objects to the remote without moving a branch.
|
|
6037
|
+
*
|
|
6038
|
+
* Pass `force: true` when overwriting a ref that may already point to an
|
|
6039
|
+
* unrelated commit (e.g. retrying a temp ref after a local rebase rewrites
|
|
6040
|
+
* history so the new SHA is not a descendant of the previous one).
|
|
6041
|
+
*/
|
|
6042
|
+
async pushObjectToRef(localSha, remoteRef, { force = false } = {}) {
|
|
6043
|
+
await this.git.cwd(this.clonePath);
|
|
6044
|
+
const spec = force ? `+${localSha}:${remoteRef}` : `${localSha}:${remoteRef}`;
|
|
6045
|
+
await this.git.raw(["push", "origin", spec]);
|
|
6046
|
+
}
|
|
6047
|
+
/**
|
|
6048
|
+
* Resets the current branch HEAD to the given SHA. After fetching a newly-created remote
|
|
6049
|
+
* signed commit, use this to align the local branch so subsequent operations (tag push,
|
|
6050
|
+
* getHeadSha(), etc.) see the signed SHA.
|
|
6051
|
+
*/
|
|
6052
|
+
async resetHardToSha(sha) {
|
|
6053
|
+
await this.git.cwd(this.clonePath);
|
|
6054
|
+
await this.git.raw(["reset", "--hard", sha]);
|
|
6055
|
+
}
|
|
6056
|
+
/**
|
|
6057
|
+
* Rebases the current branch onto the given remote branch (`git pull --rebase origin <branch>`).
|
|
6058
|
+
*/
|
|
6059
|
+
async pullWithRebase(branch) {
|
|
6060
|
+
await this.git.cwd(this.clonePath);
|
|
6061
|
+
await this.git.raw(["pull", "origin", branch, "--rebase"]);
|
|
6062
|
+
}
|
|
6020
6063
|
async getCommitTreeHash(commitSha) {
|
|
6021
6064
|
await this.git.cwd(this.clonePath);
|
|
6022
6065
|
const result = await this.git.raw(["rev-parse", `${commitSha}^{tree}`]);
|
|
@@ -10017,177 +10060,1944 @@ var consolePipelineLogger = {
|
|
|
10017
10060
|
};
|
|
10018
10061
|
|
|
10019
10062
|
// src/pipeline/PostGenerationPipeline.ts
|
|
10020
|
-
var
|
|
10063
|
+
var import_child_process5 = require("child_process");
|
|
10021
10064
|
|
|
10022
10065
|
// src/pipeline/github/constants.ts
|
|
10023
10066
|
var FERN_BOT_NAME = "fern-api";
|
|
10024
10067
|
var FERN_BOT_EMAIL = "115122769+fern-api[bot]@users.noreply.github.com";
|
|
10025
10068
|
var FERN_BOT_LOGIN = "fern-api[bot]";
|
|
10026
10069
|
|
|
10027
|
-
// src/pipeline/steps/
|
|
10028
|
-
var
|
|
10070
|
+
// src/pipeline/steps/AutoVersionStep.ts
|
|
10071
|
+
var import_child_process3 = require("child_process");
|
|
10072
|
+
var import_fs3 = require("fs");
|
|
10073
|
+
var import_promises3 = require("fs/promises");
|
|
10074
|
+
var import_path4 = require("path");
|
|
10075
|
+
|
|
10076
|
+
// src/autoversion/AutoVersioningService.ts
|
|
10077
|
+
var import_logging_execa = require("@fern-api/logging-execa");
|
|
10078
|
+
var import_fs2 = require("fs");
|
|
10029
10079
|
var import_promises2 = require("fs/promises");
|
|
10030
10080
|
var import_path3 = require("path");
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10081
|
+
var import_semver = __toESM(require("semver"));
|
|
10082
|
+
var AutoVersioningException = class extends Error {
|
|
10083
|
+
/**
|
|
10084
|
+
* True when the magic version was not found at all in the diff,
|
|
10085
|
+
* indicating a generator that doesn't embed versions in source files
|
|
10086
|
+
* (e.g., Swift uses git tags via SPM). Only this case should fall back
|
|
10087
|
+
* to reading git tags.
|
|
10088
|
+
*/
|
|
10089
|
+
magicVersionAbsent;
|
|
10090
|
+
constructor(message, options) {
|
|
10091
|
+
super(message);
|
|
10092
|
+
this.name = "AutoVersioningException";
|
|
10093
|
+
this.magicVersionAbsent = options?.magicVersionAbsent ?? false;
|
|
10094
|
+
if (options?.cause) {
|
|
10095
|
+
this.cause = options.cause;
|
|
10096
|
+
}
|
|
10037
10097
|
}
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
const
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
}
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10098
|
+
};
|
|
10099
|
+
function countFilesInDiff(diffContent) {
|
|
10100
|
+
const marker = "diff --git ";
|
|
10101
|
+
let count = 0;
|
|
10102
|
+
let pos = 0;
|
|
10103
|
+
while (true) {
|
|
10104
|
+
const idx = diffContent.indexOf(marker, pos);
|
|
10105
|
+
if (idx === -1) {
|
|
10106
|
+
break;
|
|
10107
|
+
}
|
|
10108
|
+
if (idx === 0 || diffContent[idx - 1] === "\n") {
|
|
10109
|
+
count++;
|
|
10110
|
+
}
|
|
10111
|
+
pos = idx + marker.length;
|
|
10112
|
+
}
|
|
10113
|
+
return count;
|
|
10114
|
+
}
|
|
10115
|
+
function formatSizeKB(charLength) {
|
|
10116
|
+
return (charLength / 1024).toFixed(1);
|
|
10117
|
+
}
|
|
10118
|
+
var EXCLUDED_FILE_PATTERNS = [
|
|
10119
|
+
// Documentation / generated reference
|
|
10120
|
+
/(?:^|\/)reference\.md$/i,
|
|
10121
|
+
/(?:^|\/)changelog(?:\.[^/]*)?$/i,
|
|
10122
|
+
/(?:^|\/)readme(?:\.[^/]*)?$/i,
|
|
10123
|
+
/(?:^|\/)license(?:\.[^/]*)?$/i,
|
|
10124
|
+
// Lock files (dependency resolution, zero semantic value)
|
|
10125
|
+
/(?:^|\/)pnpm-lock\.yaml$/i,
|
|
10126
|
+
/(?:^|\/)yarn\.lock$/i,
|
|
10127
|
+
/(?:^|\/)package-lock\.json$/i,
|
|
10128
|
+
/(?:^|\/)poetry\.lock$/i,
|
|
10129
|
+
/(?:^|\/)gemfile\.lock$/i,
|
|
10130
|
+
/(?:^|\/)go\.sum$/i,
|
|
10131
|
+
/(?:^|\/)cargo\.lock$/i,
|
|
10132
|
+
/(?:^|\/)composer\.lock$/i,
|
|
10133
|
+
/\.lock$/i,
|
|
10134
|
+
// catch-all for any other lockfiles
|
|
10135
|
+
// Test files (by naming convention only — directory patterns like tests/ and
|
|
10136
|
+
// test/ are intentionally omitted because a customer's API domain could use
|
|
10137
|
+
// those names, e.g. a QA platform SDK with a "tests" resource)
|
|
10138
|
+
/\.test\.ts$/i,
|
|
10139
|
+
/\.test\.js$/i,
|
|
10140
|
+
/\.test\.py$/i,
|
|
10141
|
+
/\.spec\.ts$/i,
|
|
10142
|
+
/\.spec\.js$/i,
|
|
10143
|
+
/_test\.go$/i,
|
|
10144
|
+
/_test\.py$/i,
|
|
10145
|
+
/Test\.java$/,
|
|
10146
|
+
/(?:^|\/)__tests__\//i,
|
|
10147
|
+
// __tests__/ is unambiguously a test directory
|
|
10148
|
+
/(?:^|\/)wiremock\//i,
|
|
10149
|
+
// Fern-generated WireMock test fixtures
|
|
10150
|
+
// Snapshot files
|
|
10151
|
+
/(?:^|\/)__snapshots__\//i,
|
|
10152
|
+
/\.snap$/i,
|
|
10153
|
+
// CI / editor config (no API surface relevance)
|
|
10154
|
+
/(?:^|\/)\.github\//i,
|
|
10155
|
+
/(?:^|\/)\.circleci\//i,
|
|
10156
|
+
/(?:^|\/)\.editorconfig$/i,
|
|
10157
|
+
/(?:^|\/)\.prettierrc/i,
|
|
10158
|
+
/(?:^|\/)biome\.json$/i,
|
|
10159
|
+
// Linting / static analysis config
|
|
10160
|
+
/(?:^|\/)\.eslintrc/i,
|
|
10161
|
+
/(?:^|\/)eslint\.config\./i,
|
|
10162
|
+
/(?:^|\/)\.rubocop/i,
|
|
10163
|
+
// Ruby linter
|
|
10164
|
+
/(?:^|\/)phpstan\.neon$/i,
|
|
10165
|
+
// PHP static analysis
|
|
10166
|
+
/(?:^|\/)phpunit\.xml$/i,
|
|
10167
|
+
// PHP test runner config
|
|
10168
|
+
/(?:^|\/)\.pylintrc$/i,
|
|
10169
|
+
// Python linter
|
|
10170
|
+
/(?:^|\/)pylintrc$/i,
|
|
10171
|
+
/(?:^|\/)\.flake8$/i,
|
|
10172
|
+
// Python linter
|
|
10173
|
+
/(?:^|\/)\.mypy\.ini$/i,
|
|
10174
|
+
// Python type checker
|
|
10175
|
+
/(?:^|\/)mypy\.ini$/i,
|
|
10176
|
+
/(?:^|\/)\.swiftlint\.yml$/i,
|
|
10177
|
+
// Swift linter
|
|
10178
|
+
/(?:^|\/)\.scalafmt\.conf$/i,
|
|
10179
|
+
// Scala formatter
|
|
10180
|
+
/(?:^|\/)rustfmt\.toml$/i,
|
|
10181
|
+
// Rust formatter
|
|
10182
|
+
/(?:^|\/)\.stylelintrc/i,
|
|
10183
|
+
// CSS linter
|
|
10184
|
+
// Build / devtool config (no API surface)
|
|
10185
|
+
/(?:^|\/)tsconfig[^/]*\.json$/i,
|
|
10186
|
+
// TypeScript compiler config
|
|
10187
|
+
/(?:^|\/)vitest\.config\./i,
|
|
10188
|
+
// Test runner config
|
|
10189
|
+
/(?:^|\/)jest\.config\./i,
|
|
10190
|
+
// Test runner config
|
|
10191
|
+
/(?:^|\/)pnpm-workspace\.yaml$/i,
|
|
10192
|
+
// Workspace config
|
|
10193
|
+
/(?:^|\/)\.npmrc$/i,
|
|
10194
|
+
// npm config
|
|
10195
|
+
/(?:^|\/)\.yarnrc/i,
|
|
10196
|
+
// Yarn config
|
|
10197
|
+
/(?:^|\/)tox\.ini$/i,
|
|
10198
|
+
// Python test runner
|
|
10199
|
+
/(?:^|\/)Makefile$/i,
|
|
10200
|
+
// Build automation
|
|
10201
|
+
/(?:^|\/)Rakefile$/i,
|
|
10202
|
+
// Ruby build tasks
|
|
10203
|
+
/(?:^|\/)contributing(?:\.[^/]*)?$/i,
|
|
10204
|
+
// Contributor docs
|
|
10205
|
+
/(?:^|\/)snippet\.json$/i,
|
|
10206
|
+
// Fern-generated snippet metadata
|
|
10207
|
+
/(?:^|\/)\.gitignore$/i,
|
|
10208
|
+
/(?:^|\/)\.gitattributes$/i,
|
|
10209
|
+
/\.slnx$/i
|
|
10210
|
+
// .NET solution files
|
|
10211
|
+
];
|
|
10212
|
+
var AutoVersioningService = class _AutoVersioningService {
|
|
10213
|
+
logger;
|
|
10214
|
+
constructor({ logger }) {
|
|
10215
|
+
this.logger = logger;
|
|
10216
|
+
}
|
|
10217
|
+
/**
|
|
10218
|
+
* Extracts the previous version from a git diff by finding lines with the magic version.
|
|
10219
|
+
* The diff should contain lines like: -version = '1.2.3' +version = '0.0.0-fern-placeholder'
|
|
10220
|
+
*
|
|
10221
|
+
* @param diffContent The git diff content
|
|
10222
|
+
* @param mappedMagicVersion The magic version after language transformations (e.g., "v0.0.0-fern-placeholder" for Go)
|
|
10223
|
+
* @return The previous version if found, or undefined if all magic version occurrences are in new files
|
|
10224
|
+
* @throws AutoVersioningException if the magic version is not found at all in the diff
|
|
10225
|
+
*/
|
|
10226
|
+
extractPreviousVersion(diffContent, mappedMagicVersion) {
|
|
10227
|
+
const lines = diffContent.split("\n");
|
|
10228
|
+
let currentFile = "unknown";
|
|
10229
|
+
let magicVersionOccurrences = 0;
|
|
10230
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10231
|
+
const line = lines[i];
|
|
10232
|
+
if (!line) {
|
|
10233
|
+
continue;
|
|
10065
10234
|
}
|
|
10066
|
-
if (
|
|
10067
|
-
|
|
10235
|
+
if (line.startsWith("+++ b/")) {
|
|
10236
|
+
currentFile = line.substring(6);
|
|
10237
|
+
}
|
|
10238
|
+
if (line.startsWith("+") && !line.startsWith("+++") && line.includes(mappedMagicVersion)) {
|
|
10239
|
+
magicVersionOccurrences++;
|
|
10240
|
+
const sanitizedPlusLine = line.replace(mappedMagicVersion, "<MAGIC>");
|
|
10241
|
+
this.logger.debug(
|
|
10242
|
+
`Found placeholder version in added line (file: ${currentFile}): ${sanitizedPlusLine}`
|
|
10243
|
+
);
|
|
10244
|
+
const matchingMinusLine = this.findMatchingMinusLine(lines, i, mappedMagicVersion);
|
|
10245
|
+
if (matchingMinusLine == void 0) {
|
|
10246
|
+
this.logger.debug(
|
|
10247
|
+
`No matching minus line in hunk; continuing search. file=${currentFile}, plusLine=${sanitizedPlusLine}`
|
|
10248
|
+
);
|
|
10249
|
+
continue;
|
|
10250
|
+
}
|
|
10251
|
+
const extracted = this.extractPreviousVersionFromDiffLine(matchingMinusLine);
|
|
10252
|
+
if (extracted != void 0) {
|
|
10253
|
+
this.logger.debug(`Extracted previous version from diff (file: ${currentFile}): ${extracted}`);
|
|
10254
|
+
return extracted;
|
|
10255
|
+
}
|
|
10256
|
+
this.logger.debug(
|
|
10257
|
+
`Could not parse version from matching minus line (file: ${currentFile}); continuing search.`
|
|
10258
|
+
);
|
|
10068
10259
|
continue;
|
|
10069
10260
|
}
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
pr.number,
|
|
10075
|
-
logger
|
|
10261
|
+
}
|
|
10262
|
+
if (magicVersionOccurrences > 0) {
|
|
10263
|
+
this.logger.info(
|
|
10264
|
+
`Found placeholder version in the diff but no matching previous version lines were found in any hunk. This indicates a new SDK repository with no previous version. occurrences=${magicVersionOccurrences}`
|
|
10076
10265
|
);
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10266
|
+
return void 0;
|
|
10267
|
+
}
|
|
10268
|
+
throw new AutoVersioningException(
|
|
10269
|
+
"Failed to extract version from diff. This may indicate the version file format is not supported for auto-versioning, or the placeholder version was not found in any added lines.",
|
|
10270
|
+
{ magicVersionAbsent: true }
|
|
10271
|
+
);
|
|
10272
|
+
}
|
|
10273
|
+
/**
|
|
10274
|
+
* Scans backwards from the given index to find a '-' line that forms a version-change pair
|
|
10275
|
+
* with the '+' line at the given index. Stops at hunk boundaries.
|
|
10276
|
+
*
|
|
10277
|
+
* @param lines All diff lines
|
|
10278
|
+
* @param plusLineIndex Index of the '+' line containing the magic version
|
|
10279
|
+
* @param mappedMagicVersion The magic version to match
|
|
10280
|
+
* @return The matching '-' line, or null if not found
|
|
10281
|
+
*/
|
|
10282
|
+
findMatchingMinusLine(lines, plusLineIndex, mappedMagicVersion) {
|
|
10283
|
+
const plusLine = lines[plusLineIndex];
|
|
10284
|
+
if (!plusLine) {
|
|
10285
|
+
return void 0;
|
|
10286
|
+
}
|
|
10287
|
+
let linesScanned = 0;
|
|
10288
|
+
for (let i = plusLineIndex - 1; i >= 0; i--) {
|
|
10289
|
+
const line = lines[i];
|
|
10290
|
+
if (!line) {
|
|
10291
|
+
continue;
|
|
10292
|
+
}
|
|
10293
|
+
linesScanned++;
|
|
10294
|
+
if (this.shouldStopSearching(line)) {
|
|
10295
|
+
this.logger.debug(`Stopped backward scan at hunk boundary after ${linesScanned} lines`);
|
|
10296
|
+
break;
|
|
10297
|
+
}
|
|
10298
|
+
if (this.isDeletionLine(line)) {
|
|
10299
|
+
if (this.isVersionChangePair(line, plusLine, mappedMagicVersion)) {
|
|
10300
|
+
this.logger.debug(`Found matching minus line after scanning ${linesScanned} lines backwards`);
|
|
10301
|
+
return line;
|
|
10302
|
+
}
|
|
10088
10303
|
}
|
|
10089
10304
|
}
|
|
10090
|
-
|
|
10091
|
-
} catch (error) {
|
|
10092
|
-
logger.debug(`Error finding existing PRs: ${extractErrorMessage(error)}`);
|
|
10305
|
+
this.logger.warn(`No matching minus line found after scanning ${linesScanned} lines backwards`);
|
|
10093
10306
|
return void 0;
|
|
10094
10307
|
}
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10308
|
+
/**
|
|
10309
|
+
* Cleans the git diff by removing version-related changes.
|
|
10310
|
+
* This prevents the AI from seeing our placeholder version and the version changes themselves.
|
|
10311
|
+
*
|
|
10312
|
+
* Algorithm:
|
|
10313
|
+
* 1. Parse diff into file sections (each starting with "diff --git")
|
|
10314
|
+
* 2. For each file section, identify and remove paired - and + lines where the only difference is the magic version
|
|
10315
|
+
* 3. Remove entire file sections that have no remaining content changes (no + or - lines)
|
|
10316
|
+
*
|
|
10317
|
+
* @param diffContent The git diff content
|
|
10318
|
+
* @param mappedMagicVersion The magic version after language transformations (e.g., "v0.0.0-fern-placeholder" for Go)
|
|
10319
|
+
* @return Cleaned diff content
|
|
10320
|
+
*/
|
|
10321
|
+
cleanDiffForAI(diffContent, mappedMagicVersion) {
|
|
10322
|
+
const lines = diffContent.split("\n");
|
|
10323
|
+
const fileSections = this.parseFileSections(lines);
|
|
10324
|
+
const cleanedSections = [];
|
|
10325
|
+
for (const section of fileSections) {
|
|
10326
|
+
const filePath = this.extractFilePathFromSection(section);
|
|
10327
|
+
if (filePath != null && this.shouldExcludeFile(filePath)) {
|
|
10328
|
+
this.logger.debug(`Excluding file from diff analysis: ${filePath}`);
|
|
10329
|
+
continue;
|
|
10330
|
+
}
|
|
10331
|
+
const cleaned = this.cleanFileSection(section, mappedMagicVersion);
|
|
10332
|
+
if (cleaned != void 0) {
|
|
10333
|
+
cleanedSections.push(cleaned);
|
|
10115
10334
|
}
|
|
10116
10335
|
}
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10336
|
+
const result = [];
|
|
10337
|
+
for (const section of cleanedSections) {
|
|
10338
|
+
for (const line of section.lines) {
|
|
10339
|
+
result.push(line);
|
|
10340
|
+
}
|
|
10341
|
+
}
|
|
10342
|
+
this.logger.debug(
|
|
10343
|
+
`Cleaned diff: removed ${diffContent.length - result.join("\n").length} chars containing version changes`
|
|
10344
|
+
);
|
|
10345
|
+
return result.join("\n");
|
|
10121
10346
|
}
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10347
|
+
/**
|
|
10348
|
+
* Known patterns for API-surface signatures (function/method/class declarations).
|
|
10349
|
+
* Used by truncateDiff to prioritise sections with signature-like changes.
|
|
10350
|
+
*
|
|
10351
|
+
* Covers all languages Fern generates SDKs for:
|
|
10352
|
+
* - TypeScript/JavaScript: export function, export class, export interface, export type
|
|
10353
|
+
* - Java/C#: public class, public interface, public void, protected, internal
|
|
10354
|
+
* - Python: def method_name, class ClassName
|
|
10355
|
+
* - Go: func ExportedName (uppercase = exported)
|
|
10356
|
+
* - Ruby: def method_name, class ClassName, module ModuleName
|
|
10357
|
+
* - Swift: public func, public class, open class
|
|
10358
|
+
* - Rust: pub fn, pub struct, pub enum, pub trait
|
|
10359
|
+
* - PHP: public function, class ClassName
|
|
10360
|
+
*/
|
|
10361
|
+
static SIGNATURE_PATTERNS = [
|
|
10362
|
+
// TypeScript / JavaScript
|
|
10363
|
+
/^[+-]\s*export\s+/,
|
|
10364
|
+
// Java / C# / Swift / PHP — access modifiers
|
|
10365
|
+
/^[+-]\s*public\s+/,
|
|
10366
|
+
/^[+-]\s*protected\s+/,
|
|
10367
|
+
/^[+-]\s*internal\s+/,
|
|
10368
|
+
/^[+-]\s*open\s+(class|func)\s+/,
|
|
10369
|
+
// Python / Ruby
|
|
10370
|
+
/^[+-]\s*def\s+/,
|
|
10371
|
+
/^[+-]\s*class\s+[A-Z]/,
|
|
10372
|
+
/^[+-]\s*module\s+[A-Z]/,
|
|
10373
|
+
// Go — exported identifiers start with uppercase
|
|
10374
|
+
/^[+-]\s*func\s+[A-Z]/,
|
|
10375
|
+
/^[+-]\s*func\s+\([^)]+\)\s+[A-Z]/,
|
|
10376
|
+
/^[+-]\s*type\s+[A-Z]/,
|
|
10377
|
+
// Rust
|
|
10378
|
+
/^[+-]\s*pub\s+(fn|struct|enum|trait|type|mod)\s+/,
|
|
10379
|
+
// PHP
|
|
10380
|
+
/^[+-]\s*function\s+/
|
|
10381
|
+
];
|
|
10382
|
+
/**
|
|
10383
|
+
* Splits a cleaned diff into chunks that each fit within a byte budget.
|
|
10384
|
+
* File sections are ranked by semantic priority (same 5-tier system as
|
|
10385
|
+
* classifySection) so the first chunk always contains the highest-signal
|
|
10386
|
+
* sections. Each chunk contains only complete file sections — no partial
|
|
10387
|
+
* files.
|
|
10388
|
+
*
|
|
10389
|
+
* If a single file section exceeds `maxBytesPerChunk`, it is placed alone
|
|
10390
|
+
* in its own chunk so no information is lost.
|
|
10391
|
+
*
|
|
10392
|
+
* @param diff The cleaned diff string (output of cleanDiffForAI)
|
|
10393
|
+
* @param maxBytesPerChunk Maximum byte size for each chunk
|
|
10394
|
+
* @return Array of diff chunk strings (at least one element for non-empty diffs)
|
|
10395
|
+
*/
|
|
10396
|
+
chunkDiff(diff, maxBytesPerChunk) {
|
|
10397
|
+
const lines = diff.split("\n");
|
|
10398
|
+
const fileSections = this.parseFileSections(lines);
|
|
10399
|
+
if (fileSections.length === 0) {
|
|
10400
|
+
return [diff];
|
|
10401
|
+
}
|
|
10402
|
+
const ranked = fileSections.map((section) => ({
|
|
10403
|
+
text: section.lines.join("\n"),
|
|
10404
|
+
priority: this.classifySection(section)
|
|
10405
|
+
})).sort((a, b2) => a.priority - b2.priority);
|
|
10406
|
+
const chunks = [];
|
|
10407
|
+
let currentChunkTexts = [];
|
|
10408
|
+
let currentChunkBytes = 0;
|
|
10409
|
+
for (const entry of ranked) {
|
|
10410
|
+
const sectionBytes = Buffer.byteLength(entry.text, "utf-8");
|
|
10411
|
+
const newlineBytes = currentChunkTexts.length > 0 ? 1 : 0;
|
|
10412
|
+
if (sectionBytes > maxBytesPerChunk) {
|
|
10413
|
+
if (currentChunkTexts.length > 0) {
|
|
10414
|
+
chunks.push(currentChunkTexts.join("\n"));
|
|
10415
|
+
currentChunkTexts = [];
|
|
10416
|
+
}
|
|
10417
|
+
currentChunkBytes = 0;
|
|
10418
|
+
chunks.push(entry.text);
|
|
10419
|
+
continue;
|
|
10420
|
+
}
|
|
10421
|
+
if (currentChunkBytes + sectionBytes + newlineBytes > maxBytesPerChunk && currentChunkTexts.length > 0) {
|
|
10422
|
+
chunks.push(currentChunkTexts.join("\n"));
|
|
10423
|
+
currentChunkTexts = [];
|
|
10424
|
+
currentChunkBytes = 0;
|
|
10425
|
+
}
|
|
10426
|
+
currentChunkTexts.push(entry.text);
|
|
10427
|
+
currentChunkBytes += sectionBytes + (currentChunkTexts.length > 1 ? 1 : 0);
|
|
10144
10428
|
}
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10429
|
+
if (currentChunkTexts.length > 0) {
|
|
10430
|
+
chunks.push(currentChunkTexts.join("\n"));
|
|
10431
|
+
}
|
|
10432
|
+
return chunks;
|
|
10149
10433
|
}
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10434
|
+
/**
|
|
10435
|
+
* Classifies a file section into a priority bucket for truncation ranking.
|
|
10436
|
+
* Lower number = higher priority (included first).
|
|
10437
|
+
*
|
|
10438
|
+
* 1 = deletion-only, 2 = mixed with signatures, 3 = mixed,
|
|
10439
|
+
* 4 = addition-only, 5 = context-only
|
|
10440
|
+
*/
|
|
10441
|
+
static isDiffHeader(line) {
|
|
10442
|
+
return line.startsWith("--- a/") || line.startsWith("--- /dev/null") || line.startsWith("+++ b/") || line.startsWith("+++ /dev/null");
|
|
10443
|
+
}
|
|
10444
|
+
classifySection(section) {
|
|
10445
|
+
let hasAdditions = false;
|
|
10446
|
+
let hasDeletions = false;
|
|
10447
|
+
let hasSignature = false;
|
|
10448
|
+
for (const line of section.lines) {
|
|
10449
|
+
const isAddition = line.startsWith("+") && !_AutoVersioningService.isDiffHeader(line);
|
|
10450
|
+
const isDeletion = line.startsWith("-") && !_AutoVersioningService.isDiffHeader(line);
|
|
10451
|
+
if (isAddition) {
|
|
10452
|
+
hasAdditions = true;
|
|
10453
|
+
}
|
|
10454
|
+
if (isDeletion) {
|
|
10455
|
+
hasDeletions = true;
|
|
10456
|
+
}
|
|
10457
|
+
if (isAddition || isDeletion) {
|
|
10458
|
+
for (const pattern of _AutoVersioningService.SIGNATURE_PATTERNS) {
|
|
10459
|
+
if (pattern.test(line)) {
|
|
10460
|
+
hasSignature = true;
|
|
10461
|
+
break;
|
|
10462
|
+
}
|
|
10463
|
+
}
|
|
10464
|
+
}
|
|
10465
|
+
if (hasAdditions && hasDeletions && hasSignature) {
|
|
10466
|
+
break;
|
|
10467
|
+
}
|
|
10468
|
+
}
|
|
10469
|
+
if (hasDeletions && !hasAdditions) {
|
|
10470
|
+
return 1;
|
|
10471
|
+
}
|
|
10472
|
+
if (hasDeletions && hasAdditions && hasSignature) {
|
|
10473
|
+
return 2;
|
|
10474
|
+
}
|
|
10475
|
+
if (hasDeletions && hasAdditions) {
|
|
10476
|
+
return 3;
|
|
10477
|
+
}
|
|
10478
|
+
if (hasAdditions) {
|
|
10479
|
+
return 4;
|
|
10480
|
+
}
|
|
10481
|
+
return 5;
|
|
10156
10482
|
}
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10483
|
+
/**
|
|
10484
|
+
* Parses a diff into file sections, where each section starts with "diff --git".
|
|
10485
|
+
*/
|
|
10486
|
+
parseFileSections(lines) {
|
|
10487
|
+
const sections = [];
|
|
10488
|
+
let currentSection = void 0;
|
|
10489
|
+
for (const line of lines) {
|
|
10490
|
+
if (line.startsWith("diff --git")) {
|
|
10491
|
+
if (currentSection != void 0) {
|
|
10492
|
+
sections.push(currentSection);
|
|
10493
|
+
}
|
|
10494
|
+
currentSection = { lines: [line] };
|
|
10495
|
+
} else if (currentSection != void 0) {
|
|
10496
|
+
currentSection.lines.push(line);
|
|
10497
|
+
}
|
|
10498
|
+
}
|
|
10499
|
+
if (currentSection != void 0) {
|
|
10500
|
+
sections.push(currentSection);
|
|
10501
|
+
}
|
|
10502
|
+
return sections;
|
|
10176
10503
|
}
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10504
|
+
/**
|
|
10505
|
+
* Cleans a single file section by removing version-related changes.
|
|
10506
|
+
* Returns null if the file section should be completely removed.
|
|
10507
|
+
*/
|
|
10508
|
+
cleanFileSection(section, mappedMagicVersion) {
|
|
10509
|
+
const headerLines = [];
|
|
10510
|
+
const contentLines = [];
|
|
10511
|
+
let inContent = false;
|
|
10512
|
+
for (const line of section.lines) {
|
|
10513
|
+
if (line.startsWith("@@")) {
|
|
10514
|
+
inContent = true;
|
|
10515
|
+
headerLines.push(line);
|
|
10516
|
+
} else if (!inContent) {
|
|
10517
|
+
headerLines.push(line);
|
|
10518
|
+
} else {
|
|
10519
|
+
contentLines.push(line);
|
|
10520
|
+
}
|
|
10521
|
+
}
|
|
10522
|
+
let processedContent = this.removeVersionChangePairs(contentLines, mappedMagicVersion);
|
|
10523
|
+
processedContent = this.removeRemainingMagicVersionLines(processedContent, mappedMagicVersion);
|
|
10524
|
+
let hasChanges = false;
|
|
10525
|
+
for (const line of processedContent) {
|
|
10526
|
+
const isAddition = line.startsWith("+") && !_AutoVersioningService.isDiffHeader(line);
|
|
10527
|
+
const isDeletion = line.startsWith("-") && !_AutoVersioningService.isDiffHeader(line);
|
|
10528
|
+
if (isAddition || isDeletion) {
|
|
10529
|
+
hasChanges = true;
|
|
10530
|
+
break;
|
|
10531
|
+
}
|
|
10532
|
+
}
|
|
10533
|
+
if (!hasChanges) {
|
|
10534
|
+
return void 0;
|
|
10535
|
+
}
|
|
10536
|
+
const cleaned = { lines: [] };
|
|
10537
|
+
cleaned.lines.push(...headerLines);
|
|
10538
|
+
cleaned.lines.push(...processedContent);
|
|
10539
|
+
return cleaned;
|
|
10181
10540
|
}
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10541
|
+
/**
|
|
10542
|
+
* Removes any remaining lines (additions, deletions, or context) that contain the magic version.
|
|
10543
|
+
* This is a secondary pass to catch any lines that the pairing logic missed.
|
|
10544
|
+
*/
|
|
10545
|
+
removeRemainingMagicVersionLines(lines, mappedMagicVersion) {
|
|
10546
|
+
const result = [];
|
|
10547
|
+
for (const line of lines) {
|
|
10548
|
+
if (line.includes(mappedMagicVersion)) {
|
|
10549
|
+
continue;
|
|
10550
|
+
}
|
|
10551
|
+
result.push(line);
|
|
10552
|
+
}
|
|
10553
|
+
return result;
|
|
10188
10554
|
}
|
|
10189
|
-
|
|
10190
|
-
|
|
10555
|
+
/**
|
|
10556
|
+
* Removes paired - and + lines where the only difference is the magic version.
|
|
10557
|
+
*/
|
|
10558
|
+
removeVersionChangePairs(lines, mappedMagicVersion) {
|
|
10559
|
+
const result = [];
|
|
10560
|
+
let lineIndex = 0;
|
|
10561
|
+
while (lineIndex < lines.length) {
|
|
10562
|
+
const currentLine = lines[lineIndex];
|
|
10563
|
+
if (!currentLine) {
|
|
10564
|
+
lineIndex++;
|
|
10565
|
+
continue;
|
|
10566
|
+
}
|
|
10567
|
+
if (this.isDeletionLine(currentLine)) {
|
|
10568
|
+
const pairIndex = this.findMatchingAdditionLine(lines, lineIndex, mappedMagicVersion);
|
|
10569
|
+
if (pairIndex !== -1) {
|
|
10570
|
+
result.push(...lines.slice(lineIndex + 1, pairIndex));
|
|
10571
|
+
lineIndex = pairIndex + 1;
|
|
10572
|
+
} else {
|
|
10573
|
+
result.push(currentLine);
|
|
10574
|
+
lineIndex++;
|
|
10575
|
+
}
|
|
10576
|
+
} else {
|
|
10577
|
+
result.push(currentLine);
|
|
10578
|
+
lineIndex++;
|
|
10579
|
+
}
|
|
10580
|
+
}
|
|
10581
|
+
return result;
|
|
10582
|
+
}
|
|
10583
|
+
isDeletionLine(line) {
|
|
10584
|
+
return line.startsWith("-") && !_AutoVersioningService.isDiffHeader(line);
|
|
10585
|
+
}
|
|
10586
|
+
findMatchingAdditionLine(lines, startIndex, mappedMagicVersion) {
|
|
10587
|
+
const startLine = lines[startIndex];
|
|
10588
|
+
if (!startLine) {
|
|
10589
|
+
return -1;
|
|
10590
|
+
}
|
|
10591
|
+
for (let nextIndex = startIndex + 1; nextIndex < lines.length; nextIndex++) {
|
|
10592
|
+
const nextLine = lines[nextIndex];
|
|
10593
|
+
if (!nextLine) {
|
|
10594
|
+
continue;
|
|
10595
|
+
}
|
|
10596
|
+
if (this.shouldStopSearching(nextLine)) {
|
|
10597
|
+
break;
|
|
10598
|
+
}
|
|
10599
|
+
if (this.isAdditionLine(nextLine)) {
|
|
10600
|
+
if (this.isVersionChangePair(startLine, nextLine, mappedMagicVersion)) {
|
|
10601
|
+
return nextIndex;
|
|
10602
|
+
}
|
|
10603
|
+
}
|
|
10604
|
+
}
|
|
10605
|
+
return -1;
|
|
10606
|
+
}
|
|
10607
|
+
isAdditionLine(line) {
|
|
10608
|
+
return line.startsWith("+") && !_AutoVersioningService.isDiffHeader(line);
|
|
10609
|
+
}
|
|
10610
|
+
shouldStopSearching(line) {
|
|
10611
|
+
return line.startsWith("@@") || line.startsWith("diff --git") || line.startsWith("index ") || _AutoVersioningService.isDiffHeader(line);
|
|
10612
|
+
}
|
|
10613
|
+
/**
|
|
10614
|
+
* Checks if two lines form a version change pair where the only difference is the magic version,
|
|
10615
|
+
* or where the only difference is a Go module path /vN suffix.
|
|
10616
|
+
*/
|
|
10617
|
+
isVersionChangePair(minusLine, plusLine, mappedMagicVersion) {
|
|
10618
|
+
if (this.isGoModulePathSuffixChange(minusLine, plusLine)) {
|
|
10619
|
+
return true;
|
|
10620
|
+
}
|
|
10621
|
+
if (!plusLine.includes(mappedMagicVersion)) {
|
|
10622
|
+
return false;
|
|
10623
|
+
}
|
|
10624
|
+
const minusContent = minusLine.substring(1);
|
|
10625
|
+
const plusContent = plusLine.substring(1);
|
|
10626
|
+
const magicIndex = plusContent.indexOf(mappedMagicVersion);
|
|
10627
|
+
if (magicIndex === -1) {
|
|
10628
|
+
return false;
|
|
10629
|
+
}
|
|
10630
|
+
const plusPrefix = plusContent.substring(0, magicIndex);
|
|
10631
|
+
const plusSuffix = plusContent.substring(magicIndex + mappedMagicVersion.length);
|
|
10632
|
+
if (!minusContent.startsWith(plusPrefix)) {
|
|
10633
|
+
return false;
|
|
10634
|
+
}
|
|
10635
|
+
const versionStart = plusPrefix.length;
|
|
10636
|
+
const versionEnd = minusContent.length - plusSuffix.length;
|
|
10637
|
+
if (versionEnd <= versionStart || versionEnd > minusContent.length) {
|
|
10638
|
+
return false;
|
|
10639
|
+
}
|
|
10640
|
+
const minusVersion = minusContent.substring(versionStart, versionEnd);
|
|
10641
|
+
const minusSuffix = minusContent.substring(versionEnd);
|
|
10642
|
+
if (minusSuffix !== plusSuffix) {
|
|
10643
|
+
return false;
|
|
10644
|
+
}
|
|
10645
|
+
return /.*\d+.*/.test(minusVersion);
|
|
10646
|
+
}
|
|
10647
|
+
/**
|
|
10648
|
+
* Detects when two diff lines differ only by a Go module path /vN suffix.
|
|
10649
|
+
* For example:
|
|
10650
|
+
* - core "github.com/org/repo/core"
|
|
10651
|
+
* + core "github.com/org/repo/v2/core"
|
|
10652
|
+
* These are artifacts of the major version bump and should be filtered from AI analysis.
|
|
10653
|
+
*
|
|
10654
|
+
* Only matches lines that contain a quoted Go-style module path (e.g., "github.com/..."),
|
|
10655
|
+
* so non-Go diffs like API URL version changes are not affected.
|
|
10656
|
+
*/
|
|
10657
|
+
isGoModulePathSuffixChange(minusLine, plusLine) {
|
|
10658
|
+
const minusContent = minusLine.substring(1);
|
|
10659
|
+
const plusContent = plusLine.substring(1);
|
|
10660
|
+
const goModulePathPattern = /"[^"]*(?:github\.com|golang\.org|google\.golang\.org|gopkg\.in)\/[^"]*"/;
|
|
10661
|
+
const goModuleDirectivePattern = /^\s*module\s+(?:github\.com|golang\.org|google\.golang\.org|gopkg\.in)\/\S+/;
|
|
10662
|
+
const hasGoModulePath = goModulePathPattern.test(minusContent) || goModulePathPattern.test(plusContent) || goModuleDirectivePattern.test(minusContent.trim()) || goModuleDirectivePattern.test(plusContent.trim());
|
|
10663
|
+
if (!hasGoModulePath) {
|
|
10664
|
+
return false;
|
|
10665
|
+
}
|
|
10666
|
+
const suffixPattern = /\/v\d+(?=\/|"\s*$|$)/g;
|
|
10667
|
+
const minusWithoutSuffix = minusContent.replace(suffixPattern, "");
|
|
10668
|
+
const plusWithoutSuffix = plusContent.replace(suffixPattern, "");
|
|
10669
|
+
if (minusWithoutSuffix !== plusWithoutSuffix) {
|
|
10670
|
+
return false;
|
|
10671
|
+
}
|
|
10672
|
+
return /\/v\d+(?=\/|"\s*$|$)/.test(minusContent) || /\/v\d+(?=\/|"\s*$|$)/.test(plusContent);
|
|
10673
|
+
}
|
|
10674
|
+
/**
|
|
10675
|
+
* Gets the latest semantic version from git tags in the given repository.
|
|
10676
|
+
* Used as a fallback when the magic version is not embedded in any generated file
|
|
10677
|
+
* (e.g., Swift SDKs which use git tags for versioning via SPM, not a version file).
|
|
10678
|
+
*
|
|
10679
|
+
* @param workingDirectory The git repository directory
|
|
10680
|
+
* @return The latest semver version from git tags, or undefined if no valid tags found
|
|
10681
|
+
*/
|
|
10682
|
+
async getLatestVersionFromGitTags(workingDirectory) {
|
|
10683
|
+
try {
|
|
10684
|
+
const result = await (0, import_logging_execa.loggingExeca)(this.logger, "git", ["ls-remote", "--tags", "origin"], {
|
|
10685
|
+
cwd: workingDirectory,
|
|
10686
|
+
doNotPipeOutput: true
|
|
10687
|
+
});
|
|
10688
|
+
const output = result.stdout;
|
|
10689
|
+
if (!output || output.trim().length === 0) {
|
|
10690
|
+
this.logger.info("No git tags found in repository");
|
|
10691
|
+
return void 0;
|
|
10692
|
+
}
|
|
10693
|
+
const tags = [];
|
|
10694
|
+
for (const line of output.split("\n")) {
|
|
10695
|
+
const trimmed2 = line.trim();
|
|
10696
|
+
if (trimmed2.length === 0 || trimmed2.includes("^{}")) {
|
|
10697
|
+
continue;
|
|
10698
|
+
}
|
|
10699
|
+
const match = trimmed2.match(/refs\/tags\/(.+)$/);
|
|
10700
|
+
if (match?.[1]) {
|
|
10701
|
+
tags.push(match[1]);
|
|
10702
|
+
}
|
|
10703
|
+
}
|
|
10704
|
+
const validTags = tags.filter((tag) => import_semver.default.valid(tag) != null).sort((a, b2) => import_semver.default.rcompare(a, b2));
|
|
10705
|
+
if (validTags.length > 0) {
|
|
10706
|
+
const latest = validTags[0];
|
|
10707
|
+
this.logger.info(`Found latest version from git tags: ${latest}`);
|
|
10708
|
+
return latest;
|
|
10709
|
+
}
|
|
10710
|
+
this.logger.info("No valid semver tags found in repository");
|
|
10711
|
+
return void 0;
|
|
10712
|
+
} catch (e) {
|
|
10713
|
+
this.logger.warn(`Failed to read git tags: ${e}`);
|
|
10714
|
+
return void 0;
|
|
10715
|
+
}
|
|
10716
|
+
}
|
|
10717
|
+
/**
|
|
10718
|
+
* Replaces all occurrences of the magic version with the final version in generated files.
|
|
10719
|
+
*
|
|
10720
|
+
* @param workingDirectory The directory containing generated files
|
|
10721
|
+
* @param mappedMagicVersion The magic version after language transformations (e.g., "v0.0.0-fern-placeholder" for Go)
|
|
10722
|
+
* @param finalVersion The final version to use
|
|
10723
|
+
* @throws Error if file operations fail or find/sed command fails
|
|
10724
|
+
*/
|
|
10725
|
+
async replaceMagicVersion(workingDirectory, mappedMagicVersion, finalVersion) {
|
|
10726
|
+
this.logger.debug(`Replacing placeholder version ${mappedMagicVersion} with final version: ${finalVersion}`);
|
|
10727
|
+
const sedCommand = `s/${this.escapeForSed(mappedMagicVersion)}/${this.escapeForSed(finalVersion)}/g`;
|
|
10728
|
+
let isGNUSed = false;
|
|
10729
|
+
try {
|
|
10730
|
+
await (0, import_logging_execa.loggingExeca)(this.logger, "sed", ["--version"], {
|
|
10731
|
+
doNotPipeOutput: true
|
|
10732
|
+
});
|
|
10733
|
+
isGNUSed = true;
|
|
10734
|
+
} catch {
|
|
10735
|
+
isGNUSed = false;
|
|
10736
|
+
}
|
|
10737
|
+
let command;
|
|
10738
|
+
if (isGNUSed) {
|
|
10739
|
+
command = `find "${workingDirectory}" -type f -not -path "*/.git/*" -exec sed -i '${sedCommand}' {} +`;
|
|
10740
|
+
} else {
|
|
10741
|
+
command = `find "${workingDirectory}" -type f -not -path "*/.git/*" -exec sed -i '' '${sedCommand}' {} +`;
|
|
10742
|
+
}
|
|
10743
|
+
await (0, import_logging_execa.loggingExeca)(this.logger, "bash", ["-c", command], {
|
|
10744
|
+
cwd: workingDirectory,
|
|
10745
|
+
doNotPipeOutput: true
|
|
10746
|
+
});
|
|
10747
|
+
this.logger.debug("Placeholder version replaced successfully");
|
|
10748
|
+
}
|
|
10749
|
+
/**
|
|
10750
|
+
* Adds /vN suffix to Go module paths and import statements when the computed
|
|
10751
|
+
* version is v2+. Go requires a /vN suffix on module paths for major versions >= 2.
|
|
10752
|
+
*
|
|
10753
|
+
* When using the safe magic version (major 0), the generated code has no /vN suffix.
|
|
10754
|
+
* After auto-versioning determines the real version, this method adds the correct suffix.
|
|
10755
|
+
*
|
|
10756
|
+
* @param workingDirectory The directory containing generated Go files
|
|
10757
|
+
* @param finalVersion The final computed version (e.g., "v2.0.0", "v3.1.0")
|
|
10758
|
+
*/
|
|
10759
|
+
async addGoMajorVersionSuffix(workingDirectory, finalVersion) {
|
|
10760
|
+
const major = this.extractMajorVersion(finalVersion);
|
|
10761
|
+
if (major == null || major < 2) {
|
|
10762
|
+
this.logger.debug(
|
|
10763
|
+
`Version ${finalVersion} has major ${major ?? "unknown"}, no Go module path suffix needed`
|
|
10764
|
+
);
|
|
10765
|
+
return;
|
|
10766
|
+
}
|
|
10767
|
+
const goModPath = (0, import_path3.join)(workingDirectory, "go.mod");
|
|
10768
|
+
if (!(0, import_fs2.existsSync)(goModPath)) {
|
|
10769
|
+
this.logger.debug(`No go.mod found in ${workingDirectory}, skipping Go module path update`);
|
|
10770
|
+
return;
|
|
10771
|
+
}
|
|
10772
|
+
const modulePath = await this.readGoModulePath(goModPath);
|
|
10773
|
+
if (modulePath == null) {
|
|
10774
|
+
this.logger.warn(`Could not read module path from go.mod in ${workingDirectory}`);
|
|
10775
|
+
return;
|
|
10776
|
+
}
|
|
10777
|
+
const suffix = `/v${major}`;
|
|
10778
|
+
if (/\/v\d+$/.test(modulePath)) {
|
|
10779
|
+
this.logger.debug(`Module path ${modulePath} already has a version suffix, skipping`);
|
|
10780
|
+
return;
|
|
10781
|
+
}
|
|
10782
|
+
await this.addSuffixToGoMod(goModPath, modulePath, suffix);
|
|
10783
|
+
await this.addSuffixToGoFiles(workingDirectory, modulePath, suffix);
|
|
10784
|
+
this.logger.info(`Added ${suffix} to Go module path ${modulePath} and all import paths`);
|
|
10785
|
+
}
|
|
10786
|
+
/**
|
|
10787
|
+
* Extracts the major version number from a version string.
|
|
10788
|
+
* Handles versions with or without 'v' prefix.
|
|
10789
|
+
*/
|
|
10790
|
+
extractMajorVersion(version) {
|
|
10791
|
+
const match = version.match(/^v?(\d+)\./);
|
|
10792
|
+
if (match?.[1] != null) {
|
|
10793
|
+
return parseInt(match[1], 10);
|
|
10794
|
+
}
|
|
10795
|
+
return void 0;
|
|
10796
|
+
}
|
|
10797
|
+
/**
|
|
10798
|
+
* Reads the module path from a go.mod file.
|
|
10799
|
+
*/
|
|
10800
|
+
async readGoModulePath(goModPath) {
|
|
10801
|
+
const content = await (0, import_promises2.readFile)(goModPath, "utf-8");
|
|
10802
|
+
for (const line of content.split("\n")) {
|
|
10803
|
+
const trimmed2 = line.trim();
|
|
10804
|
+
if (trimmed2.startsWith("module ")) {
|
|
10805
|
+
return trimmed2.substring("module ".length).trim();
|
|
10806
|
+
}
|
|
10807
|
+
}
|
|
10808
|
+
return void 0;
|
|
10809
|
+
}
|
|
10810
|
+
/**
|
|
10811
|
+
* Adds a version suffix to the module line in go.mod.
|
|
10812
|
+
*/
|
|
10813
|
+
async addSuffixToGoMod(goModPath, modulePath, suffix) {
|
|
10814
|
+
const content = await (0, import_promises2.readFile)(goModPath, "utf-8");
|
|
10815
|
+
const lines = content.split("\n");
|
|
10816
|
+
let changed = false;
|
|
10817
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10818
|
+
const line = lines[i];
|
|
10819
|
+
if (line != null && line.trim().startsWith("module ") && line.includes(modulePath)) {
|
|
10820
|
+
lines[i] = line.replace(modulePath, modulePath + suffix);
|
|
10821
|
+
changed = true;
|
|
10822
|
+
break;
|
|
10823
|
+
}
|
|
10824
|
+
}
|
|
10825
|
+
if (changed) {
|
|
10826
|
+
await (0, import_promises2.writeFile)(goModPath, lines.join("\n"), "utf-8");
|
|
10827
|
+
this.logger.debug(`Updated go.mod: added ${suffix} to module path`);
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
/**
|
|
10831
|
+
* Recursively walks a directory and returns all file paths matching a filter.
|
|
10832
|
+
*/
|
|
10833
|
+
async walkDirectory(dir, filter) {
|
|
10834
|
+
const results = [];
|
|
10835
|
+
const entries2 = await (0, import_promises2.readdir)(dir);
|
|
10836
|
+
for (const entry of entries2) {
|
|
10837
|
+
const fullPath = (0, import_path3.join)(dir, entry);
|
|
10838
|
+
const entryStat = await (0, import_promises2.stat)(fullPath);
|
|
10839
|
+
if (entryStat.isDirectory()) {
|
|
10840
|
+
if (entry === ".git" || entry === "vendor") {
|
|
10841
|
+
continue;
|
|
10842
|
+
}
|
|
10843
|
+
const subResults = await this.walkDirectory(fullPath, filter);
|
|
10844
|
+
results.push(...subResults);
|
|
10845
|
+
} else if (filter(fullPath)) {
|
|
10846
|
+
results.push(fullPath);
|
|
10847
|
+
}
|
|
10848
|
+
}
|
|
10849
|
+
return results;
|
|
10850
|
+
}
|
|
10851
|
+
/**
|
|
10852
|
+
* Adds a version suffix to import paths in all .go files in the repository.
|
|
10853
|
+
*/
|
|
10854
|
+
async addSuffixToGoFiles(repoRoot, modulePath, suffix) {
|
|
10855
|
+
const goFiles = await this.walkDirectory(repoRoot, (filePath) => (0, import_path3.extname)(filePath) === ".go");
|
|
10856
|
+
for (const filePath of goFiles) {
|
|
10857
|
+
await this.addSuffixToGoFile(filePath, modulePath, suffix);
|
|
10858
|
+
}
|
|
10859
|
+
}
|
|
10860
|
+
/**
|
|
10861
|
+
* Adds a version suffix to import paths in a single .go file.
|
|
10862
|
+
* Replaces occurrences of the module path with modulePath + suffix in import statements.
|
|
10863
|
+
*/
|
|
10864
|
+
async addSuffixToGoFile(filePath, modulePath, suffix) {
|
|
10865
|
+
const content = await (0, import_promises2.readFile)(filePath, "utf-8");
|
|
10866
|
+
const lines = content.split("\n");
|
|
10867
|
+
let changed = false;
|
|
10868
|
+
let inImportBlock = false;
|
|
10869
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10870
|
+
const line = lines[i];
|
|
10871
|
+
if (line == null) {
|
|
10872
|
+
continue;
|
|
10873
|
+
}
|
|
10874
|
+
const trimmed2 = line.trim();
|
|
10875
|
+
if (trimmed2.startsWith("import (")) {
|
|
10876
|
+
inImportBlock = true;
|
|
10877
|
+
} else if (inImportBlock && trimmed2 === ")") {
|
|
10878
|
+
inImportBlock = false;
|
|
10879
|
+
}
|
|
10880
|
+
const isImportLine = inImportBlock || trimmed2.startsWith('import "') || trimmed2.startsWith('import "') || /^import\s+\w+\s+"/.test(trimmed2);
|
|
10881
|
+
if (!isImportLine) {
|
|
10882
|
+
continue;
|
|
10883
|
+
}
|
|
10884
|
+
if (!line.includes(modulePath)) {
|
|
10885
|
+
continue;
|
|
10886
|
+
}
|
|
10887
|
+
const newLine = line.replace(`"${modulePath}/`, `"${modulePath}${suffix}/`).replace(`"${modulePath}"`, `"${modulePath}${suffix}"`);
|
|
10888
|
+
if (newLine !== line) {
|
|
10889
|
+
lines[i] = newLine;
|
|
10890
|
+
changed = true;
|
|
10891
|
+
}
|
|
10892
|
+
}
|
|
10893
|
+
if (changed) {
|
|
10894
|
+
await (0, import_promises2.writeFile)(filePath, lines.join("\n"), "utf-8");
|
|
10895
|
+
this.logger.debug(`Updated Go imports in ${filePath}`);
|
|
10896
|
+
}
|
|
10897
|
+
}
|
|
10898
|
+
/**
|
|
10899
|
+
* Escapes special characters for use in sed command.
|
|
10900
|
+
*/
|
|
10901
|
+
escapeForSed(str) {
|
|
10902
|
+
return str.replace(/[/&]/g, "\\$&");
|
|
10903
|
+
}
|
|
10904
|
+
/**
|
|
10905
|
+
* Extracts the file path from a diff file section.
|
|
10906
|
+
* Parses the "diff --git a/path b/path" header line.
|
|
10907
|
+
*/
|
|
10908
|
+
extractFilePathFromSection(section) {
|
|
10909
|
+
const firstLine = section.lines[0];
|
|
10910
|
+
if (firstLine == null) {
|
|
10911
|
+
return void 0;
|
|
10912
|
+
}
|
|
10913
|
+
const match = firstLine.match(/^diff --git "?a\/(.+?)"? "?b\/(.+?)"?$/);
|
|
10914
|
+
if (match?.[2] != null) {
|
|
10915
|
+
return match[2];
|
|
10916
|
+
}
|
|
10917
|
+
return void 0;
|
|
10918
|
+
}
|
|
10919
|
+
/**
|
|
10920
|
+
* Checks whether a file path matches any of the exclusion patterns.
|
|
10921
|
+
* Excluded files are noise for AI-based semantic version analysis.
|
|
10922
|
+
*/
|
|
10923
|
+
shouldExcludeFile(filePath) {
|
|
10924
|
+
return EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
10925
|
+
}
|
|
10926
|
+
/**
|
|
10927
|
+
* Extracts the previous version from a line containing the magic version.
|
|
10928
|
+
* Assumes the line format is like: "version = '0.0.0-fern-placeholder'" or "version: 0.0.0-fern-placeholder"
|
|
10929
|
+
*
|
|
10930
|
+
* @param lineWithMagicVersion A line from git diff containing the magic version
|
|
10931
|
+
* @return The inferred previous version if found, or undefined if the version cannot be parsed
|
|
10932
|
+
*/
|
|
10933
|
+
extractPreviousVersionFromDiffLine(lineWithMagicVersion) {
|
|
10934
|
+
const prevVersionPattern = /[-].*?([v]?\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?)/;
|
|
10935
|
+
const matcher = lineWithMagicVersion.match(prevVersionPattern);
|
|
10936
|
+
if (matcher && matcher[1]) {
|
|
10937
|
+
const version = matcher[1];
|
|
10938
|
+
this.logger.info(`Extracted previous version from diff: ${version}`);
|
|
10939
|
+
return version;
|
|
10940
|
+
}
|
|
10941
|
+
this.logger.warn("Could not extract previous version from diff line: " + lineWithMagicVersion);
|
|
10942
|
+
return void 0;
|
|
10943
|
+
}
|
|
10944
|
+
};
|
|
10945
|
+
|
|
10946
|
+
// src/autoversion/VersionUtils.ts
|
|
10947
|
+
var MAGIC_VERSION = "0.0.0-fern-placeholder";
|
|
10948
|
+
var MAGIC_VERSION_PYTHON = "0.0.0.dev0";
|
|
10949
|
+
function mapMagicVersionForLanguage(magicVersion, language) {
|
|
10950
|
+
if (magicVersion !== MAGIC_VERSION) {
|
|
10951
|
+
return magicVersion;
|
|
10952
|
+
}
|
|
10953
|
+
if (language === "go") {
|
|
10954
|
+
return `v${magicVersion}`;
|
|
10955
|
+
}
|
|
10956
|
+
if (language === "python") {
|
|
10957
|
+
return MAGIC_VERSION_PYTHON;
|
|
10958
|
+
}
|
|
10959
|
+
return magicVersion;
|
|
10960
|
+
}
|
|
10961
|
+
var MAX_AI_DIFF_BYTES = 4e4;
|
|
10962
|
+
var MAX_CHUNKS = 40;
|
|
10963
|
+
var MAX_RAW_DIFF_BYTES = 1e7;
|
|
10964
|
+
var VERSION_BUMP_RANK = {
|
|
10965
|
+
["MAJOR" /* MAJOR */]: 3,
|
|
10966
|
+
["MINOR" /* MINOR */]: 2,
|
|
10967
|
+
["PATCH" /* PATCH */]: 1,
|
|
10968
|
+
["NO_CHANGE" /* NO_CHANGE */]: 0
|
|
10969
|
+
};
|
|
10970
|
+
function maxVersionBump(a, b2) {
|
|
10971
|
+
return (VERSION_BUMP_RANK[a] ?? 0) >= (VERSION_BUMP_RANK[b2] ?? 0) ? a : b2;
|
|
10972
|
+
}
|
|
10973
|
+
var SEMVER_PATTERN = /^(v)?(\d+)\.(\d+)\.(\d+)(?:-([\w.-]+))?(?:\+([\w.-]+))?$/;
|
|
10974
|
+
function isValidSemver(version) {
|
|
10975
|
+
return SEMVER_PATTERN.test(version);
|
|
10976
|
+
}
|
|
10977
|
+
function incrementVersion(currentVersion, versionBump) {
|
|
10978
|
+
const matcher = currentVersion.match(SEMVER_PATTERN);
|
|
10979
|
+
if (!matcher) {
|
|
10980
|
+
throw new AutoVersioningException("Invalid semantic version format: " + currentVersion);
|
|
10981
|
+
}
|
|
10982
|
+
const prefix = matcher[1] || "";
|
|
10983
|
+
let major = parseInt(matcher[2] ?? "0", 10);
|
|
10984
|
+
let minor = parseInt(matcher[3] ?? "0", 10);
|
|
10985
|
+
let patch = parseInt(matcher[4] ?? "0", 10);
|
|
10986
|
+
const preRelease = matcher[5];
|
|
10987
|
+
const buildMetadata = matcher[6];
|
|
10988
|
+
let preserveMetadata = false;
|
|
10989
|
+
if (versionBump === "MAJOR" /* MAJOR */) {
|
|
10990
|
+
major++;
|
|
10991
|
+
minor = 0;
|
|
10992
|
+
patch = 0;
|
|
10993
|
+
} else if (versionBump === "MINOR" /* MINOR */) {
|
|
10994
|
+
minor++;
|
|
10995
|
+
patch = 0;
|
|
10996
|
+
} else if (versionBump === "PATCH" /* PATCH */) {
|
|
10997
|
+
patch++;
|
|
10998
|
+
} else if (versionBump === "NO_CHANGE" /* NO_CHANGE */) {
|
|
10999
|
+
preserveMetadata = true;
|
|
11000
|
+
} else {
|
|
11001
|
+
throw new AutoVersioningException("Unknown version bump type: " + versionBump);
|
|
11002
|
+
}
|
|
11003
|
+
let newVersion = `${prefix}${major}.${minor}.${patch}`;
|
|
11004
|
+
if (preserveMetadata) {
|
|
11005
|
+
if (preRelease) {
|
|
11006
|
+
newVersion += `-${preRelease}`;
|
|
11007
|
+
}
|
|
11008
|
+
if (buildMetadata) {
|
|
11009
|
+
newVersion += `+${buildMetadata}`;
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
return newVersion;
|
|
11013
|
+
}
|
|
11014
|
+
|
|
11015
|
+
// src/pipeline/steps/BaseStep.ts
|
|
11016
|
+
var BaseStep = class {
|
|
11017
|
+
constructor(outputDir, logger) {
|
|
11018
|
+
this.outputDir = outputDir;
|
|
11019
|
+
this.logger = logger;
|
|
11020
|
+
}
|
|
11021
|
+
outputDir;
|
|
11022
|
+
logger;
|
|
11023
|
+
};
|
|
11024
|
+
|
|
11025
|
+
// src/pipeline/steps/AutoVersionStep.ts
|
|
11026
|
+
var COMMIT_MARKER = "[fern-autoversion]";
|
|
11027
|
+
var FERN_TRAILER = "\n\n\u{1F33F} Generated with Fern";
|
|
11028
|
+
var AutoVersionStep = class extends BaseStep {
|
|
11029
|
+
constructor(outputDir, logger, config) {
|
|
11030
|
+
super(outputDir, logger);
|
|
11031
|
+
this.config = config;
|
|
11032
|
+
}
|
|
11033
|
+
config;
|
|
11034
|
+
name = "autoVersion";
|
|
11035
|
+
async execute(context) {
|
|
11036
|
+
const prepared = context.previousStepResults.generationCommit?.preparedReplay;
|
|
11037
|
+
if (!prepared) {
|
|
11038
|
+
this.logger.info(
|
|
11039
|
+
"AutoVersionStep: no replay preparation available (replay uninitialized or prepare failed); skipping."
|
|
11040
|
+
);
|
|
11041
|
+
return { executed: true, success: true };
|
|
11042
|
+
}
|
|
11043
|
+
if (prepared.flow === "skip-application") {
|
|
11044
|
+
this.logger.info("AutoVersionStep: replay flow is skip-application; skipping autoversion.");
|
|
11045
|
+
return { executed: true, success: true };
|
|
11046
|
+
}
|
|
11047
|
+
const language = this.config.language;
|
|
11048
|
+
const mappedMagicVersion = mapMagicVersionForLanguage(MAGIC_VERSION, language);
|
|
11049
|
+
const service = new AutoVersioningService({ logger: this.toTaskLogger() });
|
|
11050
|
+
if (prepared.previousGenerationSha == null) {
|
|
11051
|
+
return await this.handleFirstGeneration({ service, language, mappedMagicVersion });
|
|
11052
|
+
}
|
|
11053
|
+
return await this.handleNormalFlow({
|
|
11054
|
+
prepared,
|
|
11055
|
+
service,
|
|
11056
|
+
language,
|
|
11057
|
+
mappedMagicVersion,
|
|
11058
|
+
previousGenerationSha: prepared.previousGenerationSha
|
|
11059
|
+
});
|
|
11060
|
+
}
|
|
11061
|
+
async handleFirstGeneration(params) {
|
|
11062
|
+
const { service, language, mappedMagicVersion } = params;
|
|
11063
|
+
const initialVersion = this.config.baseVersion ?? (mappedMagicVersion.startsWith("v") ? "v0.0.1" : "0.0.1");
|
|
11064
|
+
if (!isValidSemver(initialVersion)) {
|
|
11065
|
+
const errorMessage = `AutoVersionStep: baseVersion ${JSON.stringify(initialVersion)} is not a valid semver string (expected e.g. "1.2.3" or "v1.2.3"). Refusing to run to avoid shell injection into the placeholder-rewrite step.`;
|
|
11066
|
+
this.logger.error(errorMessage);
|
|
11067
|
+
return {
|
|
11068
|
+
executed: true,
|
|
11069
|
+
success: false,
|
|
11070
|
+
errorMessage
|
|
11071
|
+
};
|
|
11072
|
+
}
|
|
11073
|
+
this.logger.info(`AutoVersionStep: first generation \u2014 using initial version ${initialVersion}`);
|
|
11074
|
+
await service.replaceMagicVersion(this.outputDir, mappedMagicVersion, initialVersion);
|
|
11075
|
+
if (language === "go") {
|
|
11076
|
+
await service.addGoMajorVersionSuffix(this.outputDir, initialVersion);
|
|
11077
|
+
}
|
|
11078
|
+
const commitMessage = this.brandMessage("Initial SDK generation");
|
|
11079
|
+
const commitSha = this.commitAutoversion(commitMessage);
|
|
11080
|
+
return {
|
|
11081
|
+
executed: true,
|
|
11082
|
+
success: true,
|
|
11083
|
+
version: initialVersion,
|
|
11084
|
+
commitMessage,
|
|
11085
|
+
commitSha
|
|
11086
|
+
};
|
|
11087
|
+
}
|
|
11088
|
+
async handleNormalFlow(params) {
|
|
11089
|
+
const { prepared, service, language, mappedMagicVersion, previousGenerationSha } = params;
|
|
11090
|
+
const rawDiff = this.gitDiff(previousGenerationSha, prepared.currentGenerationSha);
|
|
11091
|
+
if (rawDiff.trim().length === 0) {
|
|
11092
|
+
this.logger.info("AutoVersionStep: empty diff between generations; skipping autoversion.");
|
|
11093
|
+
return { executed: true, success: true };
|
|
11094
|
+
}
|
|
11095
|
+
const previousVersion = await this.resolvePreviousVersion({ service, rawDiff, mappedMagicVersion });
|
|
11096
|
+
if (previousVersion == null) {
|
|
11097
|
+
return await this.handleFirstGeneration({ service, language, mappedMagicVersion });
|
|
11098
|
+
}
|
|
11099
|
+
const cleanedDiff = service.cleanDiffForAI(rawDiff, mappedMagicVersion);
|
|
11100
|
+
const rawBytes = Buffer.byteLength(rawDiff, "utf-8");
|
|
11101
|
+
const cleanedBytes = Buffer.byteLength(cleanedDiff, "utf-8");
|
|
11102
|
+
this.logger.debug(
|
|
11103
|
+
`AutoVersionStep: raw=${formatSizeKB(rawBytes)}KB (${countFilesInDiff(rawDiff)} files), cleaned=${formatSizeKB(cleanedBytes)}KB (${countFilesInDiff(cleanedDiff)} files)`
|
|
11104
|
+
);
|
|
11105
|
+
if (cleanedDiff.trim().length === 0) {
|
|
11106
|
+
this.logger.info("AutoVersionStep: cleaned diff is empty; no semantic changes.");
|
|
11107
|
+
return { executed: true, success: true };
|
|
11108
|
+
}
|
|
11109
|
+
if (cleanedBytes > MAX_RAW_DIFF_BYTES) {
|
|
11110
|
+
this.logger.warn(
|
|
11111
|
+
`AutoVersionStep: diff too large for FAI (${formatSizeKB(cleanedBytes)}KB, limit ${formatSizeKB(MAX_RAW_DIFF_BYTES)}KB). Falling back to PATCH.`
|
|
11112
|
+
);
|
|
11113
|
+
return await this.finalizeWithBump({
|
|
11114
|
+
service,
|
|
11115
|
+
language,
|
|
11116
|
+
mappedMagicVersion,
|
|
11117
|
+
previousVersion,
|
|
11118
|
+
analysis: { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") }
|
|
11119
|
+
});
|
|
11120
|
+
}
|
|
11121
|
+
const chunks = service.chunkDiff(cleanedDiff, MAX_AI_DIFF_BYTES);
|
|
11122
|
+
const cappedChunks = chunks.slice(0, MAX_CHUNKS);
|
|
11123
|
+
const skippedChunks = chunks.length - cappedChunks.length;
|
|
11124
|
+
if (chunks.length > 1) {
|
|
11125
|
+
this.logger.info(
|
|
11126
|
+
`AutoVersionStep: split diff into ${chunks.length} chunks` + (skippedChunks > 0 ? ` (capped at ${MAX_CHUNKS}, skipping ${skippedChunks})` : "")
|
|
11127
|
+
);
|
|
11128
|
+
}
|
|
11129
|
+
let analysis;
|
|
11130
|
+
try {
|
|
11131
|
+
analysis = cappedChunks.length <= 1 ? await this.analyzeSingle(cleanedDiff, language, previousVersion) : await this.analyzeChunks(cappedChunks, language, previousVersion);
|
|
11132
|
+
} catch (error) {
|
|
11133
|
+
this.logger.warn(`AutoVersionStep: FAI analysis failed (${String(error)}); falling back to PATCH bump.`);
|
|
11134
|
+
analysis = { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") };
|
|
11135
|
+
}
|
|
11136
|
+
if (analysis == null) {
|
|
11137
|
+
this.logger.info("AutoVersionStep: FAI returned NO_CHANGE; skipping autoversion commit.");
|
|
11138
|
+
return {
|
|
11139
|
+
executed: true,
|
|
11140
|
+
success: true,
|
|
11141
|
+
previousVersion,
|
|
11142
|
+
versionBump: "NO_CHANGE"
|
|
11143
|
+
};
|
|
11144
|
+
}
|
|
11145
|
+
return await this.finalizeWithBump({
|
|
11146
|
+
service,
|
|
11147
|
+
language,
|
|
11148
|
+
mappedMagicVersion,
|
|
11149
|
+
previousVersion,
|
|
11150
|
+
analysis
|
|
11151
|
+
});
|
|
11152
|
+
}
|
|
11153
|
+
async finalizeWithBump(params) {
|
|
11154
|
+
const { service, language, mappedMagicVersion, previousVersion, analysis } = params;
|
|
11155
|
+
const newVersion = incrementVersion(previousVersion, analysis.versionBump);
|
|
11156
|
+
this.logger.info(`AutoVersionStep: ${analysis.versionBump} bump: ${previousVersion} \u2192 ${newVersion}`);
|
|
11157
|
+
await service.replaceMagicVersion(this.outputDir, mappedMagicVersion, newVersion);
|
|
11158
|
+
if (language === "go") {
|
|
11159
|
+
await service.addGoMajorVersionSuffix(this.outputDir, newVersion);
|
|
11160
|
+
}
|
|
11161
|
+
if (analysis.changelogEntry && analysis.changelogEntry.trim().length > 0) {
|
|
11162
|
+
await this.prependChangelogEntry({ version: newVersion, entry: analysis.changelogEntry });
|
|
11163
|
+
}
|
|
11164
|
+
const commitSha = this.commitAutoversion(analysis.message);
|
|
11165
|
+
return {
|
|
11166
|
+
executed: true,
|
|
11167
|
+
success: true,
|
|
11168
|
+
version: newVersion,
|
|
11169
|
+
commitMessage: analysis.message,
|
|
11170
|
+
changelogEntry: analysis.changelogEntry,
|
|
11171
|
+
previousVersion,
|
|
11172
|
+
versionBump: analysis.versionBump,
|
|
11173
|
+
prDescription: analysis.prDescription,
|
|
11174
|
+
versionBumpReason: analysis.versionBumpReason,
|
|
11175
|
+
commitSha
|
|
11176
|
+
};
|
|
11177
|
+
}
|
|
11178
|
+
async resolvePreviousVersion(params) {
|
|
11179
|
+
const { service, rawDiff, mappedMagicVersion } = params;
|
|
11180
|
+
try {
|
|
11181
|
+
const extracted = service.extractPreviousVersion(rawDiff, mappedMagicVersion);
|
|
11182
|
+
if (extracted != null) {
|
|
11183
|
+
this.logger.debug(`AutoVersionStep: previous version from diff: ${extracted}`);
|
|
11184
|
+
return extracted;
|
|
11185
|
+
}
|
|
11186
|
+
} catch (error) {
|
|
11187
|
+
if (!(error instanceof AutoVersioningException) || !error.magicVersionAbsent) {
|
|
11188
|
+
throw error;
|
|
11189
|
+
}
|
|
11190
|
+
this.logger.info("AutoVersionStep: magic version not found in diff; falling back to metadata + git tags.");
|
|
11191
|
+
}
|
|
11192
|
+
const metadataVersion = await this.readVersionFromMetadata();
|
|
11193
|
+
if (metadataVersion != null) {
|
|
11194
|
+
return this.normalizeVersionPrefix(metadataVersion, mappedMagicVersion);
|
|
11195
|
+
}
|
|
11196
|
+
try {
|
|
11197
|
+
const tagVersion = await service.getLatestVersionFromGitTags(this.outputDir);
|
|
11198
|
+
if (tagVersion != null) {
|
|
11199
|
+
return this.normalizeVersionPrefix(tagVersion, mappedMagicVersion);
|
|
11200
|
+
}
|
|
11201
|
+
} catch (error) {
|
|
11202
|
+
this.logger.debug(`AutoVersionStep: git-tags fallback failed (${String(error)}); ignoring.`);
|
|
11203
|
+
}
|
|
11204
|
+
return null;
|
|
11205
|
+
}
|
|
11206
|
+
async readVersionFromMetadata() {
|
|
11207
|
+
try {
|
|
11208
|
+
const output = (0, import_child_process3.execFileSync)("git", ["show", "HEAD~1:.fern/metadata.json"], {
|
|
11209
|
+
cwd: this.outputDir,
|
|
11210
|
+
encoding: "utf-8",
|
|
11211
|
+
stdio: "pipe"
|
|
11212
|
+
});
|
|
11213
|
+
const parsed = JSON.parse(output);
|
|
11214
|
+
return parsed.sdkVersion ?? void 0;
|
|
11215
|
+
} catch {
|
|
11216
|
+
return void 0;
|
|
11217
|
+
}
|
|
11218
|
+
}
|
|
11219
|
+
normalizeVersionPrefix(version, mappedMagicVersion) {
|
|
11220
|
+
const stripped = version.startsWith("v") ? version.slice(1) : version;
|
|
11221
|
+
return mappedMagicVersion.startsWith("v") ? `v${stripped}` : stripped;
|
|
11222
|
+
}
|
|
11223
|
+
gitDiff(from, to) {
|
|
11224
|
+
return (0, import_child_process3.execFileSync)("git", ["diff", from, to, "--", ".", ":(exclude).fern/metadata.json"], {
|
|
11225
|
+
cwd: this.outputDir,
|
|
11226
|
+
encoding: "utf-8",
|
|
11227
|
+
stdio: "pipe",
|
|
11228
|
+
maxBuffer: 256 * 1024 * 1024
|
|
11229
|
+
});
|
|
11230
|
+
}
|
|
11231
|
+
commitAutoversion(message) {
|
|
11232
|
+
const subject = message.split("\n")[0] ?? message;
|
|
11233
|
+
const body = message.slice(subject.length).trimStart();
|
|
11234
|
+
const fullMessage = body.length > 0 ? `${COMMIT_MARKER} ${subject}
|
|
11235
|
+
|
|
11236
|
+
${body}` : `${COMMIT_MARKER} ${subject}`;
|
|
11237
|
+
(0, import_child_process3.execFileSync)("git", ["add", "-A"], { cwd: this.outputDir, stdio: "pipe" });
|
|
11238
|
+
(0, import_child_process3.execFileSync)("git", ["commit", "--allow-empty", "-m", fullMessage], {
|
|
11239
|
+
cwd: this.outputDir,
|
|
11240
|
+
stdio: "pipe"
|
|
11241
|
+
});
|
|
11242
|
+
return (0, import_child_process3.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
11243
|
+
cwd: this.outputDir,
|
|
11244
|
+
encoding: "utf-8",
|
|
11245
|
+
stdio: "pipe"
|
|
11246
|
+
}).trim();
|
|
11247
|
+
}
|
|
11248
|
+
async prependChangelogEntry(params) {
|
|
11249
|
+
const { version, entry } = params;
|
|
11250
|
+
const changelogPath = (0, import_path4.join)(this.outputDir, "changelog.md");
|
|
11251
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
11252
|
+
const header = `## [${version}] - ${now}
|
|
11253
|
+
`;
|
|
11254
|
+
const newBlock = `${header}${entry.trim()}
|
|
11255
|
+
|
|
11256
|
+
`;
|
|
11257
|
+
let existing = "";
|
|
11258
|
+
if ((0, import_fs3.existsSync)(changelogPath)) {
|
|
11259
|
+
existing = await (0, import_promises3.readFile)(changelogPath, "utf-8");
|
|
11260
|
+
}
|
|
11261
|
+
let output;
|
|
11262
|
+
if (existing.trim().length === 0) {
|
|
11263
|
+
output = `# Changelog
|
|
11264
|
+
|
|
11265
|
+
${newBlock}`;
|
|
11266
|
+
} else if (existing.startsWith("# Changelog")) {
|
|
11267
|
+
const newlineIdx = existing.indexOf("\n");
|
|
11268
|
+
const headerLine = newlineIdx >= 0 ? existing.slice(0, newlineIdx) : existing;
|
|
11269
|
+
const remainder = (newlineIdx >= 0 ? existing.slice(newlineIdx + 1) : "").replace(/^\s*\n/, "");
|
|
11270
|
+
output = `${headerLine}
|
|
11271
|
+
|
|
11272
|
+
${newBlock}${remainder}`;
|
|
11273
|
+
} else {
|
|
11274
|
+
output = `${newBlock}${existing}`;
|
|
11275
|
+
}
|
|
11276
|
+
await (0, import_promises3.writeFile)(changelogPath, output, "utf-8");
|
|
11277
|
+
}
|
|
11278
|
+
brandMessage(message) {
|
|
11279
|
+
if (this.config.isWhitelabel) {
|
|
11280
|
+
return message;
|
|
11281
|
+
}
|
|
11282
|
+
if (message.includes("\u{1F33F} Generated with Fern")) {
|
|
11283
|
+
return message;
|
|
11284
|
+
}
|
|
11285
|
+
return `${message.trimEnd()}${FERN_TRAILER}`;
|
|
11286
|
+
}
|
|
11287
|
+
async analyzeSingle(cleanedDiff, language, previousVersion) {
|
|
11288
|
+
const { client, VersionBump: VersionBump2 } = await this.loadBaml();
|
|
11289
|
+
const result = await client.AnalyzeSdkDiff(
|
|
11290
|
+
cleanedDiff,
|
|
11291
|
+
language,
|
|
11292
|
+
previousVersion,
|
|
11293
|
+
this.config.priorChangelog ?? "",
|
|
11294
|
+
this.config.specCommitMessage ?? ""
|
|
11295
|
+
);
|
|
11296
|
+
if (result.version_bump === VersionBump2.NO_CHANGE) {
|
|
11297
|
+
return null;
|
|
11298
|
+
}
|
|
11299
|
+
return {
|
|
11300
|
+
versionBump: result.version_bump,
|
|
11301
|
+
message: result.message,
|
|
11302
|
+
changelogEntry: result.changelog_entry || void 0,
|
|
11303
|
+
versionBumpReason: result.version_bump_reason || void 0
|
|
11304
|
+
};
|
|
11305
|
+
}
|
|
11306
|
+
async analyzeChunks(chunks, language, previousVersion) {
|
|
11307
|
+
const { client, VersionBump: VersionBump2 } = await this.loadBaml();
|
|
11308
|
+
let bestBump = VersionBump2.NO_CHANGE;
|
|
11309
|
+
let bestMessage = "";
|
|
11310
|
+
let bestVersionBumpReason;
|
|
11311
|
+
const changelogEntries = [];
|
|
11312
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
11313
|
+
const chunk = chunks[i];
|
|
11314
|
+
if (!chunk) {
|
|
11315
|
+
continue;
|
|
11316
|
+
}
|
|
11317
|
+
const analysis = await client.AnalyzeSdkDiff(
|
|
11318
|
+
chunk,
|
|
11319
|
+
language,
|
|
11320
|
+
previousVersion,
|
|
11321
|
+
this.config.priorChangelog ?? "",
|
|
11322
|
+
this.config.specCommitMessage ?? ""
|
|
11323
|
+
);
|
|
11324
|
+
if (analysis.version_bump === VersionBump2.NO_CHANGE) {
|
|
11325
|
+
continue;
|
|
11326
|
+
}
|
|
11327
|
+
const prev = bestBump;
|
|
11328
|
+
bestBump = maxVersionBump(bestBump, analysis.version_bump);
|
|
11329
|
+
if (bestBump !== prev) {
|
|
11330
|
+
bestMessage = analysis.message;
|
|
11331
|
+
bestVersionBumpReason = analysis.version_bump_reason;
|
|
11332
|
+
}
|
|
11333
|
+
const entry = analysis.changelog_entry?.trim();
|
|
11334
|
+
if (entry) {
|
|
11335
|
+
changelogEntries.push(entry);
|
|
11336
|
+
}
|
|
11337
|
+
}
|
|
11338
|
+
if (bestBump === VersionBump2.NO_CHANGE) {
|
|
11339
|
+
return null;
|
|
11340
|
+
}
|
|
11341
|
+
if (changelogEntries.length <= 1) {
|
|
11342
|
+
return {
|
|
11343
|
+
versionBump: bestBump,
|
|
11344
|
+
message: bestMessage,
|
|
11345
|
+
changelogEntry: changelogEntries[0],
|
|
11346
|
+
versionBumpReason: bestVersionBumpReason
|
|
11347
|
+
};
|
|
11348
|
+
}
|
|
11349
|
+
try {
|
|
11350
|
+
const consolidated = await client.ConsolidateChangelog(
|
|
11351
|
+
changelogEntries.join("\n\n"),
|
|
11352
|
+
bestBump,
|
|
11353
|
+
language,
|
|
11354
|
+
previousVersion,
|
|
11355
|
+
incrementVersion(previousVersion, bestBump)
|
|
11356
|
+
);
|
|
11357
|
+
return {
|
|
11358
|
+
versionBump: bestBump,
|
|
11359
|
+
message: bestMessage,
|
|
11360
|
+
changelogEntry: consolidated.consolidated_changelog,
|
|
11361
|
+
prDescription: consolidated.pr_description || void 0,
|
|
11362
|
+
versionBumpReason: consolidated.version_bump_reason || bestVersionBumpReason
|
|
11363
|
+
};
|
|
11364
|
+
} catch (error) {
|
|
11365
|
+
this.logger.warn(`AutoVersionStep: ConsolidateChangelog failed (${String(error)}); using joined entries.`);
|
|
11366
|
+
return {
|
|
11367
|
+
versionBump: bestBump,
|
|
11368
|
+
message: bestMessage,
|
|
11369
|
+
changelogEntry: changelogEntries.join("\n\n"),
|
|
11370
|
+
versionBumpReason: bestVersionBumpReason
|
|
11371
|
+
};
|
|
11372
|
+
}
|
|
11373
|
+
}
|
|
11374
|
+
/**
|
|
11375
|
+
* Dynamically imports @fern-api/cli-ai only when autoversion actually needs to
|
|
11376
|
+
* call FAI. The package is ESM-only with extensionless internal imports, so
|
|
11377
|
+
* unbundled CJS consumers (like generator-cli's `bin/cli` used by the README /
|
|
11378
|
+
* reference commands) would fail at load time with a static import — but those
|
|
11379
|
+
* commands never reach this method, so dynamic import keeps them unaffected.
|
|
11380
|
+
*/
|
|
11381
|
+
async loadBaml() {
|
|
11382
|
+
if (this.config.ai == null) {
|
|
11383
|
+
throw new Error("AutoVersionStep: ai config is missing. Set autoVersion.ai to a BAML provider+model pair.");
|
|
11384
|
+
}
|
|
11385
|
+
const cliAi = await import("@fern-api/cli-ai");
|
|
11386
|
+
const registry = cliAi.configureBamlClient(this.config.ai);
|
|
11387
|
+
return {
|
|
11388
|
+
client: cliAi.b.withOptions({ clientRegistry: registry }),
|
|
11389
|
+
VersionBump: cliAi.VersionBump
|
|
11390
|
+
};
|
|
11391
|
+
}
|
|
11392
|
+
/**
|
|
11393
|
+
* AutoVersioningService was built against the CLI's TaskContext logger — a richer
|
|
11394
|
+
* shape with `trace`, `log`, `enable`, `disable`. Our pipeline only has the four
|
|
11395
|
+
* core levels; stub the rest as no-ops and adapt the method signatures. Cast to
|
|
11396
|
+
* the service's expected Logger type because the surface is structurally wider
|
|
11397
|
+
* than what PipelineLogger exposes.
|
|
11398
|
+
*/
|
|
11399
|
+
toTaskLogger() {
|
|
11400
|
+
const noop = () => void 0;
|
|
11401
|
+
const adapter = {
|
|
11402
|
+
debug: (msg) => this.logger.debug(msg),
|
|
11403
|
+
info: (msg) => this.logger.info(msg),
|
|
11404
|
+
warn: (msg) => this.logger.warn(msg),
|
|
11405
|
+
error: (msg) => this.logger.error(msg),
|
|
11406
|
+
trace: (msg) => this.logger.debug(msg),
|
|
11407
|
+
log: (msg) => this.logger.info(msg),
|
|
11408
|
+
enable: noop,
|
|
11409
|
+
disable: noop
|
|
11410
|
+
};
|
|
11411
|
+
return adapter;
|
|
11412
|
+
}
|
|
11413
|
+
};
|
|
11414
|
+
|
|
11415
|
+
// src/replay/replay-run.ts
|
|
11416
|
+
var import_replay = require("@fern-api/replay");
|
|
11417
|
+
var import_child_process4 = require("child_process");
|
|
11418
|
+
var import_fs5 = require("fs");
|
|
11419
|
+
var import_path6 = require("path");
|
|
11420
|
+
|
|
11421
|
+
// src/replay/fernignore.ts
|
|
11422
|
+
var import_fs4 = require("fs");
|
|
11423
|
+
var import_promises4 = require("fs/promises");
|
|
11424
|
+
var import_path5 = require("path");
|
|
11425
|
+
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
11426
|
+
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
11427
|
+
async function ensureReplayFernignoreEntries(outputDir) {
|
|
11428
|
+
const fernignorePath = (0, import_path5.join)(outputDir, ".fernignore");
|
|
11429
|
+
let content = "";
|
|
11430
|
+
try {
|
|
11431
|
+
content = await (0, import_promises4.readFile)(fernignorePath, "utf-8");
|
|
11432
|
+
} catch {
|
|
11433
|
+
}
|
|
11434
|
+
const lines = content.split("\n");
|
|
11435
|
+
const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
11436
|
+
if (toAdd.length > 0) {
|
|
11437
|
+
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
11438
|
+
await (0, import_promises4.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
11439
|
+
return true;
|
|
11440
|
+
}
|
|
11441
|
+
return false;
|
|
11442
|
+
}
|
|
11443
|
+
async function ensureGitattributesEntries(outputDir) {
|
|
11444
|
+
const gitattributesPath = (0, import_path5.join)(outputDir, ".gitattributes");
|
|
11445
|
+
let content = "";
|
|
11446
|
+
try {
|
|
11447
|
+
content = await (0, import_promises4.readFile)(gitattributesPath, "utf-8");
|
|
11448
|
+
} catch {
|
|
11449
|
+
}
|
|
11450
|
+
const lines = content.split("\n");
|
|
11451
|
+
const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
11452
|
+
if (toAdd.length > 0) {
|
|
11453
|
+
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
11454
|
+
await (0, import_promises4.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
11455
|
+
}
|
|
11456
|
+
}
|
|
11457
|
+
function ensureReplayFernignoreEntriesSync(outputDir) {
|
|
11458
|
+
const fernignorePath = (0, import_path5.join)(outputDir, ".fernignore");
|
|
11459
|
+
let content = "";
|
|
11460
|
+
if ((0, import_fs4.existsSync)(fernignorePath)) {
|
|
11461
|
+
content = (0, import_fs4.readFileSync)(fernignorePath, "utf-8");
|
|
11462
|
+
}
|
|
11463
|
+
const lines = content.split("\n");
|
|
11464
|
+
const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
11465
|
+
if (toAdd.length > 0) {
|
|
11466
|
+
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
11467
|
+
(0, import_fs4.writeFileSync)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
11468
|
+
}
|
|
11469
|
+
}
|
|
11470
|
+
function ensureGitattributesEntriesSync(outputDir) {
|
|
11471
|
+
const gitattributesPath = (0, import_path5.join)(outputDir, ".gitattributes");
|
|
11472
|
+
let content = "";
|
|
11473
|
+
if ((0, import_fs4.existsSync)(gitattributesPath)) {
|
|
11474
|
+
content = (0, import_fs4.readFileSync)(gitattributesPath, "utf-8");
|
|
11475
|
+
}
|
|
11476
|
+
const lines = content.split("\n");
|
|
11477
|
+
const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
11478
|
+
if (toAdd.length > 0) {
|
|
11479
|
+
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
11480
|
+
(0, import_fs4.writeFileSync)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
11481
|
+
}
|
|
11482
|
+
}
|
|
11483
|
+
|
|
11484
|
+
// src/replay/replay-run.ts
|
|
11485
|
+
async function replayPrepare(params) {
|
|
11486
|
+
const {
|
|
11487
|
+
outputDir,
|
|
11488
|
+
cliVersion,
|
|
11489
|
+
generatorVersions,
|
|
11490
|
+
stageOnly = false,
|
|
11491
|
+
generatorName,
|
|
11492
|
+
skipApplication,
|
|
11493
|
+
logger
|
|
11494
|
+
} = params;
|
|
11495
|
+
const lockfilePath = (0, import_path6.join)(outputDir, ".fern", "replay.lock");
|
|
11496
|
+
if (!(0, import_fs5.existsSync)(lockfilePath)) {
|
|
11497
|
+
return null;
|
|
11498
|
+
}
|
|
11499
|
+
const baseBranchHead = gitRevParse(outputDir, "HEAD");
|
|
11500
|
+
let previousGenerationSha = null;
|
|
11501
|
+
let prevBaseBranchHead = null;
|
|
11502
|
+
try {
|
|
11503
|
+
const lockManager = new import_replay.LockfileManager(outputDir);
|
|
11504
|
+
if (lockManager.exists()) {
|
|
11505
|
+
const lock = lockManager.read();
|
|
11506
|
+
previousGenerationSha = lock.current_generation;
|
|
11507
|
+
const latestGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
11508
|
+
prevBaseBranchHead = latestGen?.base_branch_head ?? null;
|
|
11509
|
+
}
|
|
11510
|
+
} catch {
|
|
11511
|
+
}
|
|
11512
|
+
if (previousGenerationSha != null) {
|
|
11513
|
+
const sanitizedName = generatorName?.replace(/\//g, "--");
|
|
11514
|
+
const namespacedTag = sanitizedName != null ? `fern-generation-base--${sanitizedName}` : null;
|
|
11515
|
+
const tagSha = (namespacedTag != null ? gitRevParse(outputDir, namespacedTag) : null) ?? gitRevParse(outputDir, "fern-generation-base");
|
|
11516
|
+
let shouldSync = false;
|
|
11517
|
+
if (tagSha != null) {
|
|
11518
|
+
const tagParent = gitRevParse(outputDir, `${tagSha}^`);
|
|
11519
|
+
if (tagParent === prevBaseBranchHead || tagParent === previousGenerationSha) {
|
|
11520
|
+
const tagDistance = gitDiffNameOnly(outputDir, tagSha, "HEAD").length;
|
|
11521
|
+
const lockDistance = gitDiffNameOnly(
|
|
11522
|
+
outputDir,
|
|
11523
|
+
prevBaseBranchHead ?? previousGenerationSha,
|
|
11524
|
+
"HEAD"
|
|
11525
|
+
).length;
|
|
11526
|
+
shouldSync = tagDistance < lockDistance;
|
|
11527
|
+
}
|
|
11528
|
+
}
|
|
11529
|
+
if (shouldSync && tagSha != null) {
|
|
11530
|
+
if (!isTagMergedIntoHead(outputDir, tagSha)) {
|
|
11531
|
+
logger?.warn(
|
|
11532
|
+
`fern-generation-base tag ${tagSha} is not reachable from HEAD \u2014 skipping divergent-merge sync. The tag likely points at an unmerged generation (PR closed without merge).`
|
|
11533
|
+
);
|
|
11534
|
+
} else {
|
|
11535
|
+
const syncService = new import_replay.ReplayService(outputDir, { enabled: true });
|
|
11536
|
+
await syncService.syncFromDivergentMerge(tagSha, {
|
|
11537
|
+
cliVersion,
|
|
11538
|
+
generatorVersions,
|
|
11539
|
+
baseBranchHead: baseBranchHead ?? void 0
|
|
11540
|
+
});
|
|
11541
|
+
try {
|
|
11542
|
+
const freshLockManager = new import_replay.LockfileManager(outputDir);
|
|
11543
|
+
if (freshLockManager.exists()) {
|
|
11544
|
+
previousGenerationSha = freshLockManager.read().current_generation;
|
|
11545
|
+
}
|
|
11546
|
+
} catch {
|
|
11547
|
+
}
|
|
11548
|
+
}
|
|
11549
|
+
}
|
|
11550
|
+
}
|
|
11551
|
+
const service = new import_replay.ReplayService(outputDir, { enabled: true });
|
|
11552
|
+
let preparation;
|
|
11553
|
+
try {
|
|
11554
|
+
preparation = await service.prepareReplay({
|
|
11555
|
+
cliVersion,
|
|
11556
|
+
generatorVersions,
|
|
11557
|
+
stageOnly,
|
|
11558
|
+
skipApplication,
|
|
11559
|
+
baseBranchHead: baseBranchHead ?? void 0
|
|
11560
|
+
});
|
|
11561
|
+
} catch (error) {
|
|
11562
|
+
logger?.warn("Replay failed, continuing without replay: " + String(error));
|
|
11563
|
+
return null;
|
|
11564
|
+
}
|
|
11565
|
+
return {
|
|
11566
|
+
_service: service,
|
|
11567
|
+
_preparation: preparation,
|
|
11568
|
+
outputDir,
|
|
11569
|
+
flow: preparation.flow,
|
|
11570
|
+
previousGenerationSha,
|
|
11571
|
+
currentGenerationSha: preparation.currentGenerationSha,
|
|
11572
|
+
baseBranchHead
|
|
11573
|
+
};
|
|
11574
|
+
}
|
|
11575
|
+
async function replayApply(prepared, params = {}) {
|
|
11576
|
+
const { stageOnly = false, logger } = params;
|
|
11577
|
+
let report;
|
|
11578
|
+
try {
|
|
11579
|
+
report = await prepared._service.applyPreparedReplay(prepared._preparation, { stageOnly });
|
|
11580
|
+
} catch (error) {
|
|
11581
|
+
logger?.warn("Replay failed, continuing without replay: " + String(error));
|
|
11582
|
+
return {
|
|
11583
|
+
report: null,
|
|
11584
|
+
fernignoreUpdated: false,
|
|
11585
|
+
previousGenerationSha: prepared.previousGenerationSha,
|
|
11586
|
+
currentGenerationSha: prepared.currentGenerationSha,
|
|
11587
|
+
baseBranchHead: prepared.baseBranchHead
|
|
11588
|
+
};
|
|
11589
|
+
}
|
|
11590
|
+
let resolvedBaseBranchHead = prepared.baseBranchHead;
|
|
11591
|
+
try {
|
|
11592
|
+
const freshLockManager = new import_replay.LockfileManager(prepared.outputDir);
|
|
11593
|
+
if (freshLockManager.exists()) {
|
|
11594
|
+
const freshLock = freshLockManager.read();
|
|
11595
|
+
const latestGen = freshLock.generations.find((g2) => g2.commit_sha === freshLock.current_generation);
|
|
11596
|
+
if (latestGen?.base_branch_head) {
|
|
11597
|
+
resolvedBaseBranchHead = latestGen.base_branch_head;
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11600
|
+
} catch {
|
|
11601
|
+
}
|
|
11602
|
+
const fernignoreUpdated = await ensureReplayFernignoreEntries(prepared.outputDir);
|
|
11603
|
+
await ensureGitattributesEntries(prepared.outputDir);
|
|
11604
|
+
return {
|
|
11605
|
+
report,
|
|
11606
|
+
fernignoreUpdated,
|
|
11607
|
+
previousGenerationSha: prepared.previousGenerationSha,
|
|
11608
|
+
currentGenerationSha: prepared.currentGenerationSha,
|
|
11609
|
+
baseBranchHead: resolvedBaseBranchHead
|
|
11610
|
+
};
|
|
11611
|
+
}
|
|
11612
|
+
async function replayRun(params) {
|
|
11613
|
+
const prepared = await replayPrepare(params);
|
|
11614
|
+
if (prepared == null) {
|
|
11615
|
+
return {
|
|
11616
|
+
report: null,
|
|
11617
|
+
fernignoreUpdated: false,
|
|
11618
|
+
previousGenerationSha: null,
|
|
11619
|
+
currentGenerationSha: null,
|
|
11620
|
+
baseBranchHead: null
|
|
11621
|
+
};
|
|
11622
|
+
}
|
|
11623
|
+
return replayApply(prepared, { stageOnly: params.stageOnly, logger: params.logger });
|
|
11624
|
+
}
|
|
11625
|
+
function isTagMergedIntoHead(cwd2, tagSha) {
|
|
11626
|
+
try {
|
|
11627
|
+
(0, import_child_process4.execFileSync)("git", ["merge-base", "--is-ancestor", tagSha, "HEAD"], {
|
|
11628
|
+
cwd: cwd2,
|
|
11629
|
+
stdio: "pipe"
|
|
11630
|
+
});
|
|
11631
|
+
return true;
|
|
11632
|
+
} catch {
|
|
11633
|
+
}
|
|
11634
|
+
const tagTree = gitRevParse(cwd2, `${tagSha}^{tree}`);
|
|
11635
|
+
if (tagTree == null) {
|
|
11636
|
+
return false;
|
|
11637
|
+
}
|
|
11638
|
+
try {
|
|
11639
|
+
const trees = (0, import_child_process4.execFileSync)("git", ["log", "HEAD", "--format=%T", "--max-count=500"], {
|
|
11640
|
+
cwd: cwd2,
|
|
11641
|
+
encoding: "utf-8",
|
|
11642
|
+
stdio: "pipe"
|
|
11643
|
+
});
|
|
11644
|
+
return trees.split("\n").some((t2) => t2.trim() === tagTree);
|
|
11645
|
+
} catch {
|
|
11646
|
+
return false;
|
|
11647
|
+
}
|
|
11648
|
+
}
|
|
11649
|
+
function gitRevParse(cwd2, rev) {
|
|
11650
|
+
try {
|
|
11651
|
+
return (0, import_child_process4.execFileSync)("git", ["rev-parse", "--verify", rev], {
|
|
11652
|
+
cwd: cwd2,
|
|
11653
|
+
encoding: "utf-8",
|
|
11654
|
+
stdio: "pipe"
|
|
11655
|
+
}).trim();
|
|
11656
|
+
} catch {
|
|
11657
|
+
return null;
|
|
11658
|
+
}
|
|
11659
|
+
}
|
|
11660
|
+
function gitDiffNameOnly(cwd2, from, to) {
|
|
11661
|
+
try {
|
|
11662
|
+
const output = (0, import_child_process4.execFileSync)("git", ["diff", "--name-only", from, to], {
|
|
11663
|
+
cwd: cwd2,
|
|
11664
|
+
encoding: "utf-8",
|
|
11665
|
+
stdio: "pipe"
|
|
11666
|
+
});
|
|
11667
|
+
return output.trim().split("\n").filter(Boolean);
|
|
11668
|
+
} catch {
|
|
11669
|
+
return [];
|
|
11670
|
+
}
|
|
11671
|
+
}
|
|
11672
|
+
|
|
11673
|
+
// src/pipeline/steps/GenerationCommitStep.ts
|
|
11674
|
+
var GenerationCommitStep = class extends BaseStep {
|
|
11675
|
+
constructor(outputDir, logger, config, cliVersion, generatorVersions, generatorName) {
|
|
11676
|
+
super(outputDir, logger);
|
|
11677
|
+
this.config = config;
|
|
11678
|
+
this.cliVersion = cliVersion;
|
|
11679
|
+
this.generatorVersions = generatorVersions;
|
|
11680
|
+
this.generatorName = generatorName;
|
|
11681
|
+
}
|
|
11682
|
+
config;
|
|
11683
|
+
cliVersion;
|
|
11684
|
+
generatorVersions;
|
|
11685
|
+
generatorName;
|
|
11686
|
+
name = "generationCommit";
|
|
11687
|
+
async execute(_context) {
|
|
11688
|
+
const prepared = await replayPrepare({
|
|
11689
|
+
outputDir: this.outputDir,
|
|
11690
|
+
cliVersion: this.cliVersion,
|
|
11691
|
+
generatorVersions: this.generatorVersions,
|
|
11692
|
+
generatorName: this.generatorName,
|
|
11693
|
+
skipApplication: this.config.skipApplication,
|
|
11694
|
+
logger: this.logger
|
|
11695
|
+
});
|
|
11696
|
+
if (prepared == null) {
|
|
11697
|
+
return {
|
|
11698
|
+
executed: true,
|
|
11699
|
+
success: true,
|
|
11700
|
+
preparedReplay: null
|
|
11701
|
+
};
|
|
11702
|
+
}
|
|
11703
|
+
return {
|
|
11704
|
+
executed: true,
|
|
11705
|
+
success: true,
|
|
11706
|
+
preparedReplay: prepared,
|
|
11707
|
+
previousGenerationSha: prepared.previousGenerationSha ?? void 0,
|
|
11708
|
+
currentGenerationSha: prepared.currentGenerationSha,
|
|
11709
|
+
baseBranchHead: prepared.baseBranchHead ?? void 0,
|
|
11710
|
+
flow: prepared.flow
|
|
11711
|
+
};
|
|
11712
|
+
}
|
|
11713
|
+
};
|
|
11714
|
+
|
|
11715
|
+
// src/pipeline/steps/GithubStep.ts
|
|
11716
|
+
var import_rest2 = require("@octokit/rest");
|
|
11717
|
+
var import_promises5 = require("fs/promises");
|
|
11718
|
+
var import_path7 = require("path");
|
|
11719
|
+
|
|
11720
|
+
// src/pipeline/github/createReplayBranch.ts
|
|
11721
|
+
async function createReplayBranch(repository, branchName, commitMessage, replayConflictInfo, logger) {
|
|
11722
|
+
if (replayConflictInfo == null) {
|
|
11723
|
+
await repository.createBranchFromHead(branchName);
|
|
11724
|
+
return void 0;
|
|
11725
|
+
}
|
|
11726
|
+
const parentSha = replayConflictInfo.previousGenerationSha;
|
|
11727
|
+
await repository.createBranchFromHead(branchName);
|
|
11728
|
+
const genTreeHash = await repository.getCommitTreeHash(replayConflictInfo.currentGenerationSha);
|
|
11729
|
+
const tagCommitSha = await repository.commitTree(
|
|
11730
|
+
genTreeHash,
|
|
11731
|
+
parentSha,
|
|
11732
|
+
`[fern-generated] ${commitMessage ?? "Update SDK"}`
|
|
11733
|
+
);
|
|
11734
|
+
return tagCommitSha;
|
|
11735
|
+
}
|
|
11736
|
+
|
|
11737
|
+
// src/pipeline/github/findExistingUpdatablePR.ts
|
|
11738
|
+
async function findExistingUpdatablePR(octokit, owner, repo, baseBranch, logger) {
|
|
11739
|
+
try {
|
|
11740
|
+
const { data: pulls } = await octokit.pulls.list({
|
|
11741
|
+
owner,
|
|
11742
|
+
repo,
|
|
11743
|
+
state: "open",
|
|
11744
|
+
base: baseBranch,
|
|
11745
|
+
sort: "updated",
|
|
11746
|
+
direction: "desc",
|
|
11747
|
+
per_page: 20
|
|
11748
|
+
});
|
|
11749
|
+
for (const pr of pulls) {
|
|
11750
|
+
const prAuthor = pr.user?.login;
|
|
11751
|
+
if (prAuthor !== FERN_BOT_LOGIN) {
|
|
11752
|
+
logger.debug(`PR #${pr.number}: author ${prAuthor} is not ${FERN_BOT_LOGIN}, checking commits anyway`);
|
|
11753
|
+
}
|
|
11754
|
+
if (!pr.head.ref.startsWith("fern-bot/")) {
|
|
11755
|
+
logger.debug(`PR #${pr.number} skipped: branch ${pr.head.ref} does not start with fern-bot/`);
|
|
11756
|
+
continue;
|
|
11757
|
+
}
|
|
11758
|
+
const hasOnlyGenerationCommits = await checkPRHasOnlyGenerationCommits(
|
|
11759
|
+
octokit,
|
|
11760
|
+
owner,
|
|
11761
|
+
repo,
|
|
11762
|
+
pr.number,
|
|
11763
|
+
logger
|
|
11764
|
+
);
|
|
11765
|
+
if (hasOnlyGenerationCommits) {
|
|
11766
|
+
logger.debug(`Found existing updatable PR #${pr.number} with branch ${pr.head.ref}`);
|
|
11767
|
+
return {
|
|
11768
|
+
number: pr.number,
|
|
11769
|
+
headBranch: pr.head.ref,
|
|
11770
|
+
htmlUrl: pr.html_url,
|
|
11771
|
+
isDraft: pr.draft ?? false,
|
|
11772
|
+
nodeId: pr.node_id
|
|
11773
|
+
};
|
|
11774
|
+
} else {
|
|
11775
|
+
logger.debug(`PR #${pr.number} skipped: contains non-generation commits`);
|
|
11776
|
+
}
|
|
11777
|
+
}
|
|
11778
|
+
return void 0;
|
|
11779
|
+
} catch (error) {
|
|
11780
|
+
logger.debug(`Error finding existing PRs: ${extractErrorMessage(error)}`);
|
|
11781
|
+
return void 0;
|
|
11782
|
+
}
|
|
11783
|
+
}
|
|
11784
|
+
async function checkPRHasOnlyGenerationCommits(octokit, owner, repo, pullNumber, logger) {
|
|
11785
|
+
try {
|
|
11786
|
+
const { data: commits } = await octokit.pulls.listCommits({
|
|
11787
|
+
owner,
|
|
11788
|
+
repo,
|
|
11789
|
+
pull_number: pullNumber,
|
|
11790
|
+
per_page: 100
|
|
11791
|
+
});
|
|
11792
|
+
for (const commit of commits) {
|
|
11793
|
+
const authorLogin = commit.author?.login;
|
|
11794
|
+
const authorEmail = commit.commit.author?.email;
|
|
11795
|
+
const commitMessage = commit.commit.message ?? "";
|
|
11796
|
+
const isFernAuthor = authorLogin === FERN_BOT_LOGIN || authorEmail === "115122769+fern-api[bot]@users.noreply.github.com" || commit.commit.author?.name === "fern-api";
|
|
11797
|
+
const isFernCommitMessage = commitMessage.startsWith("[fern-generated]") || commitMessage.startsWith("[fern-replay]");
|
|
11798
|
+
if (!isFernAuthor && !isFernCommitMessage) {
|
|
11799
|
+
logger.debug(
|
|
11800
|
+
`Commit ${commit.sha.substring(0, 7)} is not a generation commit: author=${authorLogin}, email=${authorEmail}, message="${commitMessage.slice(0, 40)}"`
|
|
11801
|
+
);
|
|
11802
|
+
return false;
|
|
11803
|
+
}
|
|
11804
|
+
}
|
|
11805
|
+
return true;
|
|
11806
|
+
} catch (error) {
|
|
11807
|
+
logger.debug(`Error checking PR commits: ${extractErrorMessage(error)}`);
|
|
11808
|
+
return false;
|
|
11809
|
+
}
|
|
11810
|
+
}
|
|
11811
|
+
|
|
11812
|
+
// src/pipeline/github/parseCommitMessage.ts
|
|
11813
|
+
function parseCommitMessageForPR(commitMessage, changelogEntry, prDescription, versionBumpReason, previousVersion, newVersion, versionBump, changelogUrl) {
|
|
11814
|
+
const lines = commitMessage.split("\n");
|
|
11815
|
+
const prTitle = lines[0]?.trim() || "SDK Generation";
|
|
11816
|
+
const bodyFromCommit = lines.slice(1).join("\n").trim() || "Automated SDK generation by Fern";
|
|
11817
|
+
let prBody = prDescription?.trim() || changelogEntry?.trim() || bodyFromCommit;
|
|
11818
|
+
const isBreaking = versionBump === "MAJOR";
|
|
11819
|
+
if (previousVersion?.trim() && newVersion?.trim()) {
|
|
11820
|
+
const emoji = isBreaking ? "\u26A0\uFE0F " : "";
|
|
11821
|
+
const header = `## ${emoji}${previousVersion.trim()} \u2192 ${newVersion.trim()}`;
|
|
11822
|
+
if (isBreaking && versionBumpReason?.trim()) {
|
|
11823
|
+
prBody = `${header}
|
|
11824
|
+
|
|
11825
|
+
**Breaking:** ${versionBumpReason.trim()}
|
|
11826
|
+
|
|
11827
|
+
${prBody}`;
|
|
11828
|
+
} else {
|
|
11829
|
+
prBody = `${header}
|
|
11830
|
+
|
|
11831
|
+
${prBody}`;
|
|
11832
|
+
}
|
|
11833
|
+
} else if (isBreaking && versionBumpReason?.trim()) {
|
|
11834
|
+
prBody = `\u26A0\uFE0F **Breaking:** ${versionBumpReason.trim()}
|
|
11835
|
+
|
|
11836
|
+
${prBody}`;
|
|
11837
|
+
}
|
|
11838
|
+
if (changelogUrl?.trim()) {
|
|
11839
|
+
prBody += `
|
|
11840
|
+
|
|
11841
|
+
---
|
|
11842
|
+
|
|
11843
|
+
[See full changelog](${changelogUrl.trim()})`;
|
|
11844
|
+
}
|
|
11845
|
+
return { prTitle, prBody };
|
|
11846
|
+
}
|
|
11847
|
+
|
|
11848
|
+
// src/pipeline/github/pushSignedCommit.ts
|
|
11849
|
+
var MAX_CONCURRENT_PUSH_RETRIES = 3;
|
|
11850
|
+
async function pushSignedCommit({
|
|
11851
|
+
repository,
|
|
11852
|
+
octokit,
|
|
11853
|
+
owner,
|
|
11854
|
+
repo,
|
|
11855
|
+
branch,
|
|
11856
|
+
force,
|
|
11857
|
+
rebaseOnConflict = false,
|
|
11858
|
+
logger
|
|
11859
|
+
}) {
|
|
11860
|
+
const tempRef = `refs/temp/fern-${Date.now()}`;
|
|
11861
|
+
let tempRefPushed = false;
|
|
11862
|
+
try {
|
|
11863
|
+
let [localHeadSha, treeSha, message, parents] = await Promise.all([
|
|
11864
|
+
repository.getHeadSha(),
|
|
11865
|
+
repository.getHeadTreeHash(),
|
|
11866
|
+
repository.getHeadCommitMessage(),
|
|
11867
|
+
repository.getHeadParents()
|
|
11868
|
+
]);
|
|
11869
|
+
await repository.pushObjectToRef(localHeadSha, tempRef);
|
|
11870
|
+
tempRefPushed = true;
|
|
11871
|
+
for (let attempt = 0; attempt < MAX_CONCURRENT_PUSH_RETRIES; attempt++) {
|
|
11872
|
+
const { data: signedCommit } = await octokit.git.createCommit({
|
|
11873
|
+
owner,
|
|
11874
|
+
repo,
|
|
11875
|
+
message,
|
|
11876
|
+
tree: treeSha,
|
|
11877
|
+
parents
|
|
11878
|
+
});
|
|
11879
|
+
const signedSha = signedCommit.sha;
|
|
11880
|
+
try {
|
|
11881
|
+
await upsertBranchRef({ octokit, owner, repo, branch, sha: signedSha, force });
|
|
11882
|
+
logger.debug(`Updated refs/heads/${branch} to signed commit ${signedSha}`);
|
|
11883
|
+
await syncLocalToSignedCommit({ repository, branch, signedSha });
|
|
11884
|
+
return signedSha;
|
|
11885
|
+
} catch (err) {
|
|
11886
|
+
if (!isNonFastForwardError(err) || force || attempt >= MAX_CONCURRENT_PUSH_RETRIES - 1) {
|
|
11887
|
+
throw err;
|
|
11888
|
+
}
|
|
11889
|
+
if (rebaseOnConflict) {
|
|
11890
|
+
logger.warn(
|
|
11891
|
+
`Non-fast-forward on refs/heads/${branch} (attempt ${attempt + 1}/${MAX_CONCURRENT_PUSH_RETRIES}); rebasing locally and retrying.`
|
|
11892
|
+
);
|
|
11893
|
+
await repository.pullWithRebase(branch);
|
|
11894
|
+
[localHeadSha, treeSha, message, parents] = await Promise.all([
|
|
11895
|
+
repository.getHeadSha(),
|
|
11896
|
+
repository.getHeadTreeHash(),
|
|
11897
|
+
repository.getHeadCommitMessage(),
|
|
11898
|
+
repository.getHeadParents()
|
|
11899
|
+
]);
|
|
11900
|
+
await repository.pushObjectToRef(localHeadSha, tempRef, { force: true });
|
|
11901
|
+
} else {
|
|
11902
|
+
throw err;
|
|
11903
|
+
}
|
|
11904
|
+
}
|
|
11905
|
+
}
|
|
11906
|
+
throw new Error(
|
|
11907
|
+
`Failed to push signed commit to refs/heads/${branch} after ${MAX_CONCURRENT_PUSH_RETRIES} attempts`
|
|
11908
|
+
);
|
|
11909
|
+
} finally {
|
|
11910
|
+
if (tempRefPushed) {
|
|
11911
|
+
try {
|
|
11912
|
+
await octokit.git.deleteRef({ owner, repo, ref: tempRef.replace(/^refs\//, "") });
|
|
11913
|
+
} catch (err) {
|
|
11914
|
+
logger.debug(`Failed to delete temp ref ${tempRef}: ${extractErrorMessage(err)}`);
|
|
11915
|
+
}
|
|
11916
|
+
}
|
|
11917
|
+
}
|
|
11918
|
+
}
|
|
11919
|
+
async function upsertBranchRef({
|
|
11920
|
+
octokit,
|
|
11921
|
+
owner,
|
|
11922
|
+
repo,
|
|
11923
|
+
branch,
|
|
11924
|
+
sha,
|
|
11925
|
+
force
|
|
11926
|
+
}) {
|
|
11927
|
+
try {
|
|
11928
|
+
await octokit.git.updateRef({
|
|
11929
|
+
owner,
|
|
11930
|
+
repo,
|
|
11931
|
+
ref: `heads/${branch}`,
|
|
11932
|
+
sha,
|
|
11933
|
+
force
|
|
11934
|
+
});
|
|
11935
|
+
} catch (err) {
|
|
11936
|
+
if (isNotFoundError(err)) {
|
|
11937
|
+
await octokit.git.createRef({
|
|
11938
|
+
owner,
|
|
11939
|
+
repo,
|
|
11940
|
+
ref: `refs/heads/${branch}`,
|
|
11941
|
+
sha
|
|
11942
|
+
});
|
|
11943
|
+
return;
|
|
11944
|
+
}
|
|
11945
|
+
throw err;
|
|
11946
|
+
}
|
|
11947
|
+
}
|
|
11948
|
+
async function syncLocalToSignedCommit({
|
|
11949
|
+
repository,
|
|
11950
|
+
branch,
|
|
11951
|
+
signedSha
|
|
11952
|
+
}) {
|
|
11953
|
+
await repository.fetch(["origin", branch]);
|
|
11954
|
+
await repository.resetHardToSha(signedSha);
|
|
11955
|
+
}
|
|
11956
|
+
function hasNumericStatus(err) {
|
|
11957
|
+
return typeof err === "object" && err != null && "status" in err && typeof err.status === "number";
|
|
11958
|
+
}
|
|
11959
|
+
function isNotFoundError(err) {
|
|
11960
|
+
return hasNumericStatus(err) && err.status === 404;
|
|
11961
|
+
}
|
|
11962
|
+
function isNonFastForwardError(err) {
|
|
11963
|
+
if (!hasNumericStatus(err) || err.status !== 422) {
|
|
11964
|
+
return false;
|
|
11965
|
+
}
|
|
11966
|
+
const message = extractErrorMessage(err).toLowerCase();
|
|
11967
|
+
return message.includes("not a fast forward") || message.includes("not a fast-forward") || message.includes("update is not a fast forward");
|
|
11968
|
+
}
|
|
11969
|
+
|
|
11970
|
+
// src/pipeline/replay-summary.ts
|
|
11971
|
+
function plural(n, word) {
|
|
11972
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
11973
|
+
}
|
|
11974
|
+
function formatConflictReason(reason) {
|
|
11975
|
+
switch (reason) {
|
|
11976
|
+
case "same-line-edit":
|
|
11977
|
+
return "The new generation changed the same lines you edited";
|
|
11978
|
+
case "new-file-both":
|
|
11979
|
+
return "Both the generator and your customization created this file";
|
|
11980
|
+
case "base-generation-mismatch":
|
|
11981
|
+
return "This customization was from a previous generation";
|
|
11982
|
+
case "patch-apply-failed":
|
|
11983
|
+
return "The customization could not be applied to the new code";
|
|
11984
|
+
default:
|
|
11985
|
+
return "Your edit overlaps with changes in the new generation";
|
|
11986
|
+
}
|
|
11987
|
+
}
|
|
11988
|
+
function patchDescription(detail) {
|
|
11989
|
+
if (detail.patchMessage && detail.patchMessage !== "update") {
|
|
11990
|
+
return detail.patchMessage;
|
|
11991
|
+
}
|
|
11992
|
+
const firstFile = detail.files[0];
|
|
11993
|
+
return firstFile != null ? `changes in ${firstFile}` : "customization";
|
|
11994
|
+
}
|
|
11995
|
+
function logReplaySummary(result, logger) {
|
|
11996
|
+
if (!result.executed) {
|
|
11997
|
+
return;
|
|
11998
|
+
}
|
|
11999
|
+
const applied = result.patchesApplied ?? 0;
|
|
12000
|
+
const absorbed = result.patchesAbsorbed ?? 0;
|
|
10191
12001
|
const unresolvedCount = result.unresolvedPatches?.length ?? 0;
|
|
10192
12002
|
const preserved = applied - absorbed;
|
|
10193
12003
|
logger.debug(
|
|
@@ -10288,16 +12098,6 @@ function formatReplayPrBody(result, options) {
|
|
|
10288
12098
|
return parts.join("\n");
|
|
10289
12099
|
}
|
|
10290
12100
|
|
|
10291
|
-
// src/pipeline/steps/BaseStep.ts
|
|
10292
|
-
var BaseStep = class {
|
|
10293
|
-
constructor(outputDir, logger) {
|
|
10294
|
-
this.outputDir = outputDir;
|
|
10295
|
-
this.logger = logger;
|
|
10296
|
-
}
|
|
10297
|
-
outputDir;
|
|
10298
|
-
logger;
|
|
10299
|
-
};
|
|
10300
|
-
|
|
10301
12101
|
// src/pipeline/steps/GithubStep.ts
|
|
10302
12102
|
var GithubStep = class extends BaseStep {
|
|
10303
12103
|
constructor(outputDir, logger, config) {
|
|
@@ -10407,7 +12207,7 @@ var GithubStep = class extends BaseStep {
|
|
|
10407
12207
|
await repository.commitAllChanges(finalCommitMessage2);
|
|
10408
12208
|
this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
|
|
10409
12209
|
}
|
|
10410
|
-
if (this.config
|
|
12210
|
+
if (shouldCheckNoDiff(this.config)) {
|
|
10411
12211
|
const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
|
|
10412
12212
|
if (noDiff) {
|
|
10413
12213
|
this.logger.info("No changes detected after generation \u2014 skipping PR creation");
|
|
@@ -10418,13 +12218,17 @@ var GithubStep = class extends BaseStep {
|
|
|
10418
12218
|
executed: true,
|
|
10419
12219
|
success: true,
|
|
10420
12220
|
updatedExistingPr: isUpdatingExistingPR
|
|
10421
|
-
};
|
|
10422
|
-
if (!this.config.previewMode) {
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
12221
|
+
};
|
|
12222
|
+
if (!this.config.previewMode) {
|
|
12223
|
+
await pushSignedCommit({
|
|
12224
|
+
repository,
|
|
12225
|
+
octokit,
|
|
12226
|
+
owner,
|
|
12227
|
+
repo,
|
|
12228
|
+
branch: prBranch,
|
|
12229
|
+
force: isUpdatingExistingPR,
|
|
12230
|
+
logger: this.logger
|
|
12231
|
+
});
|
|
10428
12232
|
const pushedBranch = await repository.getCurrentBranch();
|
|
10429
12233
|
result.branchUrl = `https://github.com/${this.config.uri}/tree/${pushedBranch}`;
|
|
10430
12234
|
this.logger.info(`Pushed branch: ${result.branchUrl}`);
|
|
@@ -10525,7 +12329,7 @@ var GithubStep = class extends BaseStep {
|
|
|
10525
12329
|
const finalCommitMessage = this.config.commitMessage ?? "SDK Generation";
|
|
10526
12330
|
await repository.commitAllChanges(finalCommitMessage);
|
|
10527
12331
|
this.logger.debug(`Committed changes to local copy of GitHub repository at ${this.outputDir}`);
|
|
10528
|
-
if (this.config
|
|
12332
|
+
if (shouldCheckNoDiff(this.config)) {
|
|
10529
12333
|
const noDiff = await repository.treeHashEquals(`origin/${baseBranch}`);
|
|
10530
12334
|
if (noDiff) {
|
|
10531
12335
|
this.logger.info("No changes detected after generation \u2014 skipping push");
|
|
@@ -10537,7 +12341,18 @@ var GithubStep = class extends BaseStep {
|
|
|
10537
12341
|
success: true
|
|
10538
12342
|
};
|
|
10539
12343
|
if (!this.config.previewMode) {
|
|
10540
|
-
|
|
12344
|
+
const octokit = new import_rest2.Octokit({ auth: this.config.token });
|
|
12345
|
+
const { owner, repo } = parseRepository(this.config.uri);
|
|
12346
|
+
await pushSignedCommit({
|
|
12347
|
+
repository,
|
|
12348
|
+
octokit,
|
|
12349
|
+
owner,
|
|
12350
|
+
repo,
|
|
12351
|
+
branch: baseBranch,
|
|
12352
|
+
force: false,
|
|
12353
|
+
rebaseOnConflict: true,
|
|
12354
|
+
logger: this.logger
|
|
12355
|
+
});
|
|
10541
12356
|
const pushedBranch = await repository.getCurrentBranch();
|
|
10542
12357
|
result.branchUrl = `https://github.com/${this.config.uri}/tree/${pushedBranch}`;
|
|
10543
12358
|
this.logger.info(`Pushed branch: ${result.branchUrl}`);
|
|
@@ -10546,13 +12361,13 @@ var GithubStep = class extends BaseStep {
|
|
|
10546
12361
|
}
|
|
10547
12362
|
async ensureFernignore() {
|
|
10548
12363
|
this.logger.debug("Checking for .fernignore file...");
|
|
10549
|
-
const fernignorePath = (0,
|
|
12364
|
+
const fernignorePath = (0, import_path7.join)(this.outputDir, ".fernignore");
|
|
10550
12365
|
try {
|
|
10551
|
-
await (0,
|
|
12366
|
+
await (0, import_promises5.access)(fernignorePath);
|
|
10552
12367
|
this.logger.debug(".fernignore already exists");
|
|
10553
12368
|
} catch {
|
|
10554
12369
|
this.logger.debug("Creating .fernignore file...");
|
|
10555
|
-
await (0,
|
|
12370
|
+
await (0, import_promises5.writeFile)(fernignorePath, "# Specify files that shouldn't be modified by Fern\n", "utf-8");
|
|
10556
12371
|
}
|
|
10557
12372
|
}
|
|
10558
12373
|
deriveSkipCommit(replayResult) {
|
|
@@ -10596,6 +12411,9 @@ _Fern Run ID: \`${config.runId}\`_`;
|
|
|
10596
12411
|
function shouldEnableAutomerge(config) {
|
|
10597
12412
|
return config.automationMode === true && config.autoMerge === true && config.hasBreakingChanges !== true;
|
|
10598
12413
|
}
|
|
12414
|
+
function shouldCheckNoDiff(config) {
|
|
12415
|
+
return config.skipIfNoDiff === true;
|
|
12416
|
+
}
|
|
10599
12417
|
function shouldPushGenerationBaseTag(replayResult) {
|
|
10600
12418
|
return (replayResult?.patchesWithConflicts ?? 0) > 0;
|
|
10601
12419
|
}
|
|
@@ -10616,242 +12434,6 @@ function resolveBranchAction({
|
|
|
10616
12434
|
return "create-from-head";
|
|
10617
12435
|
}
|
|
10618
12436
|
|
|
10619
|
-
// src/replay/replay-run.ts
|
|
10620
|
-
var import_replay = require("@fern-api/replay");
|
|
10621
|
-
var import_child_process3 = require("child_process");
|
|
10622
|
-
var import_fs3 = require("fs");
|
|
10623
|
-
var import_path5 = require("path");
|
|
10624
|
-
|
|
10625
|
-
// src/replay/fernignore.ts
|
|
10626
|
-
var import_fs2 = require("fs");
|
|
10627
|
-
var import_promises3 = require("fs/promises");
|
|
10628
|
-
var import_path4 = require("path");
|
|
10629
|
-
var REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];
|
|
10630
|
-
var GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
|
|
10631
|
-
async function ensureReplayFernignoreEntries(outputDir) {
|
|
10632
|
-
const fernignorePath = (0, import_path4.join)(outputDir, ".fernignore");
|
|
10633
|
-
let content = "";
|
|
10634
|
-
try {
|
|
10635
|
-
content = await (0, import_promises3.readFile)(fernignorePath, "utf-8");
|
|
10636
|
-
} catch {
|
|
10637
|
-
}
|
|
10638
|
-
const lines = content.split("\n");
|
|
10639
|
-
const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
10640
|
-
if (toAdd.length > 0) {
|
|
10641
|
-
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
10642
|
-
await (0, import_promises3.writeFile)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
10643
|
-
return true;
|
|
10644
|
-
}
|
|
10645
|
-
return false;
|
|
10646
|
-
}
|
|
10647
|
-
async function ensureGitattributesEntries(outputDir) {
|
|
10648
|
-
const gitattributesPath = (0, import_path4.join)(outputDir, ".gitattributes");
|
|
10649
|
-
let content = "";
|
|
10650
|
-
try {
|
|
10651
|
-
content = await (0, import_promises3.readFile)(gitattributesPath, "utf-8");
|
|
10652
|
-
} catch {
|
|
10653
|
-
}
|
|
10654
|
-
const lines = content.split("\n");
|
|
10655
|
-
const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
10656
|
-
if (toAdd.length > 0) {
|
|
10657
|
-
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
10658
|
-
await (0, import_promises3.writeFile)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
10659
|
-
}
|
|
10660
|
-
}
|
|
10661
|
-
function ensureReplayFernignoreEntriesSync(outputDir) {
|
|
10662
|
-
const fernignorePath = (0, import_path4.join)(outputDir, ".fernignore");
|
|
10663
|
-
let content = "";
|
|
10664
|
-
if ((0, import_fs2.existsSync)(fernignorePath)) {
|
|
10665
|
-
content = (0, import_fs2.readFileSync)(fernignorePath, "utf-8");
|
|
10666
|
-
}
|
|
10667
|
-
const lines = content.split("\n");
|
|
10668
|
-
const toAdd = REPLAY_FERNIGNORE_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
10669
|
-
if (toAdd.length > 0) {
|
|
10670
|
-
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
10671
|
-
(0, import_fs2.writeFileSync)(fernignorePath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
10672
|
-
}
|
|
10673
|
-
}
|
|
10674
|
-
function ensureGitattributesEntriesSync(outputDir) {
|
|
10675
|
-
const gitattributesPath = (0, import_path4.join)(outputDir, ".gitattributes");
|
|
10676
|
-
let content = "";
|
|
10677
|
-
if ((0, import_fs2.existsSync)(gitattributesPath)) {
|
|
10678
|
-
content = (0, import_fs2.readFileSync)(gitattributesPath, "utf-8");
|
|
10679
|
-
}
|
|
10680
|
-
const lines = content.split("\n");
|
|
10681
|
-
const toAdd = GITATTRIBUTES_ENTRIES.filter((entry) => !lines.some((line) => line.trim() === entry));
|
|
10682
|
-
if (toAdd.length > 0) {
|
|
10683
|
-
const suffix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
10684
|
-
(0, import_fs2.writeFileSync)(gitattributesPath, content + suffix + toAdd.join("\n") + "\n", "utf-8");
|
|
10685
|
-
}
|
|
10686
|
-
}
|
|
10687
|
-
|
|
10688
|
-
// src/replay/replay-run.ts
|
|
10689
|
-
async function replayRun(params) {
|
|
10690
|
-
const {
|
|
10691
|
-
outputDir,
|
|
10692
|
-
cliVersion,
|
|
10693
|
-
generatorVersions,
|
|
10694
|
-
stageOnly = false,
|
|
10695
|
-
generatorName,
|
|
10696
|
-
skipApplication,
|
|
10697
|
-
logger
|
|
10698
|
-
} = params;
|
|
10699
|
-
const lockfilePath = (0, import_path5.join)(outputDir, ".fern", "replay.lock");
|
|
10700
|
-
if (!(0, import_fs3.existsSync)(lockfilePath)) {
|
|
10701
|
-
return {
|
|
10702
|
-
report: null,
|
|
10703
|
-
fernignoreUpdated: false,
|
|
10704
|
-
previousGenerationSha: null,
|
|
10705
|
-
currentGenerationSha: null,
|
|
10706
|
-
baseBranchHead: null
|
|
10707
|
-
};
|
|
10708
|
-
}
|
|
10709
|
-
const baseBranchHead = gitRevParse(outputDir, "HEAD");
|
|
10710
|
-
let previousGenerationSha = null;
|
|
10711
|
-
let prevBaseBranchHead = null;
|
|
10712
|
-
try {
|
|
10713
|
-
const lockManager = new import_replay.LockfileManager(outputDir);
|
|
10714
|
-
if (lockManager.exists()) {
|
|
10715
|
-
const lock = lockManager.read();
|
|
10716
|
-
previousGenerationSha = lock.current_generation;
|
|
10717
|
-
const latestGen = lock.generations.find((g2) => g2.commit_sha === lock.current_generation);
|
|
10718
|
-
prevBaseBranchHead = latestGen?.base_branch_head ?? null;
|
|
10719
|
-
}
|
|
10720
|
-
} catch {
|
|
10721
|
-
}
|
|
10722
|
-
if (previousGenerationSha != null) {
|
|
10723
|
-
const sanitizedName = generatorName?.replace(/\//g, "--");
|
|
10724
|
-
const namespacedTag = sanitizedName != null ? `fern-generation-base--${sanitizedName}` : null;
|
|
10725
|
-
const tagSha = (namespacedTag != null ? gitRevParse(outputDir, namespacedTag) : null) ?? gitRevParse(outputDir, "fern-generation-base");
|
|
10726
|
-
let shouldSync = false;
|
|
10727
|
-
if (tagSha != null) {
|
|
10728
|
-
const tagParent = gitRevParse(outputDir, `${tagSha}^`);
|
|
10729
|
-
if (tagParent === prevBaseBranchHead || tagParent === previousGenerationSha) {
|
|
10730
|
-
const tagDistance = gitDiffNameOnly(outputDir, tagSha, "HEAD").length;
|
|
10731
|
-
const lockDistance = gitDiffNameOnly(
|
|
10732
|
-
outputDir,
|
|
10733
|
-
prevBaseBranchHead ?? previousGenerationSha,
|
|
10734
|
-
"HEAD"
|
|
10735
|
-
).length;
|
|
10736
|
-
shouldSync = tagDistance < lockDistance;
|
|
10737
|
-
}
|
|
10738
|
-
}
|
|
10739
|
-
if (shouldSync && tagSha != null) {
|
|
10740
|
-
if (!isTagMergedIntoHead(outputDir, tagSha)) {
|
|
10741
|
-
logger?.warn(
|
|
10742
|
-
`fern-generation-base tag ${tagSha} is not reachable from HEAD \u2014 skipping divergent-merge sync. The tag likely points at an unmerged generation (PR closed without merge).`
|
|
10743
|
-
);
|
|
10744
|
-
} else {
|
|
10745
|
-
const syncService = new import_replay.ReplayService(outputDir, { enabled: true });
|
|
10746
|
-
await syncService.syncFromDivergentMerge(tagSha, {
|
|
10747
|
-
cliVersion,
|
|
10748
|
-
generatorVersions,
|
|
10749
|
-
baseBranchHead: baseBranchHead ?? void 0
|
|
10750
|
-
});
|
|
10751
|
-
try {
|
|
10752
|
-
const freshLockManager = new import_replay.LockfileManager(outputDir);
|
|
10753
|
-
if (freshLockManager.exists()) {
|
|
10754
|
-
previousGenerationSha = freshLockManager.read().current_generation;
|
|
10755
|
-
}
|
|
10756
|
-
} catch {
|
|
10757
|
-
}
|
|
10758
|
-
}
|
|
10759
|
-
}
|
|
10760
|
-
}
|
|
10761
|
-
const service = new import_replay.ReplayService(outputDir, {
|
|
10762
|
-
enabled: true
|
|
10763
|
-
});
|
|
10764
|
-
let report;
|
|
10765
|
-
try {
|
|
10766
|
-
report = await service.runReplay({
|
|
10767
|
-
cliVersion,
|
|
10768
|
-
generatorVersions,
|
|
10769
|
-
stageOnly,
|
|
10770
|
-
skipApplication,
|
|
10771
|
-
baseBranchHead: baseBranchHead ?? void 0
|
|
10772
|
-
});
|
|
10773
|
-
} catch (error) {
|
|
10774
|
-
logger?.warn("Replay failed, continuing without replay: " + String(error));
|
|
10775
|
-
return {
|
|
10776
|
-
report: null,
|
|
10777
|
-
fernignoreUpdated: false,
|
|
10778
|
-
previousGenerationSha,
|
|
10779
|
-
currentGenerationSha: null,
|
|
10780
|
-
baseBranchHead
|
|
10781
|
-
};
|
|
10782
|
-
}
|
|
10783
|
-
let currentGenerationSha = null;
|
|
10784
|
-
let resolvedBaseBranchHead = baseBranchHead;
|
|
10785
|
-
try {
|
|
10786
|
-
const freshLockManager = new import_replay.LockfileManager(outputDir);
|
|
10787
|
-
if (freshLockManager.exists()) {
|
|
10788
|
-
const freshLock = freshLockManager.read();
|
|
10789
|
-
currentGenerationSha = freshLock.current_generation;
|
|
10790
|
-
const latestGen = freshLock.generations.find((g2) => g2.commit_sha === freshLock.current_generation);
|
|
10791
|
-
if (latestGen?.base_branch_head) {
|
|
10792
|
-
resolvedBaseBranchHead = latestGen.base_branch_head;
|
|
10793
|
-
}
|
|
10794
|
-
}
|
|
10795
|
-
} catch {
|
|
10796
|
-
}
|
|
10797
|
-
const fernignoreUpdated = await ensureReplayFernignoreEntries(outputDir);
|
|
10798
|
-
await ensureGitattributesEntries(outputDir);
|
|
10799
|
-
return {
|
|
10800
|
-
report,
|
|
10801
|
-
fernignoreUpdated,
|
|
10802
|
-
previousGenerationSha,
|
|
10803
|
-
currentGenerationSha,
|
|
10804
|
-
baseBranchHead: resolvedBaseBranchHead
|
|
10805
|
-
};
|
|
10806
|
-
}
|
|
10807
|
-
function isTagMergedIntoHead(cwd2, tagSha) {
|
|
10808
|
-
try {
|
|
10809
|
-
(0, import_child_process3.execFileSync)("git", ["merge-base", "--is-ancestor", tagSha, "HEAD"], {
|
|
10810
|
-
cwd: cwd2,
|
|
10811
|
-
stdio: "pipe"
|
|
10812
|
-
});
|
|
10813
|
-
return true;
|
|
10814
|
-
} catch {
|
|
10815
|
-
}
|
|
10816
|
-
const tagTree = gitRevParse(cwd2, `${tagSha}^{tree}`);
|
|
10817
|
-
if (tagTree == null) {
|
|
10818
|
-
return false;
|
|
10819
|
-
}
|
|
10820
|
-
try {
|
|
10821
|
-
const trees = (0, import_child_process3.execFileSync)("git", ["log", "HEAD", "--format=%T", "--max-count=500"], {
|
|
10822
|
-
cwd: cwd2,
|
|
10823
|
-
encoding: "utf-8",
|
|
10824
|
-
stdio: "pipe"
|
|
10825
|
-
});
|
|
10826
|
-
return trees.split("\n").some((t2) => t2.trim() === tagTree);
|
|
10827
|
-
} catch {
|
|
10828
|
-
return false;
|
|
10829
|
-
}
|
|
10830
|
-
}
|
|
10831
|
-
function gitRevParse(cwd2, rev) {
|
|
10832
|
-
try {
|
|
10833
|
-
return (0, import_child_process3.execFileSync)("git", ["rev-parse", "--verify", rev], {
|
|
10834
|
-
cwd: cwd2,
|
|
10835
|
-
encoding: "utf-8",
|
|
10836
|
-
stdio: "pipe"
|
|
10837
|
-
}).trim();
|
|
10838
|
-
} catch {
|
|
10839
|
-
return null;
|
|
10840
|
-
}
|
|
10841
|
-
}
|
|
10842
|
-
function gitDiffNameOnly(cwd2, from, to) {
|
|
10843
|
-
try {
|
|
10844
|
-
const output = (0, import_child_process3.execFileSync)("git", ["diff", "--name-only", from, to], {
|
|
10845
|
-
cwd: cwd2,
|
|
10846
|
-
encoding: "utf-8",
|
|
10847
|
-
stdio: "pipe"
|
|
10848
|
-
});
|
|
10849
|
-
return output.trim().split("\n").filter(Boolean);
|
|
10850
|
-
} catch {
|
|
10851
|
-
return [];
|
|
10852
|
-
}
|
|
10853
|
-
}
|
|
10854
|
-
|
|
10855
12437
|
// src/pipeline/steps/ReplayStep.ts
|
|
10856
12438
|
var ReplayStep = class extends BaseStep {
|
|
10857
12439
|
constructor(outputDir, logger, config, cliVersion, generatorVersions, generatorName) {
|
|
@@ -10866,8 +12448,23 @@ var ReplayStep = class extends BaseStep {
|
|
|
10866
12448
|
generatorVersions;
|
|
10867
12449
|
generatorName;
|
|
10868
12450
|
name = "replay";
|
|
10869
|
-
async execute(
|
|
10870
|
-
const
|
|
12451
|
+
async execute(context) {
|
|
12452
|
+
const generationCommit = context.previousStepResults.generationCommit;
|
|
12453
|
+
const prepared = generationCommit?.preparedReplay;
|
|
12454
|
+
if (generationCommit != null && prepared == null) {
|
|
12455
|
+
return {
|
|
12456
|
+
executed: true,
|
|
12457
|
+
success: true,
|
|
12458
|
+
flow: "first-generation",
|
|
12459
|
+
patchesDetected: 0,
|
|
12460
|
+
patchesApplied: 0,
|
|
12461
|
+
patchesWithConflicts: 0
|
|
12462
|
+
};
|
|
12463
|
+
}
|
|
12464
|
+
const result = prepared != null ? await replayApply(prepared, {
|
|
12465
|
+
stageOnly: this.config.stageOnly ?? false,
|
|
12466
|
+
logger: this.logger
|
|
12467
|
+
}) : await replayRun({
|
|
10871
12468
|
outputDir: this.outputDir,
|
|
10872
12469
|
cliVersion: this.cliVersion,
|
|
10873
12470
|
generatorVersions: this.generatorVersions,
|
|
@@ -10923,13 +12520,34 @@ var PostGenerationPipeline = class {
|
|
|
10923
12520
|
constructor(config, logger = consolePipelineLogger) {
|
|
10924
12521
|
this.config = config;
|
|
10925
12522
|
this.logger = logger;
|
|
10926
|
-
|
|
12523
|
+
let replayEnabled = config.replay?.enabled ?? false;
|
|
12524
|
+
let autoVersionEnabled = config.autoVersion?.enabled ?? false;
|
|
12525
|
+
if (replayEnabled && config.github?.mode === "push") {
|
|
10927
12526
|
this.logger.warn(
|
|
10928
12527
|
"Replay is not supported with GitHub push mode. Disabling replay to prevent force push to base branch."
|
|
10929
12528
|
);
|
|
10930
|
-
|
|
12529
|
+
replayEnabled = false;
|
|
12530
|
+
}
|
|
12531
|
+
if (autoVersionEnabled && !replayEnabled) {
|
|
12532
|
+
this.logger.warn("AutoVersion requires Replay to be enabled. Disabling AutoVersion for this run.");
|
|
12533
|
+
autoVersionEnabled = false;
|
|
12534
|
+
}
|
|
12535
|
+
if (replayEnabled && config.replay != null) {
|
|
12536
|
+
this.steps.push(
|
|
12537
|
+
new GenerationCommitStep(
|
|
12538
|
+
config.outputDir,
|
|
12539
|
+
this.logger,
|
|
12540
|
+
{ enabled: true, skipApplication: config.replay.skipApplication },
|
|
12541
|
+
config.cliVersion,
|
|
12542
|
+
config.generatorVersions,
|
|
12543
|
+
config.generatorName
|
|
12544
|
+
)
|
|
12545
|
+
);
|
|
10931
12546
|
}
|
|
10932
|
-
if (config.
|
|
12547
|
+
if (autoVersionEnabled && config.autoVersion != null) {
|
|
12548
|
+
this.steps.push(new AutoVersionStep(config.outputDir, this.logger, config.autoVersion));
|
|
12549
|
+
}
|
|
12550
|
+
if (replayEnabled && config.replay != null) {
|
|
10933
12551
|
this.steps.push(
|
|
10934
12552
|
new ReplayStep(
|
|
10935
12553
|
config.outputDir,
|
|
@@ -10951,8 +12569,8 @@ var PostGenerationPipeline = class {
|
|
|
10951
12569
|
async run() {
|
|
10952
12570
|
if (this.config.github?.enabled) {
|
|
10953
12571
|
try {
|
|
10954
|
-
(0,
|
|
10955
|
-
(0,
|
|
12572
|
+
(0, import_child_process5.execFileSync)("git", ["config", "user.name", FERN_BOT_NAME], { cwd: this.config.outputDir });
|
|
12573
|
+
(0, import_child_process5.execFileSync)("git", ["config", "user.email", FERN_BOT_EMAIL], { cwd: this.config.outputDir });
|
|
10956
12574
|
} catch {
|
|
10957
12575
|
}
|
|
10958
12576
|
}
|
|
@@ -10966,9 +12584,15 @@ var PostGenerationPipeline = class {
|
|
|
10966
12584
|
for (const step of this.steps) {
|
|
10967
12585
|
try {
|
|
10968
12586
|
const stepResult = await step.execute(pipelineContext);
|
|
10969
|
-
if (step.name === "
|
|
12587
|
+
if (step.name === "generationCommit") {
|
|
12588
|
+
result.steps.generationCommit = stepResult;
|
|
12589
|
+
pipelineContext.previousStepResults.generationCommit = stepResult;
|
|
12590
|
+
} else if (step.name === "replay") {
|
|
10970
12591
|
result.steps.replay = stepResult;
|
|
10971
12592
|
pipelineContext.previousStepResults.replay = stepResult;
|
|
12593
|
+
} else if (step.name === "autoVersion") {
|
|
12594
|
+
result.steps.autoVersion = stepResult;
|
|
12595
|
+
pipelineContext.previousStepResults.autoVersion = stepResult;
|
|
10972
12596
|
} else if (step.name === "fernignore") {
|
|
10973
12597
|
result.steps.fernignore = stepResult;
|
|
10974
12598
|
} else if (step.name === "github") {
|
|
@@ -10998,8 +12622,8 @@ function replayForget(params) {
|
|
|
10998
12622
|
|
|
10999
12623
|
// src/replay/replay-init.ts
|
|
11000
12624
|
var import_replay3 = require("@fern-api/replay");
|
|
11001
|
-
var
|
|
11002
|
-
var
|
|
12625
|
+
var import_fs6 = require("fs");
|
|
12626
|
+
var import_path8 = require("path");
|
|
11003
12627
|
var import_tmp_promise2 = __toESM(require("tmp-promise"));
|
|
11004
12628
|
async function replayInit(params) {
|
|
11005
12629
|
const { githubRepo, token, dryRun } = params;
|
|
@@ -11025,10 +12649,10 @@ async function replayInit(params) {
|
|
|
11025
12649
|
}
|
|
11026
12650
|
ensureReplayFernignoreEntriesSync(repoPath);
|
|
11027
12651
|
ensureGitattributesEntriesSync(repoPath);
|
|
11028
|
-
const lockfilePath = (0,
|
|
11029
|
-
const lockfileContent = (0,
|
|
11030
|
-
const replayYmlPath = (0,
|
|
11031
|
-
const replayYmlContent = (0,
|
|
12652
|
+
const lockfilePath = (0, import_path8.join)(repoPath, ".fern", "replay.lock");
|
|
12653
|
+
const lockfileContent = (0, import_fs6.existsSync)(lockfilePath) ? (0, import_fs6.readFileSync)(lockfilePath, "utf-8") : void 0;
|
|
12654
|
+
const replayYmlPath = (0, import_path8.join)(repoPath, ".fern", "replay.yml");
|
|
12655
|
+
const replayYmlContent = (0, import_fs6.existsSync)(replayYmlPath) ? (0, import_fs6.readFileSync)(replayYmlPath, "utf-8") : void 0;
|
|
11032
12656
|
return {
|
|
11033
12657
|
bootstrap: bootstrapResult,
|
|
11034
12658
|
lockfileContent,
|
|
@@ -11156,9 +12780,66 @@ var import_replay5 = require("@fern-api/replay");
|
|
|
11156
12780
|
function replayStatus(params) {
|
|
11157
12781
|
return (0, import_replay5.status)(params.outputDir);
|
|
11158
12782
|
}
|
|
12783
|
+
|
|
12784
|
+
// src/replay/replay-submit-init.ts
|
|
12785
|
+
async function submitReplayInit(params) {
|
|
12786
|
+
const { fiddleOrigin, fernToken, githubRepo, initResult } = params;
|
|
12787
|
+
const { owner, repo } = parseOwnerRepo(githubRepo);
|
|
12788
|
+
const response = await fetch(`${fiddleOrigin}/api/remote-gen/replay/init`, {
|
|
12789
|
+
method: "POST",
|
|
12790
|
+
headers: {
|
|
12791
|
+
"Content-Type": "application/json",
|
|
12792
|
+
Authorization: `Bearer ${fernToken}`
|
|
12793
|
+
},
|
|
12794
|
+
body: JSON.stringify({
|
|
12795
|
+
owner,
|
|
12796
|
+
repo,
|
|
12797
|
+
lockfileContents: initResult.lockfileContent,
|
|
12798
|
+
fernignoreEntries: initResult.fernignoreEntries,
|
|
12799
|
+
prBody: initResult.prBody
|
|
12800
|
+
})
|
|
12801
|
+
});
|
|
12802
|
+
if (!response.ok) {
|
|
12803
|
+
if (response.status === 404) {
|
|
12804
|
+
throw new ReplaySubmitError(
|
|
12805
|
+
"The Fern GitHub App is not installed on this repository. Install it at https://github.com/apps/fern-api to enable server-side PR creation.",
|
|
12806
|
+
response.status
|
|
12807
|
+
);
|
|
12808
|
+
}
|
|
12809
|
+
const body = await response.text();
|
|
12810
|
+
throw new ReplaySubmitError(`Failed to create PR via Fern: ${body}`, response.status);
|
|
12811
|
+
}
|
|
12812
|
+
const data = await response.json();
|
|
12813
|
+
if (!isSubmitResponse(data)) {
|
|
12814
|
+
throw new ReplaySubmitError("Unexpected response from Fern: missing prUrl field.");
|
|
12815
|
+
}
|
|
12816
|
+
return { prUrl: data.prUrl };
|
|
12817
|
+
}
|
|
12818
|
+
function parseOwnerRepo(githubRepo) {
|
|
12819
|
+
const cleaned = githubRepo.replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/^github\.com\//, "");
|
|
12820
|
+
const parts = cleaned.split("/");
|
|
12821
|
+
const owner = parts[parts.length - 2];
|
|
12822
|
+
const repo = parts[parts.length - 1];
|
|
12823
|
+
if (!owner || !repo) {
|
|
12824
|
+
throw new Error(`Could not parse owner/repo from: ${githubRepo}`);
|
|
12825
|
+
}
|
|
12826
|
+
return { owner, repo };
|
|
12827
|
+
}
|
|
12828
|
+
var ReplaySubmitError = class extends Error {
|
|
12829
|
+
statusCode;
|
|
12830
|
+
constructor(message, statusCode) {
|
|
12831
|
+
super(message);
|
|
12832
|
+
this.name = "ReplaySubmitError";
|
|
12833
|
+
this.statusCode = statusCode;
|
|
12834
|
+
}
|
|
12835
|
+
};
|
|
12836
|
+
function isSubmitResponse(data) {
|
|
12837
|
+
return typeof data === "object" && data != null && "prUrl" in data && typeof data.prUrl === "string";
|
|
12838
|
+
}
|
|
11159
12839
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11160
12840
|
0 && (module.exports = {
|
|
11161
12841
|
PostGenerationPipeline,
|
|
12842
|
+
ReplaySubmitError,
|
|
11162
12843
|
consolePipelineLogger,
|
|
11163
12844
|
formatBootstrapSummary,
|
|
11164
12845
|
formatReplayPrBody,
|
|
@@ -11170,9 +12851,11 @@ function replayStatus(params) {
|
|
|
11170
12851
|
githubPush,
|
|
11171
12852
|
githubRelease,
|
|
11172
12853
|
logReplaySummary,
|
|
12854
|
+
parseOwnerRepo,
|
|
11173
12855
|
replayForget,
|
|
11174
12856
|
replayInit,
|
|
11175
12857
|
replayResolve,
|
|
11176
12858
|
replayRun,
|
|
11177
|
-
replayStatus
|
|
12859
|
+
replayStatus,
|
|
12860
|
+
submitReplayInit
|
|
11178
12861
|
});
|