[DX-1319] Add @elevenlabs/dts-breaking-changes engine + composite action - #917
Draft
kraenhansen wants to merge 15 commits into
Draft
[DX-1319] Add @elevenlabs/dts-breaking-changes engine + composite action#917kraenhansen wants to merge 15 commits into
kraenhansen wants to merge 15 commits into
Conversation
kraenhansen
added a commit
to elevenlabs/elevenlabs-js
that referenced
this pull request
Aug 6, 2026
The action moved from the private elevenlabs-dx to the public elevenlabs/packages repo (a public repo cannot consume an action from a private one). References the feature branch until elevenlabs/packages#917 merges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 tasks
kraenhansen
added a commit
that referenced
this pull request
Aug 6, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, discovers packages with a public type entry, and runs the dts-breaking-changes action once to report all packages in one PR comment. A break is acknowledged by the `breaking` label or a `major` changeset for the package. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine and multi-surface action this uses live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 6, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 6, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
force-pushed
the
kh/dx-1319-dts-breaking-changes
branch
from
August 6, 2026 14:54
a029ea0 to
af7ec2d
Compare
kraenhansen
added a commit
that referenced
this pull request
Aug 6, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Variance-aware breaking-change detection for the public TypeScript packages (DX-1319). Hosted here (public) rather than the private docs repo, because a public repo cannot consume a GitHub Action from a private one. Engine (packages/dts-breaking-changes): pure analysis. Given two dirs of already-built .d.ts (base and head), it reports whether the head breaks existing consumers. It builds nothing; the caller supplies both trees. Models each package's public surface as a module type and uses the compiler's structural assignability to classify input vs output changes: consumer direction (New assignable to Old) breaks are gated; forward direction is informational. The MethodsToProperties transform reconstructs method signatures so parameters are checked contravariantly (identity mapped types keep method bivariance, which would hide added-required-input breaks) and drops private/protected members. Type-only exports (interfaces/type aliases with no value meaning) are compared by name in type space behind compareTypeOnlyExports (default false), because a package like @elevenlabs/types is ~150 interfaces with no values and would look empty to the value surface. A bare type's variance is ambiguous, so its findings use a heuristic (removal/narrowing breaking, added-required warning) kept separate from the value-surface convention. Composite action (.github/actions/dts-breaking-changes): runs the engine, posts a sticky PR comment, gates the check; the `breaking` label downgrades a failure to a warning. Zero external action dependencies (node/npm/gh/jq/bash only). Wired into the pnpm workspace with packages conventions (test, check-types, lint:prettier; prettier reformatted to this repo's config). 12 tests pass (variance, private members, overloads, identical, type-only add/remove/narrow, full elevenlabs-js surface no TS2589), verified via turbo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pilot surfaced false positives on a PR that changes no .d.ts: every finding bottomed out at "Types have separate declarations of a private property 'level'". A Logger class with a private member is reachable only through a parameter position (Client -> Options -> fetcher -> Args -> logging -> Logger), which MethodsToProperties keeps positional (to preserve variance/overloads) and so never strips. Comparing two independent builds then treats those private members as nominally distinct. Private/protected members are not part of the consumer-visible contract, so such a mismatch can never be a real breaking change. Drop any finding whose message is a "separate declarations of a private/protected property" (or "is private/ protected in type") artifact. The self-comparison surface test missed this because comparing a dir to itself shares module identity. Harden it to compare two separate copies (the real workflow scenario), and add a small nominal-private-param fixture that reproduces the bug directly. 13 tests pass; two identical elevenlabs-js builds now diff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A <small> footer linking to the run that generated the comment gives it provenance without changing the bot identity (which is fixed to the github-actions token). The run URL is CI-specific, so it lives in the action, not the engine. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the engine and action to check several packages in one run and report them in a single comment. Moved here (from the packages-workflow PR) so the action is complete in its home PR. - Engine: `renderCombined` renders one report with a `## <title>` heading per package; the CLI gains `--surfaces <file>` (analyze N + combine + gate) and `--label-acknowledged`. External JSON (surfaces manifest, config files) is validated with zod instead of cast. - Action: accepts a `surfaces` JSON array (or the single-surface inputs, wrapped as one), runs the engine once, posts one sticky comment with a section per package, and gates on the combined result. A break is acknowledged by the `breaking` label (all surfaces) or a surface's `allowBreaking`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the brittle package/entrypoint heuristic with proper resolution in the engine, and add a workspace mode to the action. - `discover.ts`: enumerate workspace packages (pnpm-workspace.yaml globs or package.json workspaces) and resolve each package's public type entrypoints using the TS compiler's own module resolution (self-reference from inside the package dir). This honors `exports`, the `types` condition, implicit sibling `.d.ts`, and every export subpath — so `@elevenlabs/client` yields `.`, `./internal`, and `./internal/unity`, and implicitly-typed packages like `@elevenlabs/react-native` are no longer skipped. A package can drop subpaths via `ignoreEntrypoints` in its dts-breaking-changes.json. - CLI `--discover --base-root <dir> --head-root <dir> [--allow-breaking-packages <csv>]` emits the surfaces JSON; each (package, subpath) is its own surface. - Action: `base-root`/`head-root`/`allow-breaking-packages` inputs run discovery, so callers no longer hand-build the surfaces list. Adds `yaml` for pnpm-workspace parsing. Fixtures cover explicit types, subpaths, implicit siblings, legacy types/main, and no-types packages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Discovered when the workspace run flagged @elevenlabs/client (which changes no
source) as breaking:
- Protected-member nominal artifact: TS phrases the protected variant of the
cross-build nominal mismatch as "Property 'x' is protected but type 'Y' is not
a class derived from 'Y'", which the private-only filter missed. Broaden
isNominalAccessArtifact to catch any private/protected member nominal message.
- Type-only re-export of a value: `export type { X }` where X resolves to a
class is not in the value namespace, but getAliasedSymbol resolves through the
`type` modifier and marked it a value export, so per-symbol localization
indexed a name absent from `typeof import()` and raised a spurious TS2339.
Honor the export-specifier's type-only modifier when classifying exports.
Regression fixture nominal-reexport reproduces both; the real @elevenlabs/client
surfaces (`.`, `./internal`, `./internal/unity`) now self-compare clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
force-pushed
the
kh/dx-1319-dts-breaking-changes
branch
from
August 6, 2026 15:54
af7ec2d to
67a2a2a
Compare
kraenhansen
added a commit
that referenced
this pull request
Aug 6, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Callbacks the consumer supplies are checked doubly-contravariantly (the event the library passes is itself a parameter). Lock down the variance: a callback event gaining a field is safe, losing one breaks; requiring a callback to return a value breaks; a new required callback on an options bag breaks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Model each type surface as (package, subpath, condition). Discovery now enumerates the export conditions in each subpath's exports value and resolves each via the TS compiler's customConditions, deduping by resolved .d.ts — so a condition pointing at a different type surface (e.g. @elevenlabs/react-native's `react-native` -> index.react-native.d.ts) is checked too. Conditions resolving to the same file as `default` are folded into it. The combined report groups results by package with a subsection per entrypoint (subpath, plus the condition when it isn't the default), and the summary counts packages rather than entrypoints. Fixtures cover a condition-divergent package and the grouped rendering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 7, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an `apiSummary` mode that attaches a structural, member-level summary of API additions, removals, and changes to the report, rendered as a GitHub ```diff block. It is a review aid only: the gate stays authored entirely by tsc, and the differ enumerates rather than judges. The differ (`diffApi`) walks both module surfaces' value exports via the compiler's symbol table, recursing one level into changed containers so a new method or a changed member signature shows up as its own line (e.g. `+ Client.cancel: (id: string) => void`). Class instance exports always recurse structurally, since `typeToString` renders their nominal name and would otherwise compare equal. Wired through the config (`apiSummary`, default false), the CLI (`--api-summary`), and the composite action (`api-summary` input). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 7, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The combined comment rendered a full heading and a "No type-surface changes" block for every package (and every entrypoint), which drowned the packages that actually changed. Now only surfaces with findings or API changes get a rendered section; the rest roll into a single `<small>` line so a reviewer can still confirm they were considered. Also fixes the summary wording when a package has additive API changes but no breaking findings: it now reads "No consumer-breaking changes" rather than claiming "No type-surface changes". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 7, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; list entrypoints considered
The API diff rendered spurious changes for exports typed as
`typeof import("…")` (e.g. namespace re-exports of type-only modules):
the base and head trees resolve under different absolute roots, so an
unchanged module read as changed. Normalize `import("…")` to the bare
module name before comparing and displaying, which also reads better.
Also expand the "considered" footer to list each unchanged package's
entrypoints and conditions, not just the package name, so a reviewer can
confirm every surface was checked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 7, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
to elevenlabs/elevenlabs-js
that referenced
this pull request
Aug 7, 2026
The action moved from the private elevenlabs-dx to the public elevenlabs/packages repo (a public repo cannot consume an action from a private one). References the feature branch until elevenlabs/packages#917 merges. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop the title from the sticky marker (`<!-- dts-breaking-changes -->`): there is one combined comment per PR, so it needs no disambiguator. - Move the "Compared against base <sha>" line to the footer, next to the generated-by line, so the verdict leads. - Reword the unchanged-packages ledger from "No type-surface changes in …" to a terse "Unchanged: …" so it reads as detail under the verdict rather than repeating it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 7, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render "Compared against base <sha>" and "Generated by the … workflow" on a single `<small>` line. The comment step now owns the base SHA (bare, so it still auto-links) and no longer passes `--base-sha` to the engine, which keeps the whole provenance note in one place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kraenhansen
added a commit
that referenced
this pull request
Aug 7, 2026
Single job that builds head and the merge-base (git worktree) once with turbo, then runs the dts-breaking-changes action in workspace mode: the action discovers every package's public type entrypoints and reports them in one PR comment. Major changesets are detected and passed as `allow-breaking-packages`. `@elevenlabs/types` is type-only, so it ships dts-breaking-changes.json with `compareTypeOnlyExports: true`. The engine, discovery, and multi-surface action live in the base PR (#917). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Variance-aware breaking-change detection for our public TypeScript packages: an analysis engine (
packages/dts-breaking-changes) and a composite action (.github/actions/dts-breaking-changes). The workflow that drives it in this repo is #918.Engine
Given two directories of already-built
.d.ts(base and head), it reports whether the head breaks existing consumers. It builds nothing — the caller supplies both trees.Newassignable toOld) failures are breaking; forward direction is informational. Adding a required field to an input type is breaking; to an output type is not.MethodsToPropertiesreconstructs method signatures so parameters are checked contravariantly (methods are otherwise bivariant) and dropsprivate/protectedmembers (not part of the consumer contract).pnpm-workspace.yamland resolves each package's public type entrypoints via the compiler's own module resolution — honoringexports, thetypescondition, implicit sibling.d.ts, and every subpath (e.g.@scope/pkg,@scope/pkg/internal). A package can drop subpaths withignoreEntrypointsin itsdts-breaking-changes.json.compareTypeOnlyExportscompares pure type exports by name (for packages like@elevenlabs/typesthat have no value surface). Removal/narrowing is breaking; an added required field is a warning.## <title>section per surface. External JSON (surfaces, config files) is validated with zod.Action
Runs the engine and posts one sticky PR comment (a section per package), failing the check on an unacknowledged consumer-breaking change. Three input modes: workspace (
base-root— discovers everything), explicit (surfaces), or single-surface (old-dir/new-dir). A break is acknowledged (downgraded to a warning) by thebreakinglabel or a package's major changeset. Zero external action dependencies (node/npm/gh/jq/bash).Test plan
elevenlabs-jssurface with noTS2589turbo check-types/turbo lintgreen, including whole-repo lint with the package added@elevenlabs/client's././internal/./internal/unity) and reports them in one comment🤖 Generated with Claude Code