-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix: include lock files in dependency hash for cache invalidation #8818
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: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,27 @@ | ||
| """Utility Class for Getting Function or Layer Manifest Dependency Hashes""" | ||
|
|
||
| import hashlib | ||
| import pathlib | ||
| from typing import Any, Optional | ||
|
|
||
| from samcli.lib.build.workflow_config import get_workflow_config | ||
| from samcli.lib.utils.hash import file_checksum | ||
|
|
||
|
|
||
| # Mapping of dependency managers to their lock file names | ||
| LOCK_FILE_MAPPING = { | ||
| "npm": "package-lock.json", | ||
| "npm-esbuild": "package-lock.json", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we not need to add yarn.lock and pnpm-lock.yaml? |
||
| "bundler": "Gemfile.lock", | ||
| "gradle": "gradle.lockfile", | ||
| "cli-package": "packages.lock.json", | ||
| "modules": "go.sum", | ||
| "cargo": "Cargo.lock", | ||
| "uv": "uv.lock", | ||
| "poetry": "poetry.lock", | ||
| } | ||
|
|
||
|
|
||
| # TODO Expand this class to hash specific sections of the manifest | ||
| class DependencyHashGenerator: | ||
| _code_uri: str | ||
|
|
@@ -50,15 +65,17 @@ def __init__( | |
| self._hash = None | ||
|
|
||
| def _calculate_dependency_hash(self) -> Optional[str]: | ||
| """Calculate the manifest file hash | ||
| """Calculate the manifest file hash, including lock file if applicable | ||
|
|
||
| Returns | ||
| ------- | ||
| Optional[str] | ||
| Returns manifest hash. If manifest does not exist or not supported, None will be returned. | ||
| Returns combined hash of manifest and lock file (if present). | ||
| If manifest does not exist or not supported, None will be returned. | ||
| """ | ||
| if self._manifest_path_override: | ||
| manifest_file = self._manifest_path_override | ||
| config = None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why are we not wanting to add the config to the hash if there is a different manifest path? Can the lock file not still be resolved relative to the override path's directory? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [BUG] Setting There are two further consequences of this design:
Consider resolving the workflow config (or at least the dependency manager) even when an override is present, e.g.: if self._manifest_path_override:
manifest_file = self._manifest_path_override
else:
manifest_file = None
config = get_workflow_config(self._runtime, self._code_dir, self._base_dir)
if manifest_file is None:
manifest_file = config.manifest_nameso the lock file is honored in every case where a dependency manager is known. |
||
| else: | ||
| config = get_workflow_config(self._runtime, self._code_dir, self._base_dir) | ||
| manifest_file = config.manifest_name | ||
|
|
@@ -70,7 +87,24 @@ def _calculate_dependency_hash(self) -> Optional[str]: | |
| if not manifest_path.is_file(): | ||
| return None | ||
|
|
||
| return file_checksum(str(manifest_path), hash_generator=self._hash_generator) | ||
| manifest_hash = file_checksum(str(manifest_path), hash_generator=self._hash_generator) | ||
|
|
||
| # Check if there's a lock file for this dependency manager | ||
| if config and config.dependency_manager in LOCK_FILE_MAPPING: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [GENERAL] @seshubaws explicitly asked for unit tests covering "if there is a lock file present and not present, and if there is an unknown dep manager." The diff does not modify Given the number of code paths introduced (lock file present, lock file absent, dep manager not in
|
||
| lock_file_name = LOCK_FILE_MAPPING[config.dependency_manager] | ||
| lock_file_path = pathlib.Path(self._code_dir, lock_file_name).resolve() | ||
|
|
||
| # If lock file exists, combine hashes | ||
| if lock_file_path.is_file(): | ||
| lock_file_hash = file_checksum(str(lock_file_path), hash_generator=self._hash_generator) | ||
|
|
||
| # Combine both hashes into a single hash | ||
| combined = f"{manifest_hash}:{lock_file_hash}" | ||
| hasher = self._hash_generator() if self._hash_generator else hashlib.md5() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [BUG]
Hash objects returned by There is a related latent bug on lines 90/99: Suggested fix — always construct fresh hashers locally (and use the codebase's manifest_hash = file_checksum(str(manifest_path), hash_generator=self._hash_generator)
if config and config.dependency_manager in LOCK_FILE_MAPPING:
lock_file_path = pathlib.Path(self._code_dir, LOCK_FILE_MAPPING[config.dependency_manager]).resolve()
if lock_file_path.is_file():
lock_file_hash = file_checksum(str(lock_file_path)) # fresh hasher
hasher = hashlib.md5(usedforsecurity=False)
hasher.update(f"{manifest_hash}:{lock_file_hash}".encode("utf-8"))
return hasher.hexdigest() |
||
| hasher.update(combined.encode("utf-8")) | ||
| return hasher.hexdigest() | ||
|
|
||
| return manifest_hash | ||
|
|
||
| @property | ||
| def hash(self) -> Optional[str]: | ||
|
|
||
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.
[BUG]
LOCK_FILE_MAPPINGonly mapsnpm/npm-esbuildtopackage-lock.json, but SAM classifies every Node.js project underdependency_manager="npm"regardless of whether the user actually uses npm, yarn, or pnpm (there is no separate yarn/pnpm workflow insamcli/lib/build/workflows.py). A project with ayarn.lockorpnpm-lock.yamland nopackage-lock.jsonwill not get cache invalidation when the lock file changes — exactly the bug this PR is trying to fix. @seshubaws raised this and it has not been addressed.Since only one lock file name is stored per manager, consider either supporting a list of candidate lock files (checking each and hashing whichever exists) or falling back through the known Node lock file names. For example:
and hash whichever candidate exists.