From e33196f7422957bea03ed53f6fbb155025ffc7b8 Mon Sep 17 00:00:00 2001
From: Sergey Dolin <dsame@github.com>
Date: Tue, 27 Jun 2023 13:07:43 +0200
Subject: [PATCH] Do not ivalidate the cache entirely on lock file change
 (#744)

* Do not ivalidate the cache entirely on yarn3 lock file change

* Use cache prefix if all sub-projects are yarn managed

* Rename functions & add e2e tests
---
 .github/workflows/e2e-cache.yml               | 84 ++++++++++++++++-
 __tests__/cache-utils.test.ts                 |  5 +-
 ...rojects.sh => prepare-yarn-subprojects.sh} | 19 +++-
 dist/cache-save/index.js                      | 71 +++++++++++++-
 dist/setup/index.js                           | 84 ++++++++++++++++-
 src/cache-restore.ts                          | 19 +++-
 src/cache-utils.ts                            | 92 ++++++++++++++++++-
 7 files changed, 358 insertions(+), 16 deletions(-)
 rename __tests__/{prepare-subprojects.sh => prepare-yarn-subprojects.sh} (64%)

diff --git a/.github/workflows/e2e-cache.yml b/.github/workflows/e2e-cache.yml
index 901ed87c..c8324ee1 100644
--- a/.github/workflows/e2e-cache.yml
+++ b/.github/workflows/e2e-cache.yml
@@ -146,7 +146,7 @@ jobs:
       - uses: actions/checkout@v3
 
       - name: prepare sub-projects
-        run: __tests__/prepare-subprojects.sh
+        run: __tests__/prepare-yarn-subprojects.sh yarn1
 
       # expect
       #  - no errors
@@ -161,3 +161,85 @@ jobs:
           cache-dependency-path: |
             **/*.lock
             yarn.lock
+
+  yarn-subprojects-berry-local:
+    name: Test yarn subprojects all locally managed
+    strategy:
+      matrix:
+        node-version: [12, 14, 16]
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: prepare sub-projects
+        run: __tests__/prepare-yarn-subprojects.sh
+
+      # expect
+      #  - no errors
+      #  - log
+      #    ##[info]All dependencies are managed locally by yarn3, the previous cache can be used
+      #    ##[debug]["node-cache-Linux-yarn-401024703386272f1a950c9f014cbb1bb79a7a5b6e1fb00e8b90d06734af41ee","node-cache-Linux-yarn"]
+      - name: Setup Node
+        uses: ./
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: 'yarn'
+          cache-dependency-path: |
+            sub2/*.lock
+            sub3/*.lock
+
+  yarn-subprojects-berry-global:
+    name: Test yarn subprojects some locally managed
+    strategy:
+      matrix:
+        node-version: [12, 14, 16]
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: prepare sub-projects
+        run: __tests__/prepare-yarn-subprojects.sh global
+
+      # expect
+      #  - no errors
+      #  - log must
+      #    ##[debug]"/home/runner/work/setup-node-test/setup-node-test/sub2" dependencies are managed by yarn 3 locally
+      #    ##[debug]"/home/runner/work/setup-node-test/setup-node-test/sub3" dependencies are not managed by yarn 3 locally
+      - name: Setup Node
+        uses: ./
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: 'yarn'
+          cache-dependency-path: |
+            sub2/*.lock
+            sub3/*.lock
+
+  yarn-subprojects-berry-git:
+    name: Test yarn subprojects managed by git
+    strategy:
+      matrix:
+        node-version: [12, 14, 16]
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: prepare sub-projects
+        run: /bin/bash __tests__/prepare-yarn-subprojects.sh keepcache
+
+      # expect
+      #  - no errors
+      #  - log
+      #    [debug]"/home/runner/work/setup-node-test/setup-node-test/sub2" has .yarn/cache - dependencies are kept in the repository
+      #    [debug]"/home/runner/work/setup-node-test/setup-node-test/sub3" has .yarn/cache - dependencies are kept in the repository
+      #    [debug]["node-cache-Linux-yarn-401024703386272f1a950c9f014cbb1bb79a7a5b6e1fb00e8b90d06734af41ee"]
+      - name: Setup Node
+        uses: ./
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: 'yarn'
+          cache-dependency-path: |
+            sub2/*.lock
+            sub3/*.lock
diff --git a/__tests__/cache-utils.test.ts b/__tests__/cache-utils.test.ts
index 56f46425..39f376f2 100644
--- a/__tests__/cache-utils.test.ts
+++ b/__tests__/cache-utils.test.ts
@@ -6,7 +6,8 @@ import {
   PackageManagerInfo,
   isCacheFeatureAvailable,
   supportedPackageManagers,
-  getCommandOutput
+  getCommandOutput,
+  resetProjectDirectoriesMemoized
 } from '../src/cache-utils';
 import fs from 'fs';
 import * as cacheUtils from '../src/cache-utils';
@@ -103,6 +104,8 @@ describe('cache-utils', () => {
         (pattern: string): Promise<Globber> =>
           MockGlobber.create(['/foo', '/bar'])
       );
+
+      resetProjectDirectoriesMemoized();
     });
 
     afterEach(() => {
diff --git a/__tests__/prepare-subprojects.sh b/__tests__/prepare-yarn-subprojects.sh
similarity index 64%
rename from __tests__/prepare-subprojects.sh
rename to __tests__/prepare-yarn-subprojects.sh
index 447cc531..30d894cf 100755
--- a/__tests__/prepare-subprojects.sh
+++ b/__tests__/prepare-yarn-subprojects.sh
@@ -32,10 +32,20 @@ cat <<EOT >package.json
 EOT
 yarn set version 3.5.1
 yarn install
+if [ x$1 = 'xglobal' ];then
+  echo enableGlobalCache
+  echo 'enableGlobalCache: true' >> .yarnrc.yml
+fi
 
-echo "create yarn1 project in the root"
 cd ..
-cat <<EOT >package.json
+if [ x$1 != 'xkeepcache' -a x$2 != 'xkeepcache' ]; then
+  rm -rf sub2/.yarn/cache
+  rm -rf sub3/.yarn/cache
+fi
+
+if [ x$1 = 'xyarn1' ];then
+  echo "create yarn1 project in the root"
+  cat <<EOT >package.json
 {
   "name": "subproject",
   "dependencies": {
@@ -44,5 +54,6 @@ cat <<EOT >package.json
   }
 }
 EOT
-yarn set version 1.22.19
-yarn install
\ No newline at end of file
+  yarn set version 1.22.19
+  yarn install
+fi
\ No newline at end of file
diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js
index 3a8d0cee..a3235195 100644
--- a/dist/cache-save/index.js
+++ b/dist/cache-save/index.js
@@ -60434,7 +60434,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
     return (mod && mod.__esModule) ? mod : { "default": mod };
 };
 Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.isCacheFeatureAvailable = exports.isGhes = exports.getCacheDirectories = exports.getPackageManagerInfo = exports.getCommandOutputNotEmpty = exports.getCommandOutput = exports.supportedPackageManagers = void 0;
+exports.isCacheFeatureAvailable = exports.isGhes = exports.repoHasYarnBerryManagedDependencies = exports.getCacheDirectories = exports.resetProjectDirectoriesMemoized = exports.getPackageManagerInfo = exports.getCommandOutputNotEmpty = exports.getCommandOutput = exports.supportedPackageManagers = void 0;
 const core = __importStar(__nccwpck_require__(2186));
 const exec = __importStar(__nccwpck_require__(1514));
 const cache = __importStar(__nccwpck_require__(7799));
@@ -60503,6 +60503,19 @@ const getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, void
     }
 });
 exports.getPackageManagerInfo = getPackageManagerInfo;
+/**
+ * getProjectDirectoriesFromCacheDependencyPath is called twice during `restoreCache`
+ *  - first through `getCacheDirectories`
+ *  - second from `repoHasYarn3ManagedCache`
+ *
+ *  it contains expensive IO operation and thus should be memoized
+ */
+let projectDirectoriesMemoized = null;
+/**
+ * unit test must reset memoized variables
+ */
+const resetProjectDirectoriesMemoized = () => (projectDirectoriesMemoized = null);
+exports.resetProjectDirectoriesMemoized = resetProjectDirectoriesMemoized;
 /**
  * Expands (converts) the string input `cache-dependency-path` to list of directories that
  * may be project roots
@@ -60511,6 +60524,9 @@ exports.getPackageManagerInfo = getPackageManagerInfo;
  * @return list of directories and possible
  */
 const getProjectDirectoriesFromCacheDependencyPath = (cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
+    if (projectDirectoriesMemoized !== null) {
+        return projectDirectoriesMemoized;
+    }
     const globber = yield glob.create(cacheDependencyPath);
     const cacheDependenciesPaths = yield globber.glob();
     const existingDirectories = cacheDependenciesPaths
@@ -60519,6 +60535,7 @@ const getProjectDirectoriesFromCacheDependencyPath = (cacheDependencyPath) => __
         .filter(directory => fs_1.default.lstatSync(directory).isDirectory());
     if (!existingDirectories.length)
         core.warning(`No existing directories found containing cache-dependency-path="${cacheDependencyPath}"`);
+    projectDirectoriesMemoized = existingDirectories;
     return existingDirectories;
 });
 /**
@@ -60531,7 +60548,7 @@ const getProjectDirectoriesFromCacheDependencyPath = (cacheDependencyPath) => __
 const getCacheDirectoriesFromCacheDependencyPath = (packageManagerInfo, cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
     const projectDirectories = yield getProjectDirectoriesFromCacheDependencyPath(cacheDependencyPath);
     const cacheFoldersPaths = yield Promise.all(projectDirectories.map((projectDirectory) => __awaiter(void 0, void 0, void 0, function* () {
-        const cacheFolderPath = packageManagerInfo.getCacheFolderPath(projectDirectory);
+        const cacheFolderPath = yield packageManagerInfo.getCacheFolderPath(projectDirectory);
         core.debug(`${packageManagerInfo.name}'s cache folder "${cacheFolderPath}" configured for the directory "${projectDirectory}"`);
         return cacheFolderPath;
     })));
@@ -60565,6 +60582,56 @@ const getCacheDirectories = (packageManagerInfo, cacheDependencyPath) => __await
     return getCacheDirectoriesForRootProject(packageManagerInfo);
 });
 exports.getCacheDirectories = getCacheDirectories;
+/**
+ * A function to check if the directory is a yarn project configured to manage
+ * obsolete dependencies in the local cache
+ * @param directory - a path to the folder
+ * @return - true if the directory's project is yarn managed
+ *  - if there's .yarn/cache folder do not mess with the dependencies kept in the repo, return false
+ *  - global cache is not managed by yarn @see https://yarnpkg.com/features/offline-cache, return false
+ *  - if local cache is not explicitly enabled (not yarn3), return false
+ *  - return true otherwise
+ */
+const projectHasYarnBerryManagedDependencies = (directory) => __awaiter(void 0, void 0, void 0, function* () {
+    const workDir = directory || process.env.GITHUB_WORKSPACE || '.';
+    core.debug(`check if "${workDir}" has locally managed yarn3 dependencies`);
+    // if .yarn/cache directory exists the cache is managed by version control system
+    const yarnCacheFile = path_1.default.join(workDir, '.yarn', 'cache');
+    if (fs_1.default.existsSync(yarnCacheFile) &&
+        fs_1.default.lstatSync(yarnCacheFile).isDirectory()) {
+        core.debug(`"${workDir}" has .yarn/cache - dependencies are kept in the repository`);
+        return Promise.resolve(false);
+    }
+    // NOTE: yarn1 returns 'undefined' with return code = 0
+    const enableGlobalCache = yield exports.getCommandOutput('yarn config get enableGlobalCache', workDir);
+    // only local cache is not managed by yarn
+    const managed = enableGlobalCache.includes('false');
+    if (managed) {
+        core.debug(`"${workDir}" dependencies are managed by yarn 3 locally`);
+        return true;
+    }
+    else {
+        core.debug(`"${workDir}" dependencies are not managed by yarn 3 locally`);
+        return false;
+    }
+});
+/**
+ * A function to report the repo contains Yarn managed projects
+ * @param packageManagerInfo - used to make sure current package manager is yarn
+ * @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
+ *                              expected to be the result of `core.getInput('cache-dependency-path')`
+ * @return - true if all project directories configured to be Yarn managed
+ */
+const repoHasYarnBerryManagedDependencies = (packageManagerInfo, cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
+    if (packageManagerInfo.name !== 'yarn')
+        return false;
+    const yarnDirs = cacheDependencyPath
+        ? yield getProjectDirectoriesFromCacheDependencyPath(cacheDependencyPath)
+        : [''];
+    const isManagedList = yield Promise.all(yarnDirs.map(projectHasYarnBerryManagedDependencies));
+    return isManagedList.every(Boolean);
+});
+exports.repoHasYarnBerryManagedDependencies = repoHasYarnBerryManagedDependencies;
 function isGhes() {
     const ghUrl = new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com');
     return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM';
diff --git a/dist/setup/index.js b/dist/setup/index.js
index 83bf5f6a..5c500e23 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -71153,10 +71153,19 @@ const restoreCache = (packageManager, cacheDependencyPath) => __awaiter(void 0,
     if (!fileHash) {
         throw new Error('Some specified paths were not resolved, unable to cache dependencies.');
     }
-    const primaryKey = `node-cache-${platform}-${packageManager}-${fileHash}`;
+    const keyPrefix = `node-cache-${platform}-${packageManager}`;
+    const primaryKey = `${keyPrefix}-${fileHash}`;
     core.debug(`primary key is ${primaryKey}`);
     core.saveState(constants_1.State.CachePrimaryKey, primaryKey);
-    const cacheKey = yield cache.restoreCache(cachePaths, primaryKey);
+    const isManagedByYarnBerry = yield cache_utils_1.repoHasYarnBerryManagedDependencies(packageManagerInfo, cacheDependencyPath);
+    let cacheKey;
+    if (isManagedByYarnBerry) {
+        core.info('All dependencies are managed locally by yarn3, the previous cache can be used');
+        cacheKey = yield cache.restoreCache(cachePaths, primaryKey, [keyPrefix]);
+    }
+    else {
+        cacheKey = yield cache.restoreCache(cachePaths, primaryKey);
+    }
     core.setOutput('cache-hit', Boolean(cacheKey));
     if (!cacheKey) {
         core.info(`${packageManager} cache is not found`);
@@ -71217,7 +71226,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
     return (mod && mod.__esModule) ? mod : { "default": mod };
 };
 Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.isCacheFeatureAvailable = exports.isGhes = exports.getCacheDirectories = exports.getPackageManagerInfo = exports.getCommandOutputNotEmpty = exports.getCommandOutput = exports.supportedPackageManagers = void 0;
+exports.isCacheFeatureAvailable = exports.isGhes = exports.repoHasYarnBerryManagedDependencies = exports.getCacheDirectories = exports.resetProjectDirectoriesMemoized = exports.getPackageManagerInfo = exports.getCommandOutputNotEmpty = exports.getCommandOutput = exports.supportedPackageManagers = void 0;
 const core = __importStar(__nccwpck_require__(2186));
 const exec = __importStar(__nccwpck_require__(1514));
 const cache = __importStar(__nccwpck_require__(7799));
@@ -71286,6 +71295,19 @@ const getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, void
     }
 });
 exports.getPackageManagerInfo = getPackageManagerInfo;
+/**
+ * getProjectDirectoriesFromCacheDependencyPath is called twice during `restoreCache`
+ *  - first through `getCacheDirectories`
+ *  - second from `repoHasYarn3ManagedCache`
+ *
+ *  it contains expensive IO operation and thus should be memoized
+ */
+let projectDirectoriesMemoized = null;
+/**
+ * unit test must reset memoized variables
+ */
+const resetProjectDirectoriesMemoized = () => (projectDirectoriesMemoized = null);
+exports.resetProjectDirectoriesMemoized = resetProjectDirectoriesMemoized;
 /**
  * Expands (converts) the string input `cache-dependency-path` to list of directories that
  * may be project roots
@@ -71294,6 +71316,9 @@ exports.getPackageManagerInfo = getPackageManagerInfo;
  * @return list of directories and possible
  */
 const getProjectDirectoriesFromCacheDependencyPath = (cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
+    if (projectDirectoriesMemoized !== null) {
+        return projectDirectoriesMemoized;
+    }
     const globber = yield glob.create(cacheDependencyPath);
     const cacheDependenciesPaths = yield globber.glob();
     const existingDirectories = cacheDependenciesPaths
@@ -71302,6 +71327,7 @@ const getProjectDirectoriesFromCacheDependencyPath = (cacheDependencyPath) => __
         .filter(directory => fs_1.default.lstatSync(directory).isDirectory());
     if (!existingDirectories.length)
         core.warning(`No existing directories found containing cache-dependency-path="${cacheDependencyPath}"`);
+    projectDirectoriesMemoized = existingDirectories;
     return existingDirectories;
 });
 /**
@@ -71314,7 +71340,7 @@ const getProjectDirectoriesFromCacheDependencyPath = (cacheDependencyPath) => __
 const getCacheDirectoriesFromCacheDependencyPath = (packageManagerInfo, cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
     const projectDirectories = yield getProjectDirectoriesFromCacheDependencyPath(cacheDependencyPath);
     const cacheFoldersPaths = yield Promise.all(projectDirectories.map((projectDirectory) => __awaiter(void 0, void 0, void 0, function* () {
-        const cacheFolderPath = packageManagerInfo.getCacheFolderPath(projectDirectory);
+        const cacheFolderPath = yield packageManagerInfo.getCacheFolderPath(projectDirectory);
         core.debug(`${packageManagerInfo.name}'s cache folder "${cacheFolderPath}" configured for the directory "${projectDirectory}"`);
         return cacheFolderPath;
     })));
@@ -71348,6 +71374,56 @@ const getCacheDirectories = (packageManagerInfo, cacheDependencyPath) => __await
     return getCacheDirectoriesForRootProject(packageManagerInfo);
 });
 exports.getCacheDirectories = getCacheDirectories;
+/**
+ * A function to check if the directory is a yarn project configured to manage
+ * obsolete dependencies in the local cache
+ * @param directory - a path to the folder
+ * @return - true if the directory's project is yarn managed
+ *  - if there's .yarn/cache folder do not mess with the dependencies kept in the repo, return false
+ *  - global cache is not managed by yarn @see https://yarnpkg.com/features/offline-cache, return false
+ *  - if local cache is not explicitly enabled (not yarn3), return false
+ *  - return true otherwise
+ */
+const projectHasYarnBerryManagedDependencies = (directory) => __awaiter(void 0, void 0, void 0, function* () {
+    const workDir = directory || process.env.GITHUB_WORKSPACE || '.';
+    core.debug(`check if "${workDir}" has locally managed yarn3 dependencies`);
+    // if .yarn/cache directory exists the cache is managed by version control system
+    const yarnCacheFile = path_1.default.join(workDir, '.yarn', 'cache');
+    if (fs_1.default.existsSync(yarnCacheFile) &&
+        fs_1.default.lstatSync(yarnCacheFile).isDirectory()) {
+        core.debug(`"${workDir}" has .yarn/cache - dependencies are kept in the repository`);
+        return Promise.resolve(false);
+    }
+    // NOTE: yarn1 returns 'undefined' with return code = 0
+    const enableGlobalCache = yield exports.getCommandOutput('yarn config get enableGlobalCache', workDir);
+    // only local cache is not managed by yarn
+    const managed = enableGlobalCache.includes('false');
+    if (managed) {
+        core.debug(`"${workDir}" dependencies are managed by yarn 3 locally`);
+        return true;
+    }
+    else {
+        core.debug(`"${workDir}" dependencies are not managed by yarn 3 locally`);
+        return false;
+    }
+});
+/**
+ * A function to report the repo contains Yarn managed projects
+ * @param packageManagerInfo - used to make sure current package manager is yarn
+ * @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
+ *                              expected to be the result of `core.getInput('cache-dependency-path')`
+ * @return - true if all project directories configured to be Yarn managed
+ */
+const repoHasYarnBerryManagedDependencies = (packageManagerInfo, cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
+    if (packageManagerInfo.name !== 'yarn')
+        return false;
+    const yarnDirs = cacheDependencyPath
+        ? yield getProjectDirectoriesFromCacheDependencyPath(cacheDependencyPath)
+        : [''];
+    const isManagedList = yield Promise.all(yarnDirs.map(projectHasYarnBerryManagedDependencies));
+    return isManagedList.every(Boolean);
+});
+exports.repoHasYarnBerryManagedDependencies = repoHasYarnBerryManagedDependencies;
 function isGhes() {
     const ghUrl = new URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com');
     return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM';
diff --git a/src/cache-restore.ts b/src/cache-restore.ts
index 6ac2cc75..3b230970 100644
--- a/src/cache-restore.ts
+++ b/src/cache-restore.ts
@@ -8,6 +8,7 @@ import {State} from './constants';
 import {
   getCacheDirectories,
   getPackageManagerInfo,
+  repoHasYarnBerryManagedDependencies,
   PackageManagerInfo
 } from './cache-utils';
 
@@ -37,12 +38,26 @@ export const restoreCache = async (
     );
   }
 
-  const primaryKey = `node-cache-${platform}-${packageManager}-${fileHash}`;
+  const keyPrefix = `node-cache-${platform}-${packageManager}`;
+  const primaryKey = `${keyPrefix}-${fileHash}`;
   core.debug(`primary key is ${primaryKey}`);
 
   core.saveState(State.CachePrimaryKey, primaryKey);
 
-  const cacheKey = await cache.restoreCache(cachePaths, primaryKey);
+  const isManagedByYarnBerry = await repoHasYarnBerryManagedDependencies(
+    packageManagerInfo,
+    cacheDependencyPath
+  );
+  let cacheKey: string | undefined;
+  if (isManagedByYarnBerry) {
+    core.info(
+      'All dependencies are managed locally by yarn3, the previous cache can be used'
+    );
+    cacheKey = await cache.restoreCache(cachePaths, primaryKey, [keyPrefix]);
+  } else {
+    cacheKey = await cache.restoreCache(cachePaths, primaryKey);
+  }
+
   core.setOutput('cache-hit', Boolean(cacheKey));
 
   if (!cacheKey) {
diff --git a/src/cache-utils.ts b/src/cache-utils.ts
index ac82c4c2..8d25c36d 100644
--- a/src/cache-utils.ts
+++ b/src/cache-utils.ts
@@ -110,6 +110,20 @@ export const getPackageManagerInfo = async (packageManager: string) => {
   }
 };
 
+/**
+ * getProjectDirectoriesFromCacheDependencyPath is called twice during `restoreCache`
+ *  - first through `getCacheDirectories`
+ *  - second from `repoHasYarn3ManagedCache`
+ *
+ *  it contains expensive IO operation and thus should be memoized
+ */
+
+let projectDirectoriesMemoized: string[] | null = null;
+/**
+ * unit test must reset memoized variables
+ */
+export const resetProjectDirectoriesMemoized = () =>
+  (projectDirectoriesMemoized = null);
 /**
  * Expands (converts) the string input `cache-dependency-path` to list of directories that
  * may be project roots
@@ -120,6 +134,10 @@ export const getPackageManagerInfo = async (packageManager: string) => {
 const getProjectDirectoriesFromCacheDependencyPath = async (
   cacheDependencyPath: string
 ): Promise<string[]> => {
+  if (projectDirectoriesMemoized !== null) {
+    return projectDirectoriesMemoized;
+  }
+
   const globber = await glob.create(cacheDependencyPath);
   const cacheDependenciesPaths = await globber.glob();
 
@@ -133,6 +151,7 @@ const getProjectDirectoriesFromCacheDependencyPath = async (
       `No existing directories found containing cache-dependency-path="${cacheDependencyPath}"`
     );
 
+  projectDirectoriesMemoized = existingDirectories;
   return existingDirectories;
 };
 
@@ -152,8 +171,9 @@ const getCacheDirectoriesFromCacheDependencyPath = async (
   );
   const cacheFoldersPaths = await Promise.all(
     projectDirectories.map(async projectDirectory => {
-      const cacheFolderPath =
-        packageManagerInfo.getCacheFolderPath(projectDirectory);
+      const cacheFolderPath = await packageManagerInfo.getCacheFolderPath(
+        projectDirectory
+      );
       core.debug(
         `${packageManagerInfo.name}'s cache folder "${cacheFolderPath}" configured for the directory "${projectDirectory}"`
       );
@@ -202,6 +222,74 @@ export const getCacheDirectories = async (
   return getCacheDirectoriesForRootProject(packageManagerInfo);
 };
 
+/**
+ * A function to check if the directory is a yarn project configured to manage
+ * obsolete dependencies in the local cache
+ * @param directory - a path to the folder
+ * @return - true if the directory's project is yarn managed
+ *  - if there's .yarn/cache folder do not mess with the dependencies kept in the repo, return false
+ *  - global cache is not managed by yarn @see https://yarnpkg.com/features/offline-cache, return false
+ *  - if local cache is not explicitly enabled (not yarn3), return false
+ *  - return true otherwise
+ */
+const projectHasYarnBerryManagedDependencies = async (
+  directory: string
+): Promise<boolean> => {
+  const workDir = directory || process.env.GITHUB_WORKSPACE || '.';
+  core.debug(`check if "${workDir}" has locally managed yarn3 dependencies`);
+
+  // if .yarn/cache directory exists the cache is managed by version control system
+  const yarnCacheFile = path.join(workDir, '.yarn', 'cache');
+  if (
+    fs.existsSync(yarnCacheFile) &&
+    fs.lstatSync(yarnCacheFile).isDirectory()
+  ) {
+    core.debug(
+      `"${workDir}" has .yarn/cache - dependencies are kept in the repository`
+    );
+    return Promise.resolve(false);
+  }
+
+  // NOTE: yarn1 returns 'undefined' with return code = 0
+  const enableGlobalCache = await getCommandOutput(
+    'yarn config get enableGlobalCache',
+    workDir
+  );
+  // only local cache is not managed by yarn
+  const managed = enableGlobalCache.includes('false');
+  if (managed) {
+    core.debug(`"${workDir}" dependencies are managed by yarn 3 locally`);
+    return true;
+  } else {
+    core.debug(`"${workDir}" dependencies are not managed by yarn 3 locally`);
+    return false;
+  }
+};
+
+/**
+ * A function to report the repo contains Yarn managed projects
+ * @param packageManagerInfo - used to make sure current package manager is yarn
+ * @param cacheDependencyPath - either a single string or multiline string with possible glob patterns
+ *                              expected to be the result of `core.getInput('cache-dependency-path')`
+ * @return - true if all project directories configured to be Yarn managed
+ */
+export const repoHasYarnBerryManagedDependencies = async (
+  packageManagerInfo: PackageManagerInfo,
+  cacheDependencyPath: string
+): Promise<boolean> => {
+  if (packageManagerInfo.name !== 'yarn') return false;
+
+  const yarnDirs = cacheDependencyPath
+    ? await getProjectDirectoriesFromCacheDependencyPath(cacheDependencyPath)
+    : [''];
+
+  const isManagedList = await Promise.all(
+    yarnDirs.map(projectHasYarnBerryManagedDependencies)
+  );
+
+  return isManagedList.every(Boolean);
+};
+
 export function isGhes(): boolean {
   const ghUrl = new URL(
     process.env['GITHUB_SERVER_URL'] || 'https://github.com'