Skip to content

Commit

Permalink
ci: Stabilize CI dependency cache key (#13401)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
mydea and chargome authored Aug 27, 2024
1 parent 6cb6999 commit 195482c
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 3 deletions.
4 changes: 1 addition & 3 deletions .github/actions/install-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions scripts/dependency-hash-key.js
Original file line number Diff line number Diff line change
@@ -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();

0 comments on commit 195482c

Please sign in to comment.