-
Notifications
You must be signed in to change notification settings - Fork 400
Support non-lock files for C# cache key computation #3117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…o a function Changing `paths` to be a function is necessary to allow `getTemporaryDirectory` to be called
There was a problem hiding this 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 enhances C# dependency caching support by allowing non-lock files to be used when computing cache keys. Currently, only packages.lock.json
and paket.lock
files are used for cache key computation, which limits caching benefits to projects that have these lock files checked in. The new implementation maintains backward compatibility while expanding support to more C# projects through an optional feature flag.
Key changes:
- Adds a new
CsharpNewCacheKey
feature flag with environment variable support - Refactors cache configuration from static data structures to dynamic functions
- Implements fallback logic to use
.csproj
,packages.config
, andnuget.config
files when lock files are unavailable
Reviewed Changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
File | Description |
---|---|
src/feature-flags.ts | Adds the new CsharpNewCacheKey feature flag configuration |
src/dependency-caching.ts | Core refactoring to support dynamic hash pattern generation and C# fallback logic |
src/init-action.ts | Updates function calls to pass CodeQL and features parameters |
src/analyze-action.ts | Updates function calls to pass CodeQL and features parameters |
lib/* | Generated JavaScript files reflecting the TypeScript changes |
return cacheConfig.getHashPatterns(codeql, features); | ||
} catch (err) { | ||
if (err instanceof NoMatchingFilesError) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The checkHashPatterns
function catches NoMatchingFilesError
but the error message logged doesn't distinguish between the original error condition (no lock files found) and the fallback condition (feature enabled but no fallback files found). Consider providing more specific error messages to help users understand which files are missing.
Copilot uses AI. Check for mistakes.
if (await features.getValue(Feature.CsharpNewCacheKey, codeql)) { | ||
// These are less accurate for use in cache key calculations, because they: | ||
// | ||
// - Don't contain the exact versions used. They may only contain version ranges or none at all. | ||
// - They contain information unrelated to dependencies, which we don't care about. | ||
// | ||
// As a result, the hash we compute from these files may change, even if | ||
// the dependencies haven't changed. | ||
return makePatternCheck([ | ||
"**/*.csproj", | ||
"**/packages.config", | ||
"**/nuget.config", | ||
]); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The comment explains the limitations of using non-lock files but doesn't mention the cache key compatibility concern raised in the PR description. Consider adding a comment about cache invalidation when this feature is enabled/disabled to ensure the behavior is documented in the code.
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks reasonable from an Action perspective
// If we get to this point, the `basePatterns` didn't find any files, | ||
// and `Feature.CsharpNewCacheKey` is either not enabled or we didn't | ||
// find any files using those patterns either. | ||
throw new NoMatchingFilesError(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have all the context here, but perhaps it would be clearer to return undefined
and have this path included in the types?
// As a result, the hash we compute from these files may change, even if | ||
// the dependencies haven't changed. | ||
return makePatternCheck([ | ||
"**/*.csproj", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hvitved : Can you think of other files that are relevant to include i cache-key computations?
It is probably worth noting that we might risk using "bad" caches when version (or wildcard) ranges are used (in case the available version(s) of a dependency changes in a NuGet feed). Is it correctly understood that the only impact of this is that the "right" package version is just not available in the cache (and will be downloaded)?
Related question: For how long are caches stored?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps **/global.json
and **/*.sln
?
Actually, as far as I can tell, our dependency fetching logic will look in all small non-binary files for lines like <PackageReference Include="..." />
, and then try to restore those dependencies. Obviously, we cannot meaningfully include all files in the hash, so perhaps we will just have to ignore this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, there is a balance here between capturing all changes to dependencies and the ideal case, where the hash only changes if there are dependencies added or removed, or the exact versions that are in use change. If we hashed all "small non-binary" files, for example, we'd probably be uploading a new cache for every run. The main problem with that is how much of the cache quota we'd end up using.
The main goal here is to produce dependency caches for more C# projects in (primarily) Default Setup, where it's not possible to add custom caching steps.
We could port the logic to check all "small non-binary" files for lines that look like they include dependency information into the Action to determine which files to hash for this, but I wouldn't be too keen on that duplication and it's probably beyond the scope of this change. For more advanced logic like that, it would probably make sense to revisit the parts of the original EDR for this that talk about having the CLI communicate the caching configuration to the Action.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with all of the above, I just wanted to point out this corner case in the dependency fetching logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that was helpful! Thank you ❤️
I only wanted to add the extra context for everyone's benefit.
For **/global.json
and **/*.sln
, do you know off the top of your head how common it is for them to contain dependency information?
For global.json
files, I have only seen them used to indicate the .NET version - which we wouldn't want to cache (that should ideally be handled by the toolcache) and which I don't think affects the dependencies that are downloaded? Although, I imagine it would be possible to conditionally include dependencies based on the .NET version in use.
For *.sln
files, I have only seen those include other project files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are right; let's skip global.json
files and *.sln
files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks plausible to me!
This PR modifies our implementation of dependency caching for C# to allow non-lock files to be used when computing a hash for the cache key.
The hash that's included in the cache key is derived from files which contain information about the project's dependency. The idea being that the hash changes only if the dependencies change. Currently, we only use
packages.lock.json
andpaket.lock
files for this. These files contain exact dependency information (including the exact versions that were resolved by the respective package manager). The resulting hashes we compute for the cache keys should therefore only change if the dependencies (including versions) change. This is the desired behaviour.However, relatively few C# projects use lock files and fewer yet have them checked-in to the repository. This means that most C# projects do not benefit from our implementation of dependency caching at the moment.
This PR modifies the behaviour of our dependency caching implementation for C# as follows:
packages.lock.json
orpaket.lock
files. If they exist, we use them to compute a hash for the cache key.CsharpNewCacheKey
feature is enabled (via FF or environment variable), we search for additional files that typically contain dependency information for C# projects. If they exist, we use them to compute the hash for the cache key.Using the additional files is less precise, because they may not contain exact version information (only version ranges or no versions at all) and they contain information unrelated to dependencies. This means that the hash we compute can change, even if the dependencies don't change. However, dependency caching will benefit more C# projects.
This PR is best reviewed commit-by-commit. The first few commits just add the new
Feature
and refactor the code a bit to facilitate the changes.Questions for reviewers:
Feature
wouldn't avoid restoring caches that were produced with theFeature
enabled. We likely want to do something similar as for Add feature flag to roll out JAR minimization in the Java extractor #3107 that marks caches produced with thisFeature
on.setup-dotnet
Action considers more paths and computes them dynamically. Now that we havegetDependencyPaths
as a function, it would be easy to do this.Risk assessment
For internal use only. Please select the risk level of this change:
Merge / deployment checklist