Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Hash full shrinkwrap entry to detect sub-dependency resolution changes",
"type": "minor",
"packageName": "@microsoft/rush"
}
],
"packageName": "@microsoft/rush",
"email": "198982749+Copilot@users.noreply.github.com"
}
25 changes: 10 additions & 15 deletions libraries/rush-lib/src/logic/pnpm/PnpmProjectShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,21 +143,16 @@ export class PnpmProjectShrinkwrapFile extends BaseProjectShrinkwrapFile<PnpmShr
return;
}

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

// Add the current dependency
projectShrinkwrapMap.set(specifier, integrity);
Expand Down
25 changes: 10 additions & 15 deletions libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1198,21 +1198,16 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile {
return integrityMap;
}

let selfIntegrity: string | undefined = shrinkwrapEntry.resolution?.integrity;
if (!selfIntegrity) {
// git dependency specifiers do not have an integrity entry. Instead, they specify the tarball field.
// So instead, we will hash the contents of the dependency entry and use that as the integrity hash.
// Ex:
// github.com/chfritz/node-xmlrpc/948db2fbd0260e5d56ed5ba58df0f5b6599bbe38:
// ...
// resolution:
// tarball: 'https://codeload.github.com/chfritz/node-xmlrpc/tar.gz/948db2fbd0260e5d56ed5ba58df0f5b6599bbe38'
const sha256Digest: string = crypto
.createHash('sha256')
.update(JSON.stringify(shrinkwrapEntry))
.digest('base64');
selfIntegrity = `${specifier}:${sha256Digest}:`;
}
// Hash the full shrinkwrap entry instead of using just resolution.integrity.
// This ensures that changes to sub-dependency resolutions are detected.
// For example, if package A depends on B@1.0 and B@1.0's resolution of C changes
// from C@1.3 to C@1.2, the hash of A's shrinkwrap entry will change because
// the dependencies field in the entry reflects the resolved versions.
const sha256Digest: string = crypto
.createHash('sha256')
.update(JSON.stringify(shrinkwrapEntry))
.digest('base64');
const selfIntegrity: string = `${specifier}:${sha256Digest}:`;

integrityMap.set(specifier, selfIntegrity);
const { dependencies, optionalDependencies } = shrinkwrapEntry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,95 @@ describe(PnpmShrinkwrapFile.name, () => {
});
});

describe('getIntegrityForImporter', () => {
it('produces different hashes when sub-dependency resolutions change', () => {
// This test verifies that changes to sub-dependency resolutions are detected.
// The issue is that if package A depends on B, and B's resolution of C changes
// (e.g., from C@1.3 to C@1.2), the integrity hash for A should change.
// This is important for build orchestrators that rely on shrinkwrap-deps.json
// to detect changes to resolution and invalidate caches appropriately.

// Two shrinkwrap files with the same package but different sub-dependency resolutions
const shrinkwrapContent1: string = `
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
foo:
specifier: ~1.0.0
version: 1.0.0
packages:
foo@1.0.0:
resolution:
integrity: sha512-abc123==
dependencies:
bar: 1.3.0
bar@1.3.0:
resolution:
integrity: sha512-bar130==
snapshots:
foo@1.0.0:
dependencies:
bar: 1.3.0
bar@1.3.0: {}
`;

const shrinkwrapContent2: string = `
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
foo:
specifier: ~1.0.0
version: 1.0.0
packages:
foo@1.0.0:
resolution:
integrity: sha512-abc123==
dependencies:
bar: 1.2.0
bar@1.2.0:
resolution:
integrity: sha512-bar120==
snapshots:
foo@1.0.0:
dependencies:
bar: 1.2.0
bar@1.2.0: {}
`;

const shrinkwrapFile1 = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent1);
const shrinkwrapFile2 = PnpmShrinkwrapFile.loadFromString(shrinkwrapContent2);

// Clear cache to ensure fresh computation
PnpmShrinkwrapFile.clearCache();

const integrityMap1 = shrinkwrapFile1.getIntegrityForImporter('.');
const integrityMap2 = shrinkwrapFile2.getIntegrityForImporter('.');

// Both should have integrity maps
expect(integrityMap1).toBeDefined();
expect(integrityMap2).toBeDefined();

// The integrity for 'foo@1.0.0' should be different because bar's resolution changed
const fooIntegrity1 = integrityMap1!.get('foo@1.0.0');
const fooIntegrity2 = integrityMap2!.get('foo@1.0.0');

expect(fooIntegrity1).toBeDefined();
expect(fooIntegrity2).toBeDefined();

// This is the key assertion: the integrity hashes should be different
// because the sub-dependency (bar) resolved to different versions
expect(fooIntegrity1).not.toEqual(fooIntegrity2);
});
});

describe('Check is workspace project modified', () => {
describe('pnpm lockfile major version 5', () => {
it('can detect not modified', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Array [
Array [
Object {
"../../project1": "../../project1:D5ar2j+w6/zH15/eOoF37Nkdbamt2tX47iijyj7LVXk=:",
"/jquery/1.12.3": "sha512-FzM42/Ew+Hb8ha2OlhHRBLgWIZS32gZ0+NvWTf+ZvVvGaIlJkOiXQyb7VBjv4L6fJfmTrRf3EsAmbfsHDhfemw==",
"/pad-left/1.0.2": "sha512-saxSV1EYAytuZDtQYEwi0DPzooG6aN18xyHrnJtzwjVwmMauzkEecd7hynVJGolNGk1Pl9tltmZqfze4TZTCxg==",
"/repeat-string/1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
"/jquery/1.12.3": "/jquery/1.12.3:Y74h7210GWDRLFidqe7W0rGD9dEVjPuIpExxLG3ql7U=:",
"/pad-left/1.0.2": "/pad-left/1.0.2:fNuxq+VtdNt2R9HJ6ip7x62AjQvQK7tiTHVLF6JGjpE=:",
"/repeat-string/1.6.1": "/repeat-string/1.6.1:FSrgyzed38htiD39oWRz9lAr9wD9AxuRmBW5tC28Jvc=:",
},
"project1/.rush/temp/shrinkwrap-deps.json",
Object {
Expand All @@ -22,8 +22,8 @@ Array [
Array [
Object {
"../../project2": "../../project2:PQ2FvyHHwmt/FIUaiJBVAfpHv6hj9EGJrAw69+0IY50=:",
"/jquery/2.2.4": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==",
"/q/1.5.1": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
"/jquery/2.2.4": "/jquery/2.2.4:PIfhtCRWsOQOCNHXI0s+2Ssbxc/U0IZ1ZXD+YcrHwi4=:",
"/q/1.5.1": "/q/1.5.1:A6WReS0f6nc67cF0NQHN3YGoUP6sLNfcWK/7orfz1J4=:",
},
"project2/.rush/temp/shrinkwrap-deps.json",
Object {
Expand All @@ -38,8 +38,8 @@ Array [
Array [
Object {
"../../project3": "../../project3:jomsZKvXG32qqYOfW3HUBGfWWSw6ybFV1WDf9c/kiP4=:",
"/q/1.5.1": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
"/repeat-string/1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
"/q/1.5.1": "/q/1.5.1:A6WReS0f6nc67cF0NQHN3YGoUP6sLNfcWK/7orfz1J4=:",
"/repeat-string/1.6.1": "/repeat-string/1.6.1:FSrgyzed38htiD39oWRz9lAr9wD9AxuRmBW5tC28Jvc=:",
"example.pkgs.visualstudio.com/@scope/testDep/2.1.0": "example.pkgs.visualstudio.com/@scope/testDep/2.1.0:i5jbUOp/IxUgiN0dHYsTRzmimOVBwu7f9908rRaC9VY=:",
},
"project3/.rush/temp/shrinkwrap-deps.json",
Expand All @@ -55,9 +55,9 @@ Array [
Array [
Object {
"../../project1": "../../project1:D5ar2j+w6/zH15/eOoF37Nkdbamt2tX47iijyj7LVXk=:",
"/jquery/1.12.3": "sha512-FzM42/Ew+Hb8ha2OlhHRBLgWIZS32gZ0+NvWTf+ZvVvGaIlJkOiXQyb7VBjv4L6fJfmTrRf3EsAmbfsHDhfemw==",
"/pad-left/1.0.2": "sha512-saxSV1EYAytuZDtQYEwi0DPzooG6aN18xyHrnJtzwjVwmMauzkEecd7hynVJGolNGk1Pl9tltmZqfze4TZTCxg==",
"/repeat-string/1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
"/jquery/1.12.3": "/jquery/1.12.3:Y74h7210GWDRLFidqe7W0rGD9dEVjPuIpExxLG3ql7U=:",
"/pad-left/1.0.2": "/pad-left/1.0.2:fNuxq+VtdNt2R9HJ6ip7x62AjQvQK7tiTHVLF6JGjpE=:",
"/repeat-string/1.6.1": "/repeat-string/1.6.1:FSrgyzed38htiD39oWRz9lAr9wD9AxuRmBW5tC28Jvc=:",
},
"project1/.rush/temp/shrinkwrap-deps.json",
Object {
Expand All @@ -72,8 +72,8 @@ Array [
Array [
Object {
"../../project2": "../../project2:PQ2FvyHHwmt/FIUaiJBVAfpHv6hj9EGJrAw69+0IY50=:",
"/jquery/2.2.4": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==",
"/q/1.5.1": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
"/jquery/2.2.4": "/jquery/2.2.4:PIfhtCRWsOQOCNHXI0s+2Ssbxc/U0IZ1ZXD+YcrHwi4=:",
"/q/1.5.1": "/q/1.5.1:A6WReS0f6nc67cF0NQHN3YGoUP6sLNfcWK/7orfz1J4=:",
},
"project2/.rush/temp/shrinkwrap-deps.json",
Object {
Expand All @@ -88,8 +88,8 @@ Array [
Array [
Object {
"../../project3": "../../project3:jomsZKvXG32qqYOfW3HUBGfWWSw6ybFV1WDf9c/kiP4=:",
"/q/1.5.1": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
"/repeat-string/1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
"/q/1.5.1": "/q/1.5.1:A6WReS0f6nc67cF0NQHN3YGoUP6sLNfcWK/7orfz1J4=:",
"/repeat-string/1.6.1": "/repeat-string/1.6.1:FSrgyzed38htiD39oWRz9lAr9wD9AxuRmBW5tC28Jvc=:",
"example.pkgs.visualstudio.com/@scope/testDep/2.1.0": "example.pkgs.visualstudio.com/@scope/testDep/2.1.0:i5jbUOp/IxUgiN0dHYsTRzmimOVBwu7f9908rRaC9VY=:",
},
"project3/.rush/temp/shrinkwrap-deps.json",
Expand All @@ -105,9 +105,9 @@ Array [
Array [
Object {
"../../project1": "../../project1:6yFTI2g+Ny0Au80xpo6zIY61TCNDUuLUd6EgLlbOBtc=:",
"jquery@1.12.3": "sha512-FzM42/Ew+Hb8ha2OlhHRBLgWIZS32gZ0+NvWTf+ZvVvGaIlJkOiXQyb7VBjv4L6fJfmTrRf3EsAmbfsHDhfemw==",
"pad-left@1.0.2": "sha512-saxSV1EYAytuZDtQYEwi0DPzooG6aN18xyHrnJtzwjVwmMauzkEecd7hynVJGolNGk1Pl9tltmZqfze4TZTCxg==",
"repeat-string@1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
"jquery@1.12.3": "jquery@1.12.3:nkmD9jsJt8eUeR2cOltYXX5TFnlK30nBsVeOz047iPo=:",
"pad-left@1.0.2": "pad-left@1.0.2:R80aACjIFOqWnDG/5JgPO4SM4jLta89Xjp5el1RQm+g=:",
"repeat-string@1.6.1": "repeat-string@1.6.1:YqQsoCDmP4kj4raEmb5SYE4GsoFGxpBoRbOA/U9rqB4=:",
},
"project1/.rush/temp/shrinkwrap-deps.json",
Object {
Expand All @@ -122,8 +122,8 @@ Array [
Array [
Object {
"../../project2": "../../project2:l6v/HWUhScMI0m4k6D5qHiCOFj3Z0GoIFJEcp4I63w0=:",
"jquery@2.2.4": "sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==",
"q@1.5.0": "sha512-VVMcd+HnuWZalHPycK7CsbVJ+sSrrrnCvHcW38YJVK9Tywnb5DUWJjONi81bLUj7aqDjIXnePxBl5t1r/F/ncg==",
"jquery@2.2.4": "jquery@2.2.4:e3VqitHw5v+hfYoCAwnNmSwfWqvOCOLGdwIKR1fzqhM=:",
"q@1.5.0": "q@1.5.0:lSldncUZjX1nP6wk6WAWwPXA6wLli1dIBnuA3SPeIE4=:",
},
"project2/.rush/temp/shrinkwrap-deps.json",
Object {
Expand All @@ -139,8 +139,8 @@ Array [
Object {
"../../project3": "../../project3:vMoje8cXfsHYOc6EXbxEw/qyBGXGUL1RApmNfwl7oA8=:",
"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=:",
"q@1.5.0": "sha512-VVMcd+HnuWZalHPycK7CsbVJ+sSrrrnCvHcW38YJVK9Tywnb5DUWJjONi81bLUj7aqDjIXnePxBl5t1r/F/ncg==",
"repeat-string@1.6.1": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==",
"q@1.5.0": "q@1.5.0:lSldncUZjX1nP6wk6WAWwPXA6wLli1dIBnuA3SPeIE4=:",
"repeat-string@1.6.1": "repeat-string@1.6.1:YqQsoCDmP4kj4raEmb5SYE4GsoFGxpBoRbOA/U9rqB4=:",
},
"project3/.rush/temp/shrinkwrap-deps.json",
Object {
Expand Down
Loading