Skip to content

Commit f1bdedd

Browse files
[rush-lib] Hash full shrinkwrap entry to detect sub-dependency resolution changes (#5468)
* Initial plan * fix: hash full shrinkwrap entry to detect sub-dependency resolution changes The logic in _getIntegrityForPackage and _addDependencyRecursive was using resolution.integrity when available, which does not capture changes to sub-dependency resolutions. This change ensures the entire shrinkwrap entry is hashed, including the dependencies field which reflects resolved versions. Co-authored-by: dmichon-msft <[email protected]> * Add rush change file for @microsoft/rush-lib Co-authored-by: dmichon-msft <[email protected]> * Move change file from @microsoft/rush-lib to @microsoft/rush Co-authored-by: dmichon-msft <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: dmichon-msft <[email protected]>
1 parent 3ae648c commit f1bdedd

File tree

5 files changed

+141
-51
lines changed

5 files changed

+141
-51
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "Hash full shrinkwrap entry to detect sub-dependency resolution changes",
5+
"type": "minor",
6+
"packageName": "@microsoft/rush"
7+
}
8+
],
9+
"packageName": "@microsoft/rush",
10+
"email": "[email protected]"
11+
}

libraries/rush-lib/src/logic/pnpm/PnpmProjectShrinkwrapFile.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -143,21 +143,16 @@ export class PnpmProjectShrinkwrapFile extends BaseProjectShrinkwrapFile<PnpmShr
143143
return;
144144
}
145145

146-
let integrity: string | undefined = shrinkwrapEntry.resolution?.integrity;
147-
if (!integrity) {
148-
// git dependency specifiers do not have an integrity entry. Instead, they specify the tarball field.
149-
// So instead, we will hash the contents of the dependency entry and use that as the integrity hash.
150-
// Ex:
151-
// github.com/chfritz/node-xmlrpc/948db2fbd0260e5d56ed5ba58df0f5b6599bbe38:
152-
// ...
153-
// resolution:
154-
// tarball: 'https://codeload.github.com/chfritz/node-xmlrpc/tar.gz/948db2fbd0260e5d56ed5ba58df0f5b6599bbe38'
155-
const sha256Digest: string = crypto
156-
.createHash('sha256')
157-
.update(JSON.stringify(shrinkwrapEntry))
158-
.digest('hex');
159-
integrity = `${name}@${version}:${sha256Digest}:`;
160-
}
146+
// Hash the full shrinkwrap entry instead of using just resolution.integrity.
147+
// This ensures that changes to sub-dependency resolutions are detected.
148+
// For example, if package A depends on [email protected] and [email protected]'s resolution of C changes
149+
// from [email protected] to [email protected], the hash of A's shrinkwrap entry will change because
150+
// the dependencies field in the entry reflects the resolved versions.
151+
const sha256Digest: string = crypto
152+
.createHash('sha256')
153+
.update(JSON.stringify(shrinkwrapEntry))
154+
.digest('hex');
155+
const integrity: string = `${name}@${version}:${sha256Digest}:`;
161156

162157
// Add the current dependency
163158
projectShrinkwrapMap.set(specifier, integrity);

libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,21 +1198,16 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
11981198
return integrityMap;
11991199
}
12001200

1201-
let selfIntegrity: string | undefined = shrinkwrapEntry.resolution?.integrity;
1202-
if (!selfIntegrity) {
1203-
// git dependency specifiers do not have an integrity entry. Instead, they specify the tarball field.
1204-
// So instead, we will hash the contents of the dependency entry and use that as the integrity hash.
1205-
// Ex:
1206-
// github.com/chfritz/node-xmlrpc/948db2fbd0260e5d56ed5ba58df0f5b6599bbe38:
1207-
// ...
1208-
// resolution:
1209-
// tarball: 'https://codeload.github.com/chfritz/node-xmlrpc/tar.gz/948db2fbd0260e5d56ed5ba58df0f5b6599bbe38'
1210-
const sha256Digest: string = crypto
1211-
.createHash('sha256')
1212-
.update(JSON.stringify(shrinkwrapEntry))
1213-
.digest('base64');
1214-
selfIntegrity = `${specifier}:${sha256Digest}:`;
1215-
}
1201+
// Hash the full shrinkwrap entry instead of using just resolution.integrity.
1202+
// This ensures that changes to sub-dependency resolutions are detected.
1203+
// For example, if package A depends on [email protected] and [email protected]'s resolution of C changes
1204+
// from [email protected] to [email protected], the hash of A's shrinkwrap entry will change because
1205+
// the dependencies field in the entry reflects the resolved versions.
1206+
const sha256Digest: string = crypto
1207+
.createHash('sha256')
1208+
.update(JSON.stringify(shrinkwrapEntry))
1209+
.digest('base64');
1210+
const selfIntegrity: string = `${specifier}:${sha256Digest}:`;
12161211

12171212
integrityMap.set(specifier, selfIntegrity);
12181213
const { dependencies, optionalDependencies } = shrinkwrapEntry;

libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,95 @@ describe(PnpmShrinkwrapFile.name, () => {
219219
});
220220
});
221221

222+
describe('getIntegrityForImporter', () => {
223+
it('produces different hashes when sub-dependency resolutions change', () => {
224+
// This test verifies that changes to sub-dependency resolutions are detected.
225+
// The issue is that if package A depends on B, and B's resolution of C changes
226+
// (e.g., from [email protected] to [email protected]), the integrity hash for A should change.
227+
// This is important for build orchestrators that rely on shrinkwrap-deps.json
228+
// to detect changes to resolution and invalidate caches appropriately.
229+
230+
// Two shrinkwrap files with the same package but different sub-dependency resolutions
231+
const shrinkwrapContent1: string = `
232+
lockfileVersion: '9.0'
233+
settings:
234+
autoInstallPeers: true
235+
excludeLinksFromLockfile: false
236+
importers:
237+
.:
238+
dependencies:
239+
foo:
240+
specifier: ~1.0.0
241+
version: 1.0.0
242+
packages:
243+
244+
resolution:
245+
integrity: sha512-abc123==
246+
dependencies:
247+
bar: 1.3.0
248+
249+
resolution:
250+
integrity: sha512-bar130==
251+
snapshots:
252+
253+
dependencies:
254+
bar: 1.3.0
255+
256+
`;
257+
258+
const shrinkwrapContent2: string = `
259+
lockfileVersion: '9.0'
260+
settings:
261+
autoInstallPeers: true
262+
excludeLinksFromLockfile: false
263+
importers:
264+
.:
265+
dependencies:
266+
foo:
267+
specifier: ~1.0.0
268+
version: 1.0.0
269+
packages:
270+
271+
resolution:
272+
integrity: sha512-abc123==
273+
dependencies:
274+
bar: 1.2.0
275+
276+
resolution:
277+
integrity: sha512-bar120==
278+
snapshots:
279+
280+
dependencies:
281+
bar: 1.2.0
282+
283+
`;
284+
285+
const shrinkwrapFile1 = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent1);
286+
const shrinkwrapFile2 = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent2);
287+
288+
// Clear cache to ensure fresh computation
289+
PnpmShrinkwrapFile.clearCache();
290+
291+
const integrityMap1 = shrinkwrapFile1.getIntegrityForImporter('.');
292+
const integrityMap2 = shrinkwrapFile2.getIntegrityForImporter('.');
293+
294+
// Both should have integrity maps
295+
expect(integrityMap1).toBeDefined();
296+
expect(integrityMap2).toBeDefined();
297+
298+
// The integrity for '[email protected]' should be different because bar's resolution changed
299+
const fooIntegrity1 = integrityMap1!.get('[email protected]');
300+
const fooIntegrity2 = integrityMap2!.get('[email protected]');
301+
302+
expect(fooIntegrity1).toBeDefined();
303+
expect(fooIntegrity2).toBeDefined();
304+
305+
// This is the key assertion: the integrity hashes should be different
306+
// because the sub-dependency (bar) resolved to different versions
307+
expect(fooIntegrity1).not.toEqual(fooIntegrity2);
308+
});
309+
});
310+
222311
describe('Check is workspace project modified', () => {
223312
describe('pnpm lockfile major version 5', () => {
224313
it('can detect not modified', async () => {

libraries/rush-lib/src/logic/test/__snapshots__/ShrinkwrapFile.test.ts.snap

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ Array [
55
Array [
66
Object {
77
"../../project1": "../../project1:D5ar2j+w6/zH15/eOoF37Nkdbamt2tX47iijyj7LVXk=:",
8-
"/jquery/1.12.3": "sha512-FzM42/Ew+Hb8ha2OlhHRBLgWIZS32gZ0+NvWTf+ZvVvGaIlJkOiXQyb7VBjv4L6fJfmTrRf3EsAmbfsHDhfemw==",
9-
"/pad-left/1.0.2": "sha512-saxSV1EYAytuZDtQYEwi0DPzooG6aN18xyHrnJtzwjVwmMauzkEecd7hynVJGolNGk1Pl9tltmZqfze4TZTCxg==",
10-
"/repeat-string/1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
8+
"/jquery/1.12.3": "/jquery/1.12.3:Y74h7210GWDRLFidqe7W0rGD9dEVjPuIpExxLG3ql7U=:",
9+
"/pad-left/1.0.2": "/pad-left/1.0.2:fNuxq+VtdNt2R9HJ6ip7x62AjQvQK7tiTHVLF6JGjpE=:",
10+
"/repeat-string/1.6.1": "/repeat-string/1.6.1:FSrgyzed38htiD39oWRz9lAr9wD9AxuRmBW5tC28Jvc=:",
1111
},
1212
"project1/.rush/temp/shrinkwrap-deps.json",
1313
Object {
@@ -22,8 +22,8 @@ Array [
2222
Array [
2323
Object {
2424
"../../project2": "../../project2:PQ2FvyHHwmt/FIUaiJBVAfpHv6hj9EGJrAw69+0IY50=:",
25-
"/jquery/2.2.4": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==",
26-
"/q/1.5.1": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
25+
"/jquery/2.2.4": "/jquery/2.2.4:PIfhtCRWsOQOCNHXI0s+2Ssbxc/U0IZ1ZXD+YcrHwi4=:",
26+
"/q/1.5.1": "/q/1.5.1:A6WReS0f6nc67cF0NQHN3YGoUP6sLNfcWK/7orfz1J4=:",
2727
},
2828
"project2/.rush/temp/shrinkwrap-deps.json",
2929
Object {
@@ -38,8 +38,8 @@ Array [
3838
Array [
3939
Object {
4040
"../../project3": "../../project3:jomsZKvXG32qqYOfW3HUBGfWWSw6ybFV1WDf9c/kiP4=:",
41-
"/q/1.5.1": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
42-
"/repeat-string/1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
41+
"/q/1.5.1": "/q/1.5.1:A6WReS0f6nc67cF0NQHN3YGoUP6sLNfcWK/7orfz1J4=:",
42+
"/repeat-string/1.6.1": "/repeat-string/1.6.1:FSrgyzed38htiD39oWRz9lAr9wD9AxuRmBW5tC28Jvc=:",
4343
"example.pkgs.visualstudio.com/@scope/testDep/2.1.0": "example.pkgs.visualstudio.com/@scope/testDep/2.1.0:i5jbUOp/IxUgiN0dHYsTRzmimOVBwu7f9908rRaC9VY=:",
4444
},
4545
"project3/.rush/temp/shrinkwrap-deps.json",
@@ -55,9 +55,9 @@ Array [
5555
Array [
5656
Object {
5757
"../../project1": "../../project1:D5ar2j+w6/zH15/eOoF37Nkdbamt2tX47iijyj7LVXk=:",
58-
"/jquery/1.12.3": "sha512-FzM42/Ew+Hb8ha2OlhHRBLgWIZS32gZ0+NvWTf+ZvVvGaIlJkOiXQyb7VBjv4L6fJfmTrRf3EsAmbfsHDhfemw==",
59-
"/pad-left/1.0.2": "sha512-saxSV1EYAytuZDtQYEwi0DPzooG6aN18xyHrnJtzwjVwmMauzkEecd7hynVJGolNGk1Pl9tltmZqfze4TZTCxg==",
60-
"/repeat-string/1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
58+
"/jquery/1.12.3": "/jquery/1.12.3:Y74h7210GWDRLFidqe7W0rGD9dEVjPuIpExxLG3ql7U=:",
59+
"/pad-left/1.0.2": "/pad-left/1.0.2:fNuxq+VtdNt2R9HJ6ip7x62AjQvQK7tiTHVLF6JGjpE=:",
60+
"/repeat-string/1.6.1": "/repeat-string/1.6.1:FSrgyzed38htiD39oWRz9lAr9wD9AxuRmBW5tC28Jvc=:",
6161
},
6262
"project1/.rush/temp/shrinkwrap-deps.json",
6363
Object {
@@ -72,8 +72,8 @@ Array [
7272
Array [
7373
Object {
7474
"../../project2": "../../project2:PQ2FvyHHwmt/FIUaiJBVAfpHv6hj9EGJrAw69+0IY50=:",
75-
"/jquery/2.2.4": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==",
76-
"/q/1.5.1": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
75+
"/jquery/2.2.4": "/jquery/2.2.4:PIfhtCRWsOQOCNHXI0s+2Ssbxc/U0IZ1ZXD+YcrHwi4=:",
76+
"/q/1.5.1": "/q/1.5.1:A6WReS0f6nc67cF0NQHN3YGoUP6sLNfcWK/7orfz1J4=:",
7777
},
7878
"project2/.rush/temp/shrinkwrap-deps.json",
7979
Object {
@@ -88,8 +88,8 @@ Array [
8888
Array [
8989
Object {
9090
"../../project3": "../../project3:jomsZKvXG32qqYOfW3HUBGfWWSw6ybFV1WDf9c/kiP4=:",
91-
"/q/1.5.1": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
92-
"/repeat-string/1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
91+
"/q/1.5.1": "/q/1.5.1:A6WReS0f6nc67cF0NQHN3YGoUP6sLNfcWK/7orfz1J4=:",
92+
"/repeat-string/1.6.1": "/repeat-string/1.6.1:FSrgyzed38htiD39oWRz9lAr9wD9AxuRmBW5tC28Jvc=:",
9393
"example.pkgs.visualstudio.com/@scope/testDep/2.1.0": "example.pkgs.visualstudio.com/@scope/testDep/2.1.0:i5jbUOp/IxUgiN0dHYsTRzmimOVBwu7f9908rRaC9VY=:",
9494
},
9595
"project3/.rush/temp/shrinkwrap-deps.json",
@@ -105,9 +105,9 @@ Array [
105105
Array [
106106
Object {
107107
"../../project1": "../../project1:6yFTI2g+Ny0Au80xpo6zIY61TCNDUuLUd6EgLlbOBtc=:",
108-
"[email protected]": "sha512-FzM42/Ew+Hb8ha2OlhHRBLgWIZS32gZ0+NvWTf+ZvVvGaIlJkOiXQyb7VBjv4L6fJfmTrRf3EsAmbfsHDhfemw==",
109-
"[email protected]": "sha512-saxSV1EYAytuZDtQYEwi0DPzooG6aN18xyHrnJtzwjVwmMauzkEecd7hynVJGolNGk1Pl9tltmZqfze4TZTCxg==",
110-
"[email protected]": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
108+
"[email protected]": "[email protected]:nkmD9jsJt8eUeR2cOltYXX5TFnlK30nBsVeOz047iPo=:",
109+
"[email protected]": "[email protected]:R80aACjIFOqWnDG/5JgPO4SM4jLta89Xjp5el1RQm+g=:",
110+
"[email protected]": "[email protected]:YqQsoCDmP4kj4raEmb5SYE4GsoFGxpBoRbOA/U9rqB4=:",
111111
},
112112
"project1/.rush/temp/shrinkwrap-deps.json",
113113
Object {
@@ -122,8 +122,8 @@ Array [
122122
Array [
123123
Object {
124124
"../../project2": "../../project2:l6v/HWUhScMI0m4k6D5qHiCOFj3Z0GoIFJEcp4I63w0=:",
125-
"[email protected]": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==",
126-
"[email protected]": "sha512-VVMcd+HnuWZalHPycK7CsbVJ+sSrrrnCvHcW38YJVK9Tywnb5DUWJjONi81bLUj7aqDjIXnePxBl5t1r/F/ncg==",
125+
"[email protected]": "[email protected]:e3VqitHw5v+hfYoCAwnNmSwfWqvOCOLGdwIKR1fzqhM=:",
126+
"[email protected]": "[email protected]:lSldncUZjX1nP6wk6WAWwPXA6wLli1dIBnuA3SPeIE4=:",
127127
},
128128
"project2/.rush/temp/shrinkwrap-deps.json",
129129
Object {
@@ -139,8 +139,8 @@ Array [
139139
Object {
140140
"../../project3": "../../project3:vMoje8cXfsHYOc6EXbxEw/qyBGXGUL1RApmNfwl7oA8=:",
141141
"pad-left@https://github.com/jonschlinkert/pad-left/tarball/2.1.0": "pad-left@https://github.com/jonschlinkert/pad-left/tarball/2.1.0:bKrL+SvVYubL0HwTq/GOOXq1d05LTQ+HGqlXabzGEAU=:",
142-
"[email protected]": "sha512-VVMcd+HnuWZalHPycK7CsbVJ+sSrrrnCvHcW38YJVK9Tywnb5DUWJjONi81bLUj7aqDjIXnePxBl5t1r/F/ncg==",
143-
"[email protected]": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
142+
"[email protected]": "[email protected]:lSldncUZjX1nP6wk6WAWwPXA6wLli1dIBnuA3SPeIE4=:",
143+
"[email protected]": "[email protected]:YqQsoCDmP4kj4raEmb5SYE4GsoFGxpBoRbOA/U9rqB4=:",
144144
},
145145
"project3/.rush/temp/shrinkwrap-deps.json",
146146
Object {

0 commit comments

Comments
 (0)