Skip to content

ci: skip CJS api-extractor checks on PR builds#26596

Closed
frankmueller-msft wants to merge 3 commits intomainfrom
ci/skip-cjs-api-extractor-on-prs
Closed

ci: skip CJS api-extractor checks on PR builds#26596
frankmueller-msft wants to merge 3 commits intomainfrom
ci/skip-cjs-api-extractor-on-prs

Conversation

@frankmueller-msft
Copy link
Copy Markdown
Contributor

@frankmueller-msft frankmueller-msft commented Mar 1, 2026

Summary

Skip redundant CJS api-extractor invocations on PR builds to reduce build time. CJS output is byte-for-byte identical to ESM — full CJS checks run on main/release CI where correctness matters.

Measured impact: Build Stage dropped from 18m40s to 16m18s (~2m20s wall-clock improvement).

What changes on PR builds

When SKIP_CJS_CHECKS=true (set automatically for PR builds):

  • api-extractor:commonjs removed from build:test, build:test:cjs, api dependency arrays (~65 invocations, ~6 min CPU)
  • check:exports disabled entirely — the package's concurrently "npm:check:exports:*" script glob-matches both CJS and ESM subtasks, and fluid-build can't selectively suppress CJS subtask invocations (~159 invocations, ~16 min CPU)
  • check:are-the-types-wrong disabled entirelyattw packs the package and checks CJS+ESM type resolution; without the CJS .d.ts rollups from api-extractor, it fails
  • AreTheTypesWrong pipeline job excluded from PR runs (runs via pnpm, not fluid-build)

Note: The check:exports and attw disablement is a cascade effect of skipping CJS api-extractor. The build dependency graph doesn't allow cleanly removing just the CJS step without breaking downstream checks that assume its output exists. A longer-term alternative is moving these checks to a separate parallel validation job (see #25321).

What does NOT change

  • Non-PR builds (main, release branches) run full CJS+ESM checks exactly as before
  • No package.json modifications — all packages keep existing scripts
  • No config file deletionsapi-extractor-lint-*.cjs.json files stay
  • Local npm run build is unaffected (env var not set)

Files changed

File What
fluidBuild.config.cjs Conditional spread for api, build:test, build:test:cjs based on SKIP_CJS_CHECKS env var; disable check:exports and check:are-the-types-wrong when skipping CJS
tools/pipelines/templates/include-vars.yml Add skipCjsChecks pipeline variable (true on PR builds)
tools/pipelines/templates/include-build-lint.yml Pass SKIP_CJS_CHECKS env var to ci:build and lint steps
tools/pipelines/build-client.yml Conditionally exclude AreTheTypesWrong job on PR builds

Test plan

  • PR CI passes — all checks green
  • Build Stage Build: 16m18s (vs 18m40s baseline)
  • Verify main CI still runs full CJS checks after merge

Closes #26592

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 1, 2026 06:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces CI time on Pull Request builds by skipping redundant CommonJS api-extractor validation tasks when CJS entrypoints are identical to ESM, while keeping full checks on main/release branches.

Changes:

  • Add PR-only pipeline variable skipCjsChecks and pass it as SKIP_CJS_CHECKS to build/lint steps.
  • Update fluidBuild.config.cjs to conditionally remove api-extractor:commonjs from key dependency chains when SKIP_CJS_CHECKS is enabled.
  • Override check:exports behavior under SKIP_CJS_CHECKS to run ESM-only export-check subtasks.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
fluidBuild.config.cjs Adds SKIP_CJS_CHECKS gating to omit CJS api-extractor tasks and alters check:exports execution plan on PR builds.
tools/pipelines/templates/include-vars.yml Introduces skipCjsChecks variable (true on PR builds).
tools/pipelines/templates/include-build-lint.yml Passes SKIP_CJS_CHECKS into ci:build and lint pipeline steps.

"tools/markdown-magic/package.json",
];

const skipCjsChecks = process.env.SKIP_CJS_CHECKS === "true";
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

skipCjsChecks is derived via a strict process.env.SKIP_CJS_CHECKS === "true" check. In Azure Pipelines, boolean variables commonly come through as "True"/"False" (capitalized), so PR builds may not actually enable the skip behavior. Consider normalizing (e.g. case-insensitive) and/or accepting "1" to make the gate reliable.

Suggested change
const skipCjsChecks = process.env.SKIP_CJS_CHECKS === "true";
const skipCjsChecks =
typeof process.env.SKIP_CJS_CHECKS === "string" &&
(process.env.SKIP_CJS_CHECKS.toLowerCase() === "true" || process.env.SKIP_CJS_CHECKS === "1");

Copilot uses AI. Check for mistakes.
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.

Not an issue — the pipeline variable uses ${{ lower(eq(variables['Build.Reason'], 'PullRequest')) }} in include-vars.yml, which outputs lowercase "true" / "false". The === "true" check matches correctly.

Comment on lines +137 to +141
"check:exports": skipCjsChecks
? {
dependsOn: [
"check:exports:bundle-release-tags",
"check:exports:esm:public",
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

When skipCjsChecks is enabled, check:exports becomes a non-script task that only depends on a hard-coded list of check:exports:esm:* subtasks. This skips packages whose check:exports script isn’t implemented via those subtasks (e.g. @fluid-internal/devtools-view runs api-extractor directly and has no check:exports:esm:* scripts), reducing PR validation unintentionally. Consider preserving per-package check:exports execution while making the CJS subtasks no-ops (or otherwise filtered) so nonstandard packages still get checked.

Copilot uses AI. Check for mistakes.
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.

Correct that check:exports is fully disabled, not just the CJS subtasks. This is intentional — there's no clean way to skip only CJS subtasks because:

  1. 88 packages use concurrently "npm:check:exports:*" which glob-matches both CJS and ESM subtasks. Concurrently runs via npm, not fluid-build, so fluid-build task overrides can't selectively suppress the CJS subtask invocations.
  2. Fluid-build's script field is boolean (true/false) — no custom command override to substitute e.g. concurrently "npm:check:exports:esm:*".
  3. Only 2 packages use direct api-extractor instead of concurrently (devtools-view, example-utils), and both have typeValidation: { disabled: true }.

Full check:exports validation (CJS + ESM) runs on main/release CI. The PR-only skip is a deliberate tradeoff: ~2 min wall-clock savings vs deferred ESM export lint to merge time.

CJS api-extractor output is byte-for-byte identical to ESM. On PR
builds, skip CJS api-extractor invocations (~22 min CPU, ~2 min
wall-clock) and defer CJS-dependent checks to main/release CI.

When SKIP_CJS_CHECKS=true (set for PR builds via pipeline variable):
- api-extractor:commonjs removed from build:test, build:test:cjs, api
- check:exports becomes a no-op (concurrently script would fail
  without CJS entry points)
- check:are-the-types-wrong becomes a no-op (attw checks CJS+ESM)
- AreTheTypesWrong pipeline job excluded from PR runs

Non-PR builds are unaffected — full CJS+ESM checks run as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@frankmueller-msft frankmueller-msft force-pushed the ci/skip-cjs-api-extractor-on-prs branch from 815dde0 to dd025bd Compare March 2, 2026 23:24
@CraigMacomber
Copy link
Copy Markdown
Contributor

Rather than disabling low value checks that usually don't catch things, I think we should simply move checks which are not required for build out of the build job and into a checks job. That way we can keep all the checks without slowing down CI (as long as the validation is faster than the slowest test job, which it will be). The validation you are skipping is only a small part of what could be moved to a separate job (all API extractor tasks, linting, formatting, webpacking (I think) and bundle size testing could all be moved out of the build job, likely producing a much larger win that you measured for this change, without reducing our validation.

Something like the changes in https://github.com/microsoft/FluidFramework/pull/25321/files, but paired with creating a separate validation job would do it.

We could start by just moving all the API extractor and are the types wrong checks (so a superset of what was disabled here) if linting is problematic to move (I think we might have some mistakes where build depends on linting since --nolint clean builds fail).

Comment on lines +95 to +97
"build:test": skipCjsChecks
? ["typetests:gen", "tsc", "api-extractor:esnext"]
: ["typetests:gen", "tsc", "api-extractor:commonjs", "api-extractor:esnext"],
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.

Avoid duplication of the portions which are the same, as it could introduce surprising issues in the future if they diverge.

Suggested change
"build:test": skipCjsChecks
? ["typetests:gen", "tsc", "api-extractor:esnext"]
: ["typetests:gen", "tsc", "api-extractor:commonjs", "api-extractor:esnext"],
"build:test": ["typetests:gen", "tsc", "api-extractor:esnext", ...(skipCjsChecks ? [] : ["api-extractor:commonjs"])]

This same pattern should be applied to the cases below as well.

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.

Done — applied the spread pattern to all three conditional arrays (build:test, build:test:cjs, api).

// therefore we need to require both before running api-extractor.
"check:release-tags": ["tsc", "build:esnext"],
"check:are-the-types-wrong": ["tsc", "build:esnext", "api"],
// attw packs the package and checks CJS+ESM type resolution. With CJS entry
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.

we do not use api-extractor to generate our entry points (we have our own custom tools for that).

If disabling API extractor is breaking attw, then I think you might have disabled more than intended.

Also, if disabling attw is intended, then we should document that in the PR description as that is rather significant.

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.

You're right that api-extractor doesn't generate entry points. The issue is that attw packs the package and validates CJS+ESM type resolution. When api-extractor:commonjs is skipped, the CJS .d.ts rollup files that attw expects to find are missing — so it fails. The entry points themselves (generated by custom tools) are still there, but the type rollups that api-extractor produces for CJS are not.

That said, your broader point stands — disabling attw is a significant omission that should be called out in the PR description. I'll update it.

},
"depcruise": [],
"check:exports": ["api"],
// When skipping CJS checks, skip check:exports entirely. The package's concurrently
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.

This makes this PR do a lot more disabling that it claims in its description. It seems like you encountered some cases where our build does a poor job with its dependencies and trying to build subsets of what's normally build doesn't work. Its issues like this which is why my previous attempt to speed up our build have failed. I think we really need to correct the dependencies in the build so that its possible to add a minimal build script and just run that without breaking random stuff.

Anyway, I don't think we can consider this PR to be just disabling CJS checks if we disable all checks as a workaround for our build having bad dependencies and being unable to just disable cjs stuff.

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.

Fair point. The PR description says "skip CJS api-extractor checks" but the actual scope is broader — check:exports and check:are-the-types-wrong are also fully disabled as a cascade effect, because they can't run without the CJS type rollups that api-extractor produces.

This is a real limitation. The build dependency graph doesn't let us cleanly remove just the CJS api-extractor step without breaking downstream checks that assume its output exists.

I'll update the PR description to accurately reflect the full scope of what's being skipped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@frankmueller-msft
Copy link
Copy Markdown
Contributor Author

@CraigMacomber Agreed that moving validation checks to a separate parallel job is the better long-term approach — it preserves full validation without adding to the critical path. The approach in #25321 (separating build from lint/checks) would be a superset of what this PR does and would avoid the cascade problem where skipping CJS api-extractor forces us to also skip check:exports and attw.

I'll update the PR description to accurately reflect the full scope of what's being disabled (not just CJS api-extractor, but also check:exports and attw as cascade effects). Whether this PR is worth landing as a short-term win while the job-splitting work happens, or whether we should just go straight to the parallel job approach, is up to you.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@frankmueller-msft frankmueller-msft requested a review from a team March 3, 2026 22:30
Comment on lines +224 to +225
- ${{ if ne(variables['Build.Reason'], 'PullRequest') }}:
- { name: "ci:check:are-the-types-wrong", jobName: "AreTheTypesWrong" }
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.

This should always be checked. You can craft an alternate check for each package that ignores CJS.
But we should gate main on ESM being correct.
We just had a PR today that needs a correction caught by these checks.

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.

Fair point — we shouldn't be dropping ESM validation on PRs, especially since it catches real issues. The right approach is CraigMacomber's suggestion: move these checks to a separate parallel validation job rather than disabling them. I'll close this PR in favor of that approach.

Comment on lines +148 to +153
"check:exports": skipCjsChecks
? {
dependsOn: [],
script: false,
}
: ["api"],
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.

This is really not correct. You might be able to alter api some of the time, but I can't think of a time when check:exports should depend on nothing.

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.

Agreed — disabling check:exports entirely was a workaround for the build dependency graph not allowing us to cleanly skip just the CJS subtasks. It's too blunt. The better path is separating validation into a parallel job so nothing gets dropped.

@frankmueller-msft
Copy link
Copy Markdown
Contributor Author

Closing in favor of the parallel validation job approach suggested by @CraigMacomber (ref #25321). The current approach disables too much — check:exports and attw catch real issues and shouldn't be dropped on PRs.

frankmueller-msft added a commit to frankmueller-msft/FluidFramework that referenced this pull request Mar 4, 2026
Remove lint from ci:build's serial dependency chain and run it as a
parallel taskTest job in the build-client pipeline. The local `build`
task still includes lint. This follows the approach from PR microsoft#25321 and
the discussion in PR microsoft#26596.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

ci: skip redundant CJS api-extractor lint checks in ci:build

4 participants