@fern-api/generator-cli 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +189 -62
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -42017,36 +42017,24 @@ var AutoVersionStep = class extends BaseStep {
42017
42017
  });
42018
42018
  }
42019
42019
  if (cleanedBytes > MAX_RAW_DIFF_BYTES) {
42020
- this.logger.warn(
42021
- `AutoVersionStep: diff too large for FAI (${formatSizeKB(cleanedBytes)}KB, limit ${formatSizeKB(MAX_RAW_DIFF_BYTES)}KB). Falling back to PATCH.`
42022
- );
42020
+ const warning = `diff too large for FAI (${formatSizeKB(cleanedBytes)}KB, limit ${formatSizeKB(MAX_RAW_DIFF_BYTES)}KB)`;
42021
+ this.logger.warn(`AutoVersionStep: ${warning}. Falling back to PATCH.`);
42023
42022
  return await this.finalizeWithBump({
42024
42023
  service,
42025
42024
  language,
42026
42025
  mappedMagicVersion,
42027
42026
  previousVersion,
42028
- analysis: { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") }
42027
+ analysis: this.fallbackPatchAnalysis(),
42028
+ analysisWarning: warning
42029
42029
  });
42030
42030
  }
42031
- let analysis;
42032
- try {
42033
- if (this.config.ai == null && this.config.fernToken != null) {
42034
- analysis = await this.analyzeViaFaiService(cleanedDiff, language, previousVersion);
42035
- } else {
42036
- const chunks = service.chunkDiff(cleanedDiff, MAX_AI_DIFF_BYTES);
42037
- const cappedChunks = chunks.slice(0, MAX_CHUNKS);
42038
- const skippedChunks = chunks.length - cappedChunks.length;
42039
- if (chunks.length > 1) {
42040
- this.logger.info(
42041
- `AutoVersionStep: split diff into ${chunks.length} chunks` + (skippedChunks > 0 ? ` (capped at ${MAX_CHUNKS}, skipping ${skippedChunks})` : "")
42042
- );
42043
- }
42044
- analysis = cappedChunks.length <= 1 ? await this.analyzeSingle(cleanedDiff, language, previousVersion) : await this.analyzeChunks(cappedChunks, language, previousVersion);
42045
- }
42046
- } catch (error) {
42047
- this.logger.warn(`AutoVersionStep: FAI analysis failed (${String(error)}); falling back to PATCH bump.`);
42048
- analysis = { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") };
42049
- }
42031
+ const { analysis, analysisWarning } = await this.analyze({
42032
+ service,
42033
+ cleanedDiff,
42034
+ language,
42035
+ previousVersion,
42036
+ label: "AutoVersionStep"
42037
+ });
42050
42038
  if (analysis == null) {
42051
42039
  this.logger.info(`AutoVersionStep: FAI returned NO_CHANGE; rewriting placeholder to ${previousVersion}.`);
42052
42040
  return await this.finalizeNoChange({
@@ -42054,7 +42042,8 @@ var AutoVersionStep = class extends BaseStep {
42054
42042
  language,
42055
42043
  mappedMagicVersion,
42056
42044
  previousVersion,
42057
- reason: "FAI returned NO_CHANGE"
42045
+ reason: "FAI returned NO_CHANGE",
42046
+ analysisWarning
42058
42047
  });
42059
42048
  }
42060
42049
  return await this.finalizeWithBump({
@@ -42062,7 +42051,8 @@ var AutoVersionStep = class extends BaseStep {
42062
42051
  language,
42063
42052
  mappedMagicVersion,
42064
42053
  previousVersion,
42065
- analysis
42054
+ analysis,
42055
+ analysisWarning
42066
42056
  });
42067
42057
  }
42068
42058
  /**
@@ -42113,38 +42103,24 @@ var AutoVersionStep = class extends BaseStep {
42113
42103
  });
42114
42104
  }
42115
42105
  if (cleanedBytes > MAX_RAW_DIFF_BYTES) {
42116
- this.logger.warn(
42117
- `AutoVersionStep (non-replay): diff too large (${formatSizeKB(cleanedBytes)}KB). Falling back to PATCH.`
42118
- );
42106
+ const warning = `diff too large for FAI (${formatSizeKB(cleanedBytes)}KB, limit ${formatSizeKB(MAX_RAW_DIFF_BYTES)}KB)`;
42107
+ this.logger.warn(`AutoVersionStep (non-replay): ${warning}. Falling back to PATCH.`);
42119
42108
  return await this.finalizeWithBumpNonReplay({
42120
42109
  service,
42121
42110
  language,
42122
42111
  mappedMagicVersion,
42123
42112
  previousVersion,
42124
- analysis: { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") }
42113
+ analysis: this.fallbackPatchAnalysis(),
42114
+ analysisWarning: warning
42125
42115
  });
42126
42116
  }
42127
- let analysis;
42128
- try {
42129
- if (this.config.ai == null && this.config.fernToken != null) {
42130
- analysis = await this.analyzeViaFaiService(cleanedDiff, language, previousVersion);
42131
- } else {
42132
- const chunks = service.chunkDiff(cleanedDiff, MAX_AI_DIFF_BYTES);
42133
- const cappedChunks = chunks.slice(0, MAX_CHUNKS);
42134
- const skippedChunks = chunks.length - cappedChunks.length;
42135
- if (chunks.length > 1) {
42136
- this.logger.info(
42137
- `AutoVersionStep (non-replay): split diff into ${chunks.length} chunks` + (skippedChunks > 0 ? ` (capped at ${MAX_CHUNKS}, skipping ${skippedChunks})` : "")
42138
- );
42139
- }
42140
- analysis = cappedChunks.length <= 1 ? await this.analyzeSingle(cleanedDiff, language, previousVersion) : await this.analyzeChunks(cappedChunks, language, previousVersion);
42141
- }
42142
- } catch (error) {
42143
- this.logger.warn(
42144
- `AutoVersionStep (non-replay): FAI analysis failed (${String(error)}); falling back to PATCH bump.`
42145
- );
42146
- analysis = { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") };
42147
- }
42117
+ const { analysis, analysisWarning } = await this.analyze({
42118
+ service,
42119
+ cleanedDiff,
42120
+ language,
42121
+ previousVersion,
42122
+ label: "AutoVersionStep (non-replay)"
42123
+ });
42148
42124
  if (analysis == null) {
42149
42125
  this.logger.info(
42150
42126
  `AutoVersionStep (non-replay): FAI returned NO_CHANGE; rewriting placeholder to ${previousVersion}.`
@@ -42154,7 +42130,8 @@ var AutoVersionStep = class extends BaseStep {
42154
42130
  language,
42155
42131
  mappedMagicVersion,
42156
42132
  previousVersion,
42157
- reason: "FAI returned NO_CHANGE"
42133
+ reason: "FAI returned NO_CHANGE",
42134
+ analysisWarning
42158
42135
  });
42159
42136
  }
42160
42137
  return await this.finalizeWithBumpNonReplay({
@@ -42162,9 +42139,72 @@ var AutoVersionStep = class extends BaseStep {
42162
42139
  language,
42163
42140
  mappedMagicVersion,
42164
42141
  previousVersion,
42165
- analysis
42142
+ analysis,
42143
+ analysisWarning
42166
42144
  });
42167
42145
  }
42146
+ fallbackPatchAnalysis() {
42147
+ return { versionBump: "PATCH", message: this.brandMessage("SDK regeneration") };
42148
+ }
42149
+ /**
42150
+ * Runs FAI analysis over the cleaned diff. Never throws: when analysis cannot be
42151
+ * obtained at all, returns a PATCH fallback together with an `analysisWarning`
42152
+ * describing why, so callers can surface it in the step result / PR body instead
42153
+ * of shipping a silent PATCH for what may be a breaking change.
42154
+ */
42155
+ async analyze(params) {
42156
+ const { service, cleanedDiff, language, previousVersion, label } = params;
42157
+ const chunks = service.chunkDiff(cleanedDiff, MAX_AI_DIFF_BYTES);
42158
+ const cappedChunks = chunks.slice(0, MAX_CHUNKS);
42159
+ const skippedChunks = chunks.length - cappedChunks.length;
42160
+ const logSplit = () => {
42161
+ if (chunks.length > 1) {
42162
+ this.logger.info(
42163
+ `${label}: split diff into ${chunks.length} chunks` + (skippedChunks > 0 ? ` (capped at ${MAX_CHUNKS}, skipping ${skippedChunks})` : "")
42164
+ );
42165
+ }
42166
+ };
42167
+ try {
42168
+ if (this.config.ai == null && this.config.fernToken != null) {
42169
+ try {
42170
+ return { analysis: await this.analyzeViaFaiService(cleanedDiff, language, previousVersion) };
42171
+ } catch (error) {
42172
+ if (cappedChunks.length <= 1) {
42173
+ throw error;
42174
+ }
42175
+ this.logger.warn(
42176
+ `${label}: FAI analysis of the full diff failed (${String(error)}); retrying in ${cappedChunks.length} chunks.`
42177
+ );
42178
+ logSplit();
42179
+ const chunked = await this.analyzeChunksViaFaiService(
42180
+ cappedChunks,
42181
+ language,
42182
+ previousVersion,
42183
+ label
42184
+ );
42185
+ const warnings = [chunked.analysisWarning];
42186
+ if (skippedChunks > 0) {
42187
+ warnings.push(
42188
+ `${skippedChunks} of ${chunks.length} diff chunks were not analyzed (capped at ${MAX_CHUNKS})`
42189
+ );
42190
+ }
42191
+ const analysisWarning = warnings.filter(hasText).join("; ");
42192
+ return {
42193
+ analysis: chunked.analysis,
42194
+ analysisWarning: analysisWarning.length > 0 ? analysisWarning : void 0
42195
+ };
42196
+ }
42197
+ }
42198
+ logSplit();
42199
+ return {
42200
+ analysis: cappedChunks.length <= 1 ? await this.analyzeSingle(cleanedDiff, language, previousVersion) : await this.analyzeChunks(cappedChunks, language, previousVersion)
42201
+ };
42202
+ } catch (error) {
42203
+ const warning = `FAI analysis failed (${String(error)})`;
42204
+ this.logger.warn(`${label}: ${warning}; falling back to PATCH bump.`);
42205
+ return { analysis: this.fallbackPatchAnalysis(), analysisWarning: warning };
42206
+ }
42207
+ }
42168
42208
  /**
42169
42209
  * Terminal path for runs that determine no semver bump is needed. The
42170
42210
  * current working tree still contains `0.0.0-fern-placeholder` from this
@@ -42175,7 +42215,7 @@ var AutoVersionStep = class extends BaseStep {
42175
42215
  * `[fern-autoversion]` so the replay + github steps see a stable base.
42176
42216
  */
42177
42217
  async finalizeNoChange(params) {
42178
- const { service, language, mappedMagicVersion, previousVersion, reason } = params;
42218
+ const { service, language, mappedMagicVersion, previousVersion, reason, analysisWarning } = params;
42179
42219
  if (!isValidSemver(previousVersion)) {
42180
42220
  const errorMessage = `AutoVersionStep: resolved previousVersion ${JSON.stringify(previousVersion)} is not a valid semver string. Refusing to rewrite placeholder to avoid shell injection.`;
42181
42221
  this.logger.error(errorMessage);
@@ -42194,11 +42234,12 @@ var AutoVersionStep = class extends BaseStep {
42194
42234
  previousVersion,
42195
42235
  versionBump: "NO_CHANGE",
42196
42236
  commitMessage,
42197
- commitSha
42237
+ commitSha,
42238
+ analysisWarning
42198
42239
  };
42199
42240
  }
42200
42241
  async finalizeWithBump(params) {
42201
- const { service, language, mappedMagicVersion, previousVersion } = params;
42242
+ const { service, language, mappedMagicVersion, previousVersion, analysisWarning } = params;
42202
42243
  const analysis = this.withChangelogFallback(params.analysis);
42203
42244
  const newVersion = incrementVersion(previousVersion, analysis.versionBump);
42204
42245
  this.logger.info(`AutoVersionStep: ${analysis.versionBump} bump: ${previousVersion} \u2192 ${newVersion}`);
@@ -42220,6 +42261,7 @@ var AutoVersionStep = class extends BaseStep {
42220
42261
  versionBump: analysis.versionBump,
42221
42262
  prDescription: analysis.prDescription,
42222
42263
  versionBumpReason: analysis.versionBumpReason,
42264
+ analysisWarning,
42223
42265
  commitSha
42224
42266
  };
42225
42267
  }
@@ -42228,7 +42270,7 @@ var AutoVersionStep = class extends BaseStep {
42228
42270
  * `previousVersion` but does NOT commit — GithubStep handles the commit.
42229
42271
  */
42230
42272
  async finalizeNoChangeNonReplay(params) {
42231
- const { service, language, mappedMagicVersion, previousVersion, reason } = params;
42273
+ const { service, language, mappedMagicVersion, previousVersion, reason, analysisWarning } = params;
42232
42274
  if (!isValidSemver(previousVersion)) {
42233
42275
  const errorMessage = `AutoVersionStep: resolved previousVersion ${JSON.stringify(previousVersion)} is not a valid semver string. Refusing to rewrite placeholder to avoid shell injection.`;
42234
42276
  this.logger.error(errorMessage);
@@ -42245,7 +42287,8 @@ var AutoVersionStep = class extends BaseStep {
42245
42287
  version: previousVersion,
42246
42288
  previousVersion,
42247
42289
  versionBump: "NO_CHANGE",
42248
- commitMessage
42290
+ commitMessage,
42291
+ analysisWarning
42249
42292
  };
42250
42293
  }
42251
42294
  /**
@@ -42253,7 +42296,7 @@ var AutoVersionStep = class extends BaseStep {
42253
42296
  * the bumped version but does NOT commit — GithubStep handles the commit.
42254
42297
  */
42255
42298
  async finalizeWithBumpNonReplay(params) {
42256
- const { service, language, mappedMagicVersion, previousVersion } = params;
42299
+ const { service, language, mappedMagicVersion, previousVersion, analysisWarning } = params;
42257
42300
  const analysis = this.withChangelogFallback(params.analysis);
42258
42301
  const newVersion = incrementVersion(previousVersion, analysis.versionBump);
42259
42302
  this.logger.info(
@@ -42275,7 +42318,8 @@ var AutoVersionStep = class extends BaseStep {
42275
42318
  previousVersion,
42276
42319
  versionBump: analysis.versionBump,
42277
42320
  prDescription: analysis.prDescription,
42278
- versionBumpReason: analysis.versionBumpReason
42321
+ versionBumpReason: analysis.versionBumpReason,
42322
+ analysisWarning
42279
42323
  };
42280
42324
  }
42281
42325
  /**
@@ -42676,6 +42720,68 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
42676
42720
  };
42677
42721
  }
42678
42722
  }
42723
+ /**
42724
+ * Chunked retry for the hosted FAI path. Each chunk is analyzed independently;
42725
+ * chunks whose request fails are skipped as long as at least one chunk succeeds,
42726
+ * and the partial coverage is reported via `analysisWarning` so the PR is flagged
42727
+ * and never automerged. Results are aggregated with the same max-bump rule as the
42728
+ * BAML path; entries are joined rather than consolidated (no hosted rollup endpoint).
42729
+ */
42730
+ async analyzeChunksViaFaiService(chunks, language, previousVersion, label) {
42731
+ const chunkAnalyses = [];
42732
+ let attemptedChunks = 0;
42733
+ let failedChunks = 0;
42734
+ let lastError;
42735
+ for (let i2 = 0; i2 < chunks.length; i2++) {
42736
+ const chunk = chunks[i2];
42737
+ if (!chunk) {
42738
+ continue;
42739
+ }
42740
+ attemptedChunks++;
42741
+ let analysis;
42742
+ try {
42743
+ analysis = await this.analyzeViaFaiService(chunk, language, previousVersion);
42744
+ } catch (error) {
42745
+ failedChunks++;
42746
+ lastError = error;
42747
+ this.logger.warn(
42748
+ `${label}: FAI analysis of chunk ${i2 + 1}/${chunks.length} failed (${String(error)}).`
42749
+ );
42750
+ continue;
42751
+ }
42752
+ if (analysis == null) {
42753
+ continue;
42754
+ }
42755
+ chunkAnalyses.push(analysis);
42756
+ }
42757
+ if (failedChunks > 0 && failedChunks === attemptedChunks) {
42758
+ throw new Error(`all ${attemptedChunks} FAI chunk requests failed; last error: ${String(lastError)}`);
42759
+ }
42760
+ const analysisWarning = failedChunks > 0 ? `${failedChunks} of ${attemptedChunks} FAI chunk requests failed; last error: ${String(lastError)}` : void 0;
42761
+ if (chunkAnalyses.length === 0) {
42762
+ return { analysis: null, analysisWarning };
42763
+ }
42764
+ const { bestBump, bestMessage, bestVersionBumpReason, changelogEntries, usedBumpReasonAsEntry } = aggregateChunkAnalyses(chunkAnalyses);
42765
+ if (usedBumpReasonAsEntry) {
42766
+ this.logger.warn(
42767
+ `${label}: no chunk produced a changelog entry for the ${bestBump} bump; using its version bump reason instead.`
42768
+ );
42769
+ }
42770
+ if (analysisWarning != null) {
42771
+ this.logger.warn(`${label}: ${analysisWarning}; the ${bestBump} bump is based on the remaining chunks.`);
42772
+ }
42773
+ const prDescriptions = chunkAnalyses.filter((analysis) => analysis.versionBump === bestBump && hasText(analysis.prDescription)).map((analysis) => analysis.prDescription?.trim() ?? "");
42774
+ return {
42775
+ analysis: {
42776
+ versionBump: bestBump,
42777
+ message: bestMessage,
42778
+ changelogEntry: changelogEntries.length > 0 ? changelogEntries.join("\n\n") : void 0,
42779
+ prDescription: prDescriptions.length > 0 ? prDescriptions.join("\n\n") : void 0,
42780
+ versionBumpReason: bestVersionBumpReason
42781
+ },
42782
+ analysisWarning
42783
+ };
42784
+ }
42679
42785
  /**
42680
42786
  * Calls the hosted FAI service (`/sdks/analyze-commit-diff`) with the fern token.
42681
42787
  * Used when no BAML `ai` config is supplied (remote generation via fiddle). FAI
@@ -42700,7 +42806,10 @@ ${body}` : `${COMMIT_MARKER} ${subject}`;
42700
42806
  });
42701
42807
  if (!response.ok) {
42702
42808
  const body = await response.text().catch(() => "");
42703
- throw new Error(`FAI analyze-commit-diff failed with status ${response.status}: ${body.slice(0, 500)}`);
42809
+ if (body.length > 0) {
42810
+ this.logger.debug(`AutoVersionStep: FAI analyze-commit-diff response body: ${body.slice(0, 500)}`);
42811
+ }
42812
+ throw new Error(`FAI analyze-commit-diff failed with status ${response.status}`);
42704
42813
  }
42705
42814
  const parsed = await response.json();
42706
42815
  if (!isFaiAnalyzeResponse(parsed)) {
@@ -43500,6 +43609,7 @@ var GithubStep = class extends BaseStep {
43500
43609
  );
43501
43610
  const replaySection = formatReplayPrBody(replayResult, { branchName: prBranch, repoUri: this.config.uri });
43502
43611
  let enrichedBody = replaySection != null ? prBody + "\n\n---\n\n" + replaySection : prBody;
43612
+ enrichedBody = appendAutoVersionWarning(enrichedBody, resolved.analysisWarning);
43503
43613
  enrichedBody = enrichPrBodyForAutomation(enrichedBody, this.config, resolved);
43504
43614
  if (isUpdatingExistingPR && existingPR != null) {
43505
43615
  this.logger.info(`Updated existing pull request: ${existingPR.htmlUrl}`);
@@ -43758,9 +43868,25 @@ function resolvePrFields(config, autoVersion) {
43758
43868
  newVersion: config.newVersion ?? autoVersion?.version,
43759
43869
  versionBump: config.versionBump ?? autoVersion?.versionBump,
43760
43870
  hasBreakingChanges: config.hasBreakingChanges ?? autoVersionBreaking,
43761
- breakingChangesSummary: config.breakingChangesSummary ?? autoVersion?.prDescription
43871
+ breakingChangesSummary: config.breakingChangesSummary ?? autoVersion?.prDescription,
43872
+ analysisWarning: autoVersion?.analysisWarning
43762
43873
  };
43763
43874
  }
43875
+ function appendAutoVersionWarning(body, analysisWarning) {
43876
+ if (analysisWarning == null || analysisWarning.trim().length === 0) {
43877
+ return body;
43878
+ }
43879
+ const reason = analysisWarning.replace(/[`\s]+/g, " ").trim().slice(0, 300);
43880
+ return body + `
43881
+
43882
+ ---
43883
+
43884
+ ## \u26A0\uFE0F Version bump not verified
43885
+
43886
+ Automatic version analysis was unavailable or incomplete for this generation, so the version bump and changelog may understate the changes (a **PATCH** bump with no changelog is applied when no analysis is available). If this change is breaking or adds features, adjust the version and changelog before merging.
43887
+
43888
+ Reason: \`${reason}\``;
43889
+ }
43764
43890
  function enrichPrBodyForAutomation(body, config, breaking = {}) {
43765
43891
  if (!config.automationMode) {
43766
43892
  return body;
@@ -43782,7 +43908,8 @@ _Fern Run ID: \`${config.runId}\`_`;
43782
43908
  }
43783
43909
  function shouldEnableAutomerge(config, breaking = {}) {
43784
43910
  const hasBreakingChanges = breaking.hasBreakingChanges ?? config.hasBreakingChanges;
43785
- return config.automationMode === true && config.autoMerge === true && hasBreakingChanges !== true;
43911
+ const analysisUnavailable = breaking.analysisWarning != null && breaking.analysisWarning.length > 0;
43912
+ return config.automationMode === true && config.autoMerge === true && hasBreakingChanges !== true && !analysisUnavailable;
43786
43913
  }
43787
43914
  function shouldCheckNoDiff(config) {
43788
43915
  return config.skipIfNoDiff === true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fern-api/generator-cli",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fern-api/fern.git",