Enable incremental build caching in CI pipeline#26593
Enable incremental build caching in CI pipeline#26593frankmueller-msft wants to merge 10 commits intomicrosoft:mainfrom
Conversation
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>
There was a problem hiding this comment.
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@2restore step and Bash extract step before the build, and a Bash save step after the build to persist incremental artifacts. - Set
FLUID_BUILD_ENABLE_*_HASHenvironment 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)' |
There was a problem hiding this comment.
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.
| key: 'tsc-cache | "$(Agent.OS)" | $(Build.SourceVersion)' | |
| key: 'tsc-cache | "$(Agent.OS)" | $(Pipeline.Workspace)/${{ parameters.buildDirectory }}/package.json' |
| # 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 |
There was a problem hiding this comment.
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.
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>
|
Closing this PR — the tsbuildinfo caching approach doesn't work for CI builds. Root cause: fluid-build's What we tried:
Why full build output caching won't help either: "Set Package Version" modifies Build speed improvements are better pursued via:
|
Summary
Cache
.tsbuildinfoand.done.build.logfiles 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:
Cache@2task before the build step with commit-based cache key and OS-level restore key.tsbuildinfo+.done.build.logfiles beforeci:build; save updated files afterFLUID_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:
.tsbuildinfofiles using content hashes, not file mtimes — stale entries cause a rebuild, never silent corruptionTscTaskandTscDependentTask(api-extractor, eslint) also use content hashes in their.done.build.logfilescontinueOnError: trueon all cache steps — cache failures fall back to a clean build$(Build.SourceVersion)so re-runs get exact hits; new commits restore from the OS-level prefix key and TypeScript incrementally rebuildsExpected impact:
Files changed
tools/pipelines/templates/build-npm-client-package.ymltools/pipelines/templates/include-build-lint.ymlTest plan
🤖 Generated with Claude Code