Skip to content

fix: only treat call-signature and type changes as breaking in release metadata#32

Open
gjtorikian wants to merge 2 commits into
mainfrom
fix/breaking-change-classification
Open

fix: only treat call-signature and type changes as breaking in release metadata#32
gjtorikian wants to merge 2 commits into
mainfrom
fix/breaking-change-classification

Conversation

@gjtorikian

Copy link
Copy Markdown
Collaborator

Summary

scripts/sdk-release-metadata.mjs over-classified changes as breaking, producing feat! (major-bump) commits for what are really backend API changes. It inherited the spec differ's classification for field / enum-value / response / request changes, and trusted every breaking-severity compat symbol — including the model properties the compat snapshot tracks (~2000 of them).

This tightens the rule so a change is breaking (feat!) only when:

  • the call signature a user invokes changes — parameters / constructor (parameter_removed, parameter_renamed, parameter_requiredness_increased, parameter_type_narrowed, signature, operation param changes), or
  • a whole type is removed/renamed — model / enum / service, or a method is removed/renamed.

Everything else becomes a non-breaking backend API change (fix):

  • a field renamed or removed, removed response data, field type/format/required changes
  • an enum value removed
  • a response/request-body type renamed

How

  • Spec-diff path: a capSeverity helper downgrades would-be-breaking field-*, value-*, response-changed, and request-body-changed facts to fix. The made-required/optional changelog wording, which previously rode on severity === 'breaking', now carries an explicit madeRequired flag on the fact.
  • Compat path: compatChangeIsBreaking skips backend-only categories (field_type_changed, enum_member_value_changed, return_type_changed, default_value_changed) and, for symbol_removed/symbol_renamed, distinguishes a model property (field → not breaking) from a service/client method or whole type (→ breaking) by checking the symbol's owner against the IR's service/type names (now exposed from buildIndexes).

By this same rule the tooling change itself is not breaking → fix:, no !.

Test plan

Verified with synthetic and real fixtures (no automated test exists for this script):

  • Mixed spec diff → model-removed + operation-removed render under ⚠️ Breaking; field-removed, made-required, value-removed, response-changed render under Fixes.
  • Field + response changes only → no feat!; rollup is fix.
  • Real .oagen/ruby/compat-report.jsonencryptor parameter removal and SSO / UserManagement method removals stay breaking.
  • Property-vs-method discriminator → ActionAuthenticationDenied.context (property) and a field_type_changed are dropped; SSO.get_authorization_url (method) stays breaking.
  • --format json output keeps correct prefixes; madeRequired does not leak into the emitted changes[].

🤖 Generated with Claude Code

gjtorikian and others added 2 commits June 17, 2026 17:30
Two improvements to the changelog produced by sdk-release-metadata.mjs,
both aimed at the local "--spec-commit --sdk-repo --format changelog" flow:

1. Auto-derive the SDK compat report from the checkout when --compat-report
   is omitted. It snapshots the current tree (candidate) and the tree at the
   base ref (baseline, via a throwaway git worktree) and diffs them — no SDK
   regeneration. This is what surfaces SDK-surface changes like method
   renames, which the spec diff alone cannot see. Fails open (spec-diff-only
   changelog) and is skipped when --compat-report is passed, so CI is
   unaffected. Language is inferred from the repo basename (workos-python →
   python) or set via --lang.

2. Name the types in operation response/request-body changes: instead of
   "Changed response for `X.list`", emit "Changed response of `X.list` from
   `UserOrganizationMembership` to `UserOrganizationMembershipList`", read
   from the old/new IR (the diff report only carries a boolean). Falls back
   to the generic wording when a type name isn't resolvable.

Verified end-to-end against workos-python PR #670 (rename + enriched response
lines) and across all per-language compat reports under --strict-scopes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e metadata

sdk-release-metadata.mjs marked field, enum-value, and response/request-shape
changes as breaking — it inherited the spec differ's classification and trusted
every breaking-severity compat symbol, including the model properties the compat
snapshot tracks. That emitted feat! (major-bump) commits for what are really
backend API changes.

Cap them so only a changed call signature (parameters, constructor) or a
removed/renamed type (model/enum/service) or removed method stays breaking. Field
renames/removals, removed response data, dropped enum values, and response/request
body type renames now land under Fixes. The compat path tells a model property
(field — not breaking) apart from a service method using the IR's service/type
names, so SDK-surface method removals/renames stay breaking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown

Greptile Summary

  • Narrows SDK release metadata so only SDK call-signature changes and whole-type removals or renames are treated as breaking.
  • Downgrades backend-only spec-diff categories and compat-report categories to fix-level release metadata.
  • Adds explicit requiredness direction tracking and service/type indexes to support the new classification rules.

Confidence Score: 4/5

The release metadata changes need attention before merge because several remaining paths can still produce incorrect changelog or rollup output.

Focused runtime checks exercised the affected CLI paths and confirmed the reported release classification issues with synthetic fixtures and controlled harnesses.

scripts/sdk-release-metadata.mjs

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex reproduced mixed requiredness misreports by running a diff fixture with two identical-scope field-required-changed facts for foo, which were grouped into a single fix entry with an incorrect summary.
  • T-Rex reproduced that filtered breaks still bump the metadata rollup, using a minimal raw compat report fixture with only a breaking field_type_changed change and a workflow-compatible harness that yields ROLLUP_TYPE=feat and ROLLUP_BANG(!).
  • T-Rex reproduced that required field additions break, with the CLI JSON output classifying the entry as feat! and breaking, and the changelog rendering feat(generated)! with a Breaking section.
  • T-Rex ran the requested verification, but its local artifact references were not uploaded.
  • T-Rex compared base and head artifact classifications and observed that base reported value-removed, response-changed, and request-body-changed as breaking, while head emitted backend-only value-removed, response-changed, and request-body-changed with a single field-required-changed as fix, preserving wording and not emitting madeRequired in changes.
  • T-Rex evaluated further and noted a reduction from eight to four feat!/breaking entries when filtering, confirming which categories and changes survive the filtering.

View all artifacts

T-Rex Ran code and verified through T-Rex

Fix All in Devin Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
scripts/sdk-release-metadata.mjs:974-976
**Mixed requiredness misreports**

When the same field is made required in one model and optional in another model within the same scope, both facts now end up in the `fix` group after severity capping. This branch treats any mixed group as optional because `every((fact) => fact.madeRequired)` is false, so callers can receive a summary like `Make foo optional in SDK models` even though one of the included changes made `foo` required. Please only use the grouped required/optional wording when all facts have the same direction; mixed directions should fall back to the per-fact details.

### Issue 2 of 4
scripts/sdk-release-metadata.mjs:904
**Filtered breaks still bump**

This filters backend-only compat changes out of the metadata entries, but the release workflow still reads the raw compat report and forces `ROLLUP_TYPE=feat` / `ROLLUP_BANG=!` whenever any raw `severity == "breaking"` compat row exists. A `field_type_changed`, `return_type_changed`, or model-property `symbol_removed` that this function drops can still produce a major-bump PR because the workflow does not apply `compatChangeIsBreaking`. The filtered result needs to be the single source used for the rollup, or the workflow needs the same filtering before it counts compat breaks.

### Issue 3 of 4
scripts/sdk-release-metadata.mjs:606-616
**Required field additions break**

`field-added` is not included in the capped backend-only kinds, so a required field addition reported by `oagen diff` with `classification: "breaking"` still flows through as `feat!`. For example, adding a required `Foo.bar` model field produces a breaking release entry even though this PR’s policy treats model field shape changes as backend API fixes. This can still create major-bump changelog fragments for field additions.

```suggestion
const BACKEND_ONLY_DIFF_KINDS = new Set([
  'field-added',
  'field-removed',
  'field-type-changed',
  'field-format-changed',
  'field-required-changed',
  'field-access-changed',
  'value-removed',
  'value-modified',
  'response-changed',
  'request-body-changed',
]);
```

### Issue 4 of 4
scripts/sdk-release-metadata.mjs:272-295
**Historical compat uses current spec**

When the documented manual flow passes `--spec-commit --sdk-repo` without `--compat-report`, `prepareSpecInputs()` creates temporary old/new spec and IR files for that commit, but this compat path recomputes `specPath` from the current checkout. A historical changelog repair can therefore run `compat-extract` against today’s `spec/open-api-spec.yaml` instead of the requested commit’s spec, causing false compat failures or missing SDK-surface changes after the fail-open warning path.

Reviews (1): Last reviewed commit: "fix: only treat call-signature and type ..." | Re-trigger Greptile

Comment on lines +974 to 976
return facts.every((fact) => fact.madeRequired)
? `Make ${code(commonField)} required in ${label} models`
: `Make ${code(commonField)} optional in ${label} models`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Mixed requiredness misreports

When the same field is made required in one model and optional in another model within the same scope, both facts now end up in the fix group after severity capping. This branch treats any mixed group as optional because every((fact) => fact.madeRequired) is false, so callers can receive a summary like Make foo optional in SDK models even though one of the included changes made foo required. Please only use the grouped required/optional wording when all facts have the same direction; mixed directions should fall back to the per-fact details.

Artifacts

Repro: diff fixture with mixed requiredness directions for the same field

  • Contains supporting evidence from the run (application/json; charset=utf-8).

Repro: old IR fixture placing both models in the same widget scope

  • Contains supporting evidence from the run (application/json; charset=utf-8).

Repro: new IR fixture placing both models in the same widget scope

  • Contains supporting evidence from the run (application/json; charset=utf-8).

Repro: JSON CLI output showing the mixed group summarized as optional

  • Keeps the command output available without making the summary code-heavy.

Repro: changelog CLI output showing the mixed group rendered as optional

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/sdk-release-metadata.mjs
Line: 974-976

Comment:
**Mixed requiredness misreports**

When the same field is made required in one model and optional in another model within the same scope, both facts now end up in the `fix` group after severity capping. This branch treats any mixed group as optional because `every((fact) => fact.madeRequired)` is false, so callers can receive a summary like `Make foo optional in SDK models` even though one of the included changes made `foo` required. Please only use the grouped required/optional wording when all facts have the same direction; mixed directions should fall back to the per-fact details.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Devin Fix in Claude Code

const isAsync = (change) => (/^Async(?=[A-Z])/.test(String(change.symbol ?? '')) ? 1 : 0);
const breakingChanges = (compatReport?.changes ?? [])
.filter((change) => change.severity === 'breaking')
.filter((change) => change.severity === 'breaking' && compatChangeIsBreaking(change, indexes))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Filtered breaks still bump

This filters backend-only compat changes out of the metadata entries, but the release workflow still reads the raw compat report and forces ROLLUP_TYPE=feat / ROLLUP_BANG=! whenever any raw severity == "breaking" compat row exists. A field_type_changed, return_type_changed, or model-property symbol_removed that this function drops can still produce a major-bump PR because the workflow does not apply compatChangeIsBreaking. The filtered result needs to be the single source used for the rollup, or the workflow needs the same filtering before it counts compat breaks.

Artifacts

Repro: minimal raw compat report fixture with filtered breaking field_type_changed change

  • Contains supporting evidence from the run (application/json; charset=utf-8).

Repro: workflow-compatible raw severity rollup harness

  • Contains supporting evidence from the run (text/x-shellscript; charset=utf-8).

Repro: sdk-release-metadata output omitting the filtered compat change

  • Contains supporting evidence from the run (application/json; charset=utf-8).

Repro: command output showing empty metadata entries but feat bang rollup

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/sdk-release-metadata.mjs
Line: 904

Comment:
**Filtered breaks still bump**

This filters backend-only compat changes out of the metadata entries, but the release workflow still reads the raw compat report and forces `ROLLUP_TYPE=feat` / `ROLLUP_BANG=!` whenever any raw `severity == "breaking"` compat row exists. A `field_type_changed`, `return_type_changed`, or model-property `symbol_removed` that this function drops can still produce a major-bump PR because the workflow does not apply `compatChangeIsBreaking`. The filtered result needs to be the single source used for the rollup, or the workflow needs the same filtering before it counts compat breaks.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Devin Fix in Claude Code

Comment on lines +606 to +616
const BACKEND_ONLY_DIFF_KINDS = new Set([
'field-removed',
'field-type-changed',
'field-format-changed',
'field-required-changed',
'field-access-changed',
'value-removed',
'value-modified',
'response-changed',
'request-body-changed',
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Required field additions break

field-added is not included in the capped backend-only kinds, so a required field addition reported by oagen diff with classification: "breaking" still flows through as feat!. For example, adding a required Foo.bar model field produces a breaking release entry even though this PR’s policy treats model field shape changes as backend API fixes. This can still create major-bump changelog fragments for field additions.

Suggested change
const BACKEND_ONLY_DIFF_KINDS = new Set([
'field-removed',
'field-type-changed',
'field-format-changed',
'field-required-changed',
'field-access-changed',
'value-removed',
'value-modified',
'response-changed',
'request-body-changed',
]);
const BACKEND_ONLY_DIFF_KINDS = new Set([
'field-added',
'field-removed',
'field-type-changed',
'field-format-changed',
'field-required-changed',
'field-access-changed',
'value-removed',
'value-modified',
'response-changed',
'request-body-changed',
]);
Artifacts

Repro: synthetic oagen diff fixture with breaking field-added change

  • Contains supporting evidence from the run (application/json; charset=utf-8).

Repro: old IR fixture used by the CLI

  • Contains supporting evidence from the run (application/json; charset=utf-8).

Repro: new IR fixture with required added field used by the CLI

  • Contains supporting evidence from the run (application/json; charset=utf-8).

Repro: CLI harness that runs JSON and changelog output paths

  • Contains supporting evidence from the run (text/x-shellscript; charset=utf-8).

Repro: real CLI output showing feat! breaking classification

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/sdk-release-metadata.mjs
Line: 606-616

Comment:
**Required field additions break**

`field-added` is not included in the capped backend-only kinds, so a required field addition reported by `oagen diff` with `classification: "breaking"` still flows through as `feat!`. For example, adding a required `Foo.bar` model field produces a breaking release entry even though this PR’s policy treats model field shape changes as backend API fixes. This can still create major-bump changelog fragments for field additions.

```suggestion
const BACKEND_ONLY_DIFF_KINDS = new Set([
  'field-added',
  'field-removed',
  'field-type-changed',
  'field-format-changed',
  'field-required-changed',
  'field-access-changed',
  'value-removed',
  'value-modified',
  'response-changed',
  'request-body-changed',
]);
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Devin Fix in Claude Code

Comment on lines +272 to +295
const openapiRepo = args['openapi-repo'] ?? process.cwd();
const specPath = join(openapiRepo, args['spec-path'] ?? 'spec/open-api-spec.yaml');
const rawBase = args['sdk-base'] ?? 'origin/main...HEAD';
const leftRef = rawBase.split(/\.\.\.?/)[0] || 'origin/main';

const tmp = mkdtempSync(join(tmpdir(), 'sdk-release-compat-'));
const baselineSrc = join(tmp, 'baseline-src');
let worktreeAdded = false;
let succeeded = false;
try {
const baselineCommit = rawBase.includes('...')
? git(sdkRepo, ['merge-base', leftRef, 'HEAD'])
: git(sdkRepo, ['rev-parse', leftRef]);

git(sdkRepo, ['worktree', 'add', '--detach', baselineSrc, baselineCommit]);
worktreeAdded = true;

const baselineOut = join(tmp, 'baseline');
const candidateOut = join(tmp, 'candidate');
mkdirSync(baselineOut, { recursive: true });
mkdirSync(candidateOut, { recursive: true });

const extract = (sdkPath, output) =>
run('npx', ['oagen', 'compat-extract', '--lang', lang, '--sdk-path', sdkPath, '--output', output, '--spec', specPath], {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Historical compat uses current spec

When the documented manual flow passes --spec-commit --sdk-repo without --compat-report, prepareSpecInputs() creates temporary old/new spec and IR files for that commit, but this compat path recomputes specPath from the current checkout. A historical changelog repair can therefore run compat-extract against today’s spec/open-api-spec.yaml instead of the requested commit’s spec, causing false compat failures or missing SDK-surface changes after the fail-open warning path.

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/sdk-release-metadata.mjs
Line: 272-295

Comment:
**Historical compat uses current spec**

When the documented manual flow passes `--spec-commit --sdk-repo` without `--compat-report`, `prepareSpecInputs()` creates temporary old/new spec and IR files for that commit, but this compat path recomputes `specPath` from the current checkout. A historical changelog repair can therefore run `compat-extract` against today’s `spec/open-api-spec.yaml` instead of the requested commit’s spec, causing false compat failures or missing SDK-surface changes after the fail-open warning path.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Devin Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant