Skip to content

Enable incremental build caching in CI pipeline#26593

Closed
frankmueller-msft wants to merge 10 commits intomicrosoft:mainfrom
frankmueller-msft:ci/tsbuildinfo-caching
Closed

Enable incremental build caching in CI pipeline#26593
frankmueller-msft wants to merge 10 commits intomicrosoft:mainfrom
frankmueller-msft:ci/tsbuildinfo-caching

Conversation

@frankmueller-msft
Copy link
Contributor

Summary

Cache .tsbuildinfo and .done.build.log files across CI runs to enable TypeScript and fluid-build incremental compilation. First run on a branch builds from scratch and saves the cache; subsequent runs restore the cache and only rebuild what changed.

Changes:

  • Add Cache@2 task before the build step with commit-based cache key and OS-level restore key
  • Extract cached .tsbuildinfo + .done.build.log files before ci:build; save updated files after
  • Enable hash-mode env vars (FLUID_BUILD_ENABLE_*_HASH) for fluid-build tasks that default to mtime-based done file checks (copyfiles, type-validation, good-fences, depcruise)

Why this is safe:

  • TypeScript validates .tsbuildinfo files using content hashes, not file mtimes — stale entries cause a rebuild, never silent corruption
  • fluid-build's TscTask and TscDependentTask (api-extractor, eslint) also use content hashes in their .done.build.log files
  • continueOnError: true on all cache steps — cache failures fall back to a clean build
  • Cache key includes $(Build.SourceVersion) so re-runs get exact hits; new commits restore from the OS-level prefix key and TypeScript incrementally rebuilds

Expected impact:

  • First run (cold cache): no change (~8m build)
  • Subsequent runs (warm cache): ~30-50% build time reduction (~4-5m build)
  • Cache size: ~8 MB gzipped

Files changed

File Change
tools/pipelines/templates/build-npm-client-package.yml Add Cache@2 + extract/save scripts
tools/pipelines/templates/include-build-lint.yml Add hash-mode env vars to build step

Test plan

  • First CI run completes successfully (cold cache, saves cache artifact)
  • Second CI run (re-run or new commit) restores cache and build time decreases
  • Cache extraction failure does not block the build (continueOnError)
  • All existing tests continue to pass

🤖 Generated with Claude Code

Cache .tsbuildinfo and .done.build.log files across CI runs using ADO
Cache@2. TypeScript and fluid-build validate cached files using content
hashes (not mtimes), so stale caches are safe — tasks simply re-run
when their inputs change.

- Add Cache@2 task before build step with commit-based key + OS restore key
- Restore: extract cached tar archive to build directory before ci:build
- Save: collect all incremental files into tar archive after build
- Enable hash-mode env vars (FLUID_BUILD_ENABLE_*_HASH) for tasks that
  default to mtime-based done file checks

Expected impact: ~30-50% build time reduction on cache hits (8m → 4-5m),
with zero risk on cache misses (falls back to full build).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 28, 2026 18:00
Copy link
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

Enables incremental build caching in the CI pipeline by persisting TypeScript .tsbuildinfo and fluid-build .done.build.log files between runs, aiming to reduce rebuild time on subsequent CI executions.

Changes:

  • Add Azure Pipelines Cache@2 restore step and Bash extract step before the build, and a Bash save step after the build to persist incremental artifacts.
  • Set FLUID_BUILD_ENABLE_*_HASH environment variables in the build step to use content-hash based donefile checks for specific fluid-build tasks.

Reviewed changes

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

File Description
tools/pipelines/templates/include-build-lint.yml Adds env vars to enable hash-based incremental checks for selected fluid-build tasks during the build step.
tools/pipelines/templates/build-npm-client-package.yml Adds restore/extract/save steps around the build to cache .tsbuildinfo and .done.build.log files across CI runs.

continueOnError: true
timeoutInMinutes: 5
inputs:
key: 'tsc-cache | "$(Agent.OS)" | $(Build.SourceVersion)'
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

The cache key is effectively unique per commit due to $(Build.SourceVersion), which will create a new cache entry for every commit (and still restore from the broad OS restoreKey). Over time this can cause cache churn/evictions and unnecessary uploads. Consider keying off stable inputs instead (e.g., include ${{ parameters.buildDirectory }} plus a hashed file like pnpm-lock.yaml / package-lock.json and optionally other build config files), so the cache only rolls when relevant inputs change while still enabling incremental rebuilds across commits.

Suggested change
key: 'tsc-cache | "$(Agent.OS)" | $(Build.SourceVersion)'
key: 'tsc-cache | "$(Agent.OS)" | $(Pipeline.Workspace)/${{ parameters.buildDirectory }}/package.json'

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +40
# Enable content-hash mode for fluid-build tasks that default to mtime-based
# done file checks. This is required for incremental build caching across CI runs,
# since git checkout resets file mtimes but content hashes remain stable.
FLUID_BUILD_ENABLE_COPYFILES_HASH: 1
FLUID_BUILD_ENABLE_TYPEVALIDATION_HASH: 1
FLUID_BUILD_ENABLE_GOODFENCE_HASH: 1
FLUID_BUILD_ENABLE_DEPCRUISE_HASH: 1
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

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

These hash-mode env vars are only set on the build (ci:build) step. Since the pipeline also runs npm run lint (which in this repo typically invokes fluid-build --task lint), lint-time tasks like good-fences/depcruise/type-validation may still fall back to mtime-based done files and won’t benefit from the restored cache. Consider setting the same FLUID_BUILD_ENABLE_*_HASH variables on the lint step as well for consistent incremental behavior.

Copilot uses AI. Check for mistakes.
frankmueller-msft and others added 9 commits February 28, 2026 10:49
Pipelines that use include-build-lint with taskLint enabled will now
also use content-hash mode for fluid-build done file checks during
the lint step, consistent with the build step.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Cache@2 task requires the cache path directory to exist at step
execution time to register the post-job save. Without this, the
post-job silently skips the upload.

Also exclude api-extractor-*.done.build.log from the cache — these
files account for ~115 MB but regenerate quickly from their
tsbuildinfo dependencies, reducing cache size from 124 MB to ~9 MB.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previous attempts used a path outside the build directory and a
string-based cache key. Match the pnpm cache pattern exactly:
- Use file-based key (pnpm-lock.yaml hash) instead of $(Build.SourceVersion)
- Place cache directory inside the build directory tree
- Add cacheHitVar for diagnostic output
- Increase timeout to 10 minutes for cache upload

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cache@2 requires an existing cache entry before it will save new ones,
making it impossible to bootstrap from PR builds. Switch to explicit
DownloadPipelineArtifact/PublishPipelineArtifact which gives us full
control over the save/restore lifecycle.

Restore: downloads tsc-cache artifact from the most recent successful
build on main. Gracefully skips if no previous build has the artifact.

Save: publishes the tsc-cache artifact via 1ES template outputs, so
every successful build makes its cache available to future builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Temporarily use $(Build.SourceBranch) to test the warm cache path
using the artifact published by the previous PR build. Will revert
to refs/heads/main before merging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The warm cache test (build #381098) failed with 641 TypeScript errors
because stale .done.build.log files prevented non-tsc tasks (like
typetests:gen) from re-running after "Set Package Version" bumped
package versions.

TscTask (the main build bottleneck) uses .tsbuildinfo directly — it
does NOT use done files. So excluding done files from the cache
preserves 100% of the TypeScript incremental compilation benefit while
eliminating stale done file issues.

Also reverts runBranch back to refs/heads/main for production use.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Temporarily use $(Build.SourceBranch) to download the tsc-cache
artifact from build #381109 (which contains only .tsbuildinfo files,
no .done.build.log files). This tests whether the tsbuildinfo-only
fix resolves the warm cache failure seen in build #381098.

Will revert to refs/heads/main before merging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Warm cache test confirmed that tsbuildinfo-only caching doesn't work
for CI: fluid-build's TscTask checks source hashes against tsbuildinfo
and skips tsc when they match, but the build outputs (lib/, dist/)
don't exist on fresh CI agents, causing downstream TS2307 errors.

The fundamental issue is that TscTask.checkLeafIsUpToDate() validates
source file hashes but never checks output file existence.

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

Closing this PR — the tsbuildinfo caching approach doesn't work for CI builds.

Root cause: fluid-build's TscTask.checkLeafIsUpToDate() only validates source file hashes against the tsbuildinfo. When all hashes match, it skips tsc entirely — but on a fresh CI agent the build outputs (lib/, dist/, .d.ts) don't exist, causing downstream TS2307: Cannot find module errors.

What we tried:

  1. Cache@2 — can't bootstrap new cache types (post-job save requires an existing restore key hit)
  2. Pipeline artifacts with tsbuildinfo + done files — stale done files caused TS2344 type validation errors after version bumping
  3. Pipeline artifacts with tsbuildinfo only — fluid-build skips tsc for "up to date" packages whose outputs don't exist

Why full build output caching won't help either: "Set Package Version" modifies packageVersion.ts in most packages, invalidating the cached tsbuildinfo for those packages anyway.

Build speed improvements are better pursued via:

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