From 195482c8e2811a84cb003845a64992c69b64237b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 27 Aug 2024 12:37:03 +0200 Subject: [PATCH] ci: Stabilize CI dependency cache key (#13401) Ensure it only changes if actual dependencies change. Previously, we would invalidate the dependency cache every time a package.json of the workspace changed in any way. This is defensive, but it also means that we also invalidate if one of these things happen: 1. A script or similar is added/edited for workspace package 2. A release is made, bumping internal dependency versions This change updates this to calculate the hash with a slightly more sophisticated approach, which should hopefully ensure we only actually bust the cache when a dependency _actually_ changes. This should lead to the dependency cache being re-used much more, because only rarely is an actual dependency changed. --------- Co-authored-by: Charly Gomez --- .../actions/install-dependencies/action.yml | 4 +- scripts/dependency-hash-key.js | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 scripts/dependency-hash-key.js diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 8cb80ac7440e..5fbf87c67d79 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -9,11 +9,9 @@ outputs: runs: using: "composite" steps: - # we use a hash of yarn.lock as our cache key, because if it hasn't changed, our dependencies haven't changed, - # so no need to reinstall them - name: Compute dependency cache key id: compute_lockfile_hash - run: echo "hash=dependencies-${{ hashFiles('yarn.lock', 'packages/*/package.json', 'dev-packages/*/package.json') }}" >> "$GITHUB_OUTPUT" + run: node ./scripts/dependency-hash-key.js >> "$GITHUB_OUTPUT" shell: bash - name: Check dependency cache diff --git a/scripts/dependency-hash-key.js b/scripts/dependency-hash-key.js new file mode 100644 index 000000000000..55e38e4a385e --- /dev/null +++ b/scripts/dependency-hash-key.js @@ -0,0 +1,73 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +/** + * Build a cache key for the dependencies of the monorepo. + * In addition to the content of the yarn.lock file, we also include + * dependencies of all workspace packages in the cache key. + * This ensures that we get a consistent cache key even if a dependency change does not affect + * the yarn.lock file. + */ +function outputDependencyCacheKey() { + const lockfileContent = fs.readFileSync(path.join(process.cwd(), 'yarn.lock'), 'utf8'); + + const hashParts = [lockfileContent]; + + const packageJson = require(path.join(process.cwd(), 'package.json')); + + const workspacePackages = packageJson.workspaces || []; + + // Get the package name (e.g. @sentry/browser) of all workspace packages + // we want to ignore their version numbers later + const workspacePackageNames = getWorkspacePackageNames(workspacePackages); + + // Add the dependencies of the workspace itself + hashParts.push(getNormalizedDependencies(packageJson, workspacePackageNames)); + + // Now for each workspace package, add the dependencies + workspacePackages.forEach(workspace => { + const packageJsonPath = path.join(process.cwd(), workspace, 'package.json'); + const packageJson = require(packageJsonPath); + hashParts.push(getNormalizedDependencies(packageJson, workspacePackageNames)); + }); + + const hash = crypto.createHash('md5').update(hashParts.join('\n')).digest('hex'); + // We log the output in a way that the GitHub Actions can append it to the output + // We prefix it with `dependencies-` so it is easier to identify in the logs + // eslint-disable-next-line no-console + console.log(`hash=dependencies-${hash}`); +} + +function getNormalizedDependencies(packageJson, workspacePackageNames) { + const { dependencies, devDependencies } = packageJson; + + const mergedDependencies = { + ...devDependencies, + ...dependencies, + }; + + const normalizedDependencies = {}; + + // Sort the keys to ensure a consistent order + Object.keys(mergedDependencies) + .sort() + .forEach(key => { + // If the dependency is a workspace package, ignore the version + // No need to invalidate a cache after every release + const version = workspacePackageNames.includes(key) ? '**workspace**' : mergedDependencies[key]; + normalizedDependencies[key] = version; + }); + + return JSON.stringify(normalizedDependencies); +} + +function getWorkspacePackageNames(workspacePackages) { + return workspacePackages.map(workspace => { + const packageJsonPath = path.join(process.cwd(), workspace, 'package.json'); + const packageJson = require(packageJsonPath); + return packageJson.name; + }); +} + +outputDependencyCacheKey();