ci: leverage pnpm for breaking change check by danieljbruce · Pull Request #9634 · googleapis/google-cloud-node · GitHub
Skip to content

ci: leverage pnpm for breaking change check - #9634

Merged
danieljbruce merged 2 commits into
googleapis:mainfrom
danieljbruce:ci/pnpm-breaking-change-check
Oct 15, 2026
Merged

danieljbruce merged 2 commits into
googleapis:mainfrom
danieljbruce:ci/pnpm-breaking-change-check

Conversation

@danieljbruce

@danieljbruce danieljbruce commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR adds a lightweight, pnpm-based breaking-change check workflow as an alternative to the 1,764-line custom TypeScript AST/TypeChecker script in #9583, without adding any custom script files in bin/:

  1. Workspace dependent compilation via pnpm --filter "...[HEAD^1...HEAD]": Passes --config.link-workspace-packages=deep --config.prefer-workspace-packages=true to pnpm so modified core/* and handwritten/* packages are linked and compiled alongside their downstream workspace dependents (catching cross-package breakages like feat(gax): emit T4 attempt spans #9496 where a google-gax change broke @google-cloud/pubsub).
  2. Declaration comparison via pnpm dlx @api-extractor-tools/change-detector: Emits .d.ts declarations for modified TypeScript files (HEAD^1 vs HEAD) and runs pnpm dlx @api-extractor-tools/change-detector /tmp/base.d.ts /tmp/head.d.ts --json, flagging any removed exports/members (action === 'removed') or narrowed types (impact === 'narrowing').

Impact

  • CI / Pull Requests: Automatically checks core/* and handwritten/* TypeScript changes for breaking public API changes and cross-package compilation regressions.
  • Maintainability: Only adds .github/workflows/breaking-change-check.yaml (113 lines) with zero custom bin/breaking-change-check.mjs code to maintain.

Changes

  • .github/workflows/breaking-change-check.yaml: Compiles affected packages and workspace dependents via pnpm --filter "...[HEAD^1...HEAD]" and checks modified declarations using pnpm dlx @api-extractor-tools/change-detector.

Testing

Alternatives

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new tool, breaking-change-check.mjs, along with its test suite, to detect breaking changes in public TypeScript interfaces across git revisions. The feedback identifies three key areas for improvement: refining the regex used to strip private members to prevent syntax errors on inline types with semicolons, adding a safety check to ensure filePath is defined during git diff parsing to avoid runtime crashes, and improving the pre-compiled .d.ts lookup logic to support different build output structures with or without the src directory prefix.

Comment thread bin/breaking-change-check.mjs Outdated
Comment on lines +184 to +186

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The regex private\s+[^;]+; is fragile and can cause syntax errors in the stripped .d.ts files. Specifically, if a private member has an inline type containing a semicolon (e.g., private config: { host: string; port: number; }; or a multi-line signature with inline types), [^;]+ will stop at the first semicolon inside the type definition. Because of the gm flags, the regex will match and replace only the portion up to that first semicolon, leaving the rest of the declaration (e.g., port: number; };) intact, which results in invalid TypeScript syntax and compiler errors.

To make this safer, we can use a greedy match for single-line private members, which will match up to the last semicolon on the line.

Suggested change
const strippedDts = rawDts
.replace(/^\s*(?:static\s+)?(?:#private;|private\s+[^;]+;)\s*$/gm, '')
.replace(/^(\s*(?:static\s+|readonly\s+|abstract\s+)*)protected\s+/gm, '$1');
const strippedDts = rawDts
.replace(/^\s*(?:static\s+)?(?:#private;|private\s+.*;)\s*$/gm, '')
.replace(/^(\s*(?:static\s+|readonly\s+|abstract\s+)*)protected\s+/gm, '$1');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 7a9a8ed and e382c51: removed the custom bin/breaking-change-check.mjs regex stripping logic altogether and switched to pnpm dlx @api-extractor-tools/change-detector, which parses the TypeScript AST directly without regex-stripping private members.

Comment thread bin/breaking-change-check.mjs Outdated
Comment on lines +1085 to +1087
const filePath = status.startsWith('R') ? parts[2] : parts[1];
const oldFilePath = parts[1];
if (!filePath.endsWith('.ts') || isIgnoredSourceFile(filePath)) continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If git diff returns an unexpected format or if parts does not contain the expected number of elements, filePath could be undefined. Calling filePath.endsWith or passing it to isIgnoredSourceFile (which calls filePath.split) will then throw a TypeError and crash the script.

We should add a guard to ensure filePath is defined.

Suggested change
const filePath = status.startsWith('R') ? parts[2] : parts[1];
const oldFilePath = parts[1];
if (!filePath.endsWith('.ts') || isIgnoredSourceFile(filePath)) continue;
const filePath = status.startsWith('R') ? parts[2] : parts[1];
const oldFilePath = parts[1];
if (!filePath || !filePath.endsWith('.ts') || isIgnoredSourceFile(filePath)) continue;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e382c51: added a guard in .github/workflows/breaking-change-check.yaml to ensure status, file, and oldPath are defined and that file ends with .ts (excluding .test.ts/.spec.ts) before proceeding.

Comment thread bin/breaking-change-check.mjs Outdated
Comment on lines +1173 to +1179

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

If the package's tsconfig.json specifies "rootDir": "src" (which is the default/common setup), the compiled .d.ts files will be emitted directly under build/ (e.g., build/index.d.ts) rather than preserving the src/ directory structure (e.g., build/src/index.d.ts). Since relInPkg includes the src/ prefix, compiledDts will look for build/src/..., which won't exist, causing the tool to silently fall back to transpiling the source file on the fly.

We can make this more robust by checking both paths.

      const relInPkg = path.relative(pkgDir, real);
      let compiledDts = path.join(
        pkgDir,
        'build',
        relInPkg.replace(/\.ts$/, '.d.ts'),
      );
      if (!fs.existsSync(compiledDts) && relInPkg.startsWith('src' + path.sep)) {
        const strippedRel = relInPkg.substring('src'.length + 1);
        const alternativeDts = path.join(pkgDir, 'build', strippedRel.replace(/\.ts$/, '.d.ts'));
        if (fs.existsSync(alternativeDts)) {
          compiledDts = alternativeDts;
        }
      }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e382c51: added findCompiledDts in .github/workflows/breaking-change-check.yaml to check both build/<relInPkg>.d.ts (e.g. build/src/index.d.ts) and build/<relWithoutSrc>.d.ts (e.g. build/index.d.ts) before falling back to ts.transpileDeclaration.

@danieljbruce
danieljbruce force-pushed the ci/pnpm-breaking-change-check branch from bc72deb to 7a9a8ed Compare October 15, 2026 18:54
@danieljbruce danieljbruce changed the title ci: leverage pnpm and .d.ts declarations for breaking change check ci: leverage pnpm for breaking change check Oct 15, 2026
@danieljbruce
danieljbruce requested a review from quirogas October 15, 2026 19:19
@danieljbruce
danieljbruce marked this pull request as ready for review October 15, 2026 19:19
@danieljbruce
danieljbruce requested a review from a team as a code owner October 15, 2026 19:19
@github-actions
github-actions Bot requested a review from feywind October 15, 2026 19:19
@danieljbruce
danieljbruce merged commit e971073 into googleapis:main Oct 15, 2026
47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants