From 162ddf7c40a5ef49459ceb64575b09d8f89e2b34 Mon Sep 17 00:00:00 2001
From: Jacob Gillespie <jacobwgillespie@gmail.com>
Date: Wed, 30 Jun 2021 16:44:51 +0100
Subject: [PATCH] Add pnpm caching support

---
 .github/workflows/e2e-cache.yml |  33 +++
 README.md                       |  19 +-
 __tests__/cache-restore.test.ts |  16 +-
 __tests__/cache-save.test.ts    |  46 ++++
 __tests__/cache-utils.test.ts   |   5 +
 __tests__/data/pnpm-lock.yaml   | 360 ++++++++++++++++++++++++++++++++
 action.yml                      |   2 +-
 dist/cache-save/index.js        |  20 +-
 dist/setup/index.js             |  20 +-
 src/cache-utils.ts              |  23 +-
 src/constants.ts                |   1 +
 11 files changed, 531 insertions(+), 14 deletions(-)
 create mode 100644 __tests__/data/pnpm-lock.yaml

diff --git a/.github/workflows/e2e-cache.yml b/.github/workflows/e2e-cache.yml
index fd0d4046..7e0fd247 100644
--- a/.github/workflows/e2e-cache.yml
+++ b/.github/workflows/e2e-cache.yml
@@ -35,6 +35,39 @@ jobs:
         run: __tests__/verify-node.sh "${{ matrix.node-version }}"
         shell: bash
 
+  node-pnpm-depencies-caching:
+    name: Test pnpm (Node ${{ matrix.node-version}}, ${{ matrix.os }})
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest, windows-latest, macos-latest]
+        node-version: [12, 14, 16]
+    steps:
+      - uses: actions/checkout@v2
+      - name: Install pnpm
+        uses: pnpm/action-setup@v2
+        with:
+          version: 6.9.0
+      - name: Generate pnpm file
+        run: pnpm install
+      - name: Remove dependencies
+        shell: pwsh
+        run: Remove-Item node_modules -Force -Recurse
+      - name: Clean global cache
+        run: rm -rf ~/.pnpm-store
+        shell: bash
+      - name: Setup Node
+        uses: ./
+        with:
+          node-version: ${{ matrix.node-version }}
+          cache: 'pnpm'
+      - name: Install dependencies
+        run: pnpm install
+      - name: Verify node and pnpm
+        run: __tests__/verify-node.sh "${{ matrix.node-version }}"
+        shell: bash
+
   node-yarn1-depencies-caching:
     name: Test yarn 1 (Node ${{ matrix.node-version}}, ${{ matrix.os }})
     runs-on: ${{ matrix.os }}
diff --git a/README.md b/README.md
index aa243c2e..d813bb8c 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
 This action provides the following functionality for GitHub Actions users:
 
 - Optionally downloading and caching distribution of the requested Node.js version, and adding it to the PATH
-- Optionally caching npm/yarn dependencies
+- Optionally caching npm/pnpm/yarn dependencies
 - Registering problem matchers for error output
 - Configuring authentication for GPR or npm
 
@@ -41,7 +41,7 @@ nvm lts syntax: `lts/erbium`, `lts/fermium`, `lts/*`
 
 ### Caching packages dependencies
 
-The action has a built-in functionality for caching and restoring npm/yarn dependencies. Supported package managers are `npm`, `yarn`. The `cache` input is optional, and caching is turned off by default.
+The action has a built-in functionality for caching and restoring npm/yarn dependencies. Supported package managers are `npm`, `pnpm`, `yarn`. The `cache` input is optional, and caching is turned off by default.
 
 **Caching npm dependencies:**
 ```yaml
@@ -55,6 +55,21 @@ steps:
 - run: npm test
 ```
 
+**Caching pnpm dependencies:**
+```yaml
+steps:
+- uses: actions/checkout@v2
+- uses: pnpm/action-setup@v2
+  with:
+    version: 6.9.0
+- uses: actions/setup-node@v2
+  with:
+    node-version: '14'
+    cache: 'pnpm'
+- run: pnpm install
+- run: pnpm test
+```
+
 **Caching yarn dependencies:**
 ```yaml
 steps:
diff --git a/__tests__/cache-restore.test.ts b/__tests__/cache-restore.test.ts
index 18962e57..483f8eb6 100644
--- a/__tests__/cache-restore.test.ts
+++ b/__tests__/cache-restore.test.ts
@@ -14,14 +14,18 @@ describe('cache-restore', () => {
   const platform = process.env.RUNNER_OS;
   const commonPath = '/some/random/path';
   const npmCachePath = `${commonPath}/npm`;
+  const pnpmCachePath = `${commonPath}/pnpm`;
   const yarn1CachePath = `${commonPath}/yarn1`;
   const yarn2CachePath = `${commonPath}/yarn2`;
   const yarnFileHash =
     'b8a0bae5243251f7c07dd52d1f78ff78281dfefaded700a176261b6b54fa245b';
   const npmFileHash =
     'abf7c9b306a3149dcfba4673e2362755503bcceaab46f0e4e6fee0ade493e20c';
+  const pnpmFileHash =
+    '26309058093e84713f38869c50cf1cee9b08155ede874ec1b44ce3fca8c68c70';
   const cachesObject = {
     [npmCachePath]: npmFileHash,
+    [pnpmCachePath]: pnpmFileHash,
     [yarn1CachePath]: yarnFileHash,
     [yarn2CachePath]: yarnFileHash
   };
@@ -30,6 +34,8 @@ describe('cache-restore', () => {
     switch (command) {
       case utils.supportedPackageManagers.npm.getCacheFolderCommand:
         return npmCachePath;
+      case utils.supportedPackageManagers.pnpm.getCacheFolderCommand:
+        return pnpmCachePath;
       case utils.supportedPackageManagers.yarn1.getCacheFolderCommand:
         return yarn1CachePath;
       case utils.supportedPackageManagers.yarn2.getCacheFolderCommand:
@@ -66,6 +72,8 @@ describe('cache-restore', () => {
     hashFilesSpy.mockImplementation((pattern: string) => {
       if (pattern.includes('package-lock.json')) {
         return npmFileHash;
+      } else if (pattern.includes('pnpm-lock.yaml')) {
+        return pnpmFileHash;
       } else if (pattern.includes('yarn.lock')) {
         return yarnFileHash;
       } else {
@@ -97,7 +105,7 @@ describe('cache-restore', () => {
   });
 
   describe('Validate provided package manager', () => {
-    it.each([['npm7'], ['npm6'], ['yarn1'], ['yarn2'], ['random']])(
+    it.each([['npm7'], ['npm6'], ['pnpm6'], ['yarn1'], ['yarn2'], ['random']])(
       'Throw an error because %s is not supported',
       async packageManager => {
         await expect(restoreCache(packageManager)).rejects.toThrowError(
@@ -111,7 +119,8 @@ describe('cache-restore', () => {
     it.each([
       ['yarn', '2.1.2', yarnFileHash],
       ['yarn', '1.2.3', yarnFileHash],
-      ['npm', '', npmFileHash]
+      ['npm', '', npmFileHash],
+      ['pnpm', '', pnpmFileHash]
     ])(
       'restored dependencies for %s',
       async (packageManager, toolVersion, fileHash) => {
@@ -139,7 +148,8 @@ describe('cache-restore', () => {
     it.each([
       ['yarn', '2.1.2', yarnFileHash],
       ['yarn', '1.2.3', yarnFileHash],
-      ['npm', '', npmFileHash]
+      ['npm', '', npmFileHash],
+      ['pnpm', '', pnpmFileHash]
     ])(
       'dependencies are changed %s',
       async (packageManager, toolVersion, fileHash) => {
diff --git a/__tests__/cache-save.test.ts b/__tests__/cache-save.test.ts
index 099a6119..64ef942c 100644
--- a/__tests__/cache-save.test.ts
+++ b/__tests__/cache-save.test.ts
@@ -12,6 +12,8 @@ describe('run', () => {
     'b8a0bae5243251f7c07dd52d1f78ff78281dfefaded700a176261b6b54fa245b';
   const npmFileHash =
     'abf7c9b306a3149dcfba4673e2362755503bcceaab46f0e4e6fee0ade493e20c';
+  const pnpmFileHash =
+    '26309058093e84713f38869c50cf1cee9b08155ede874ec1b44ce3fca8c68c70';
   const commonPath = '/some/random/path';
   process.env['GITHUB_WORKSPACE'] = path.join(__dirname, 'data');
 
@@ -150,6 +152,23 @@ describe('run', () => {
       );
       expect(setFailedSpy).not.toHaveBeenCalled();
     });
+
+    it('should not save cache for npm', async () => {
+      inputs['cache'] = 'pnpm';
+      getStateSpy.mockImplementation(() => pnpmFileHash);
+      getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/pnpm`);
+
+      await run();
+
+      expect(getInputSpy).toHaveBeenCalled();
+      expect(getStateSpy).toHaveBeenCalledTimes(2);
+      expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
+      expect(debugSpy).toHaveBeenCalledWith(`pnpm path is ${commonPath}/pnpm`);
+      expect(infoSpy).toHaveBeenCalledWith(
+        `Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.`
+      );
+      expect(setFailedSpy).not.toHaveBeenCalled();
+    });
   });
 
   describe('action saves the cache', () => {
@@ -239,6 +258,33 @@ describe('run', () => {
       );
       expect(setFailedSpy).not.toHaveBeenCalled();
     });
+
+    it('saves cache from pnpm', async () => {
+      inputs['cache'] = 'pnpm';
+      getStateSpy.mockImplementation((name: string) => {
+        if (name === State.CacheMatchedKey) {
+          return pnpmFileHash;
+        } else {
+          return npmFileHash;
+        }
+      });
+      getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/pnpm`);
+
+      await run();
+
+      expect(getInputSpy).toHaveBeenCalled();
+      expect(getStateSpy).toHaveBeenCalledTimes(2);
+      expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
+      expect(debugSpy).toHaveBeenCalledWith(`pnpm path is ${commonPath}/pnpm`);
+      expect(infoSpy).not.toHaveBeenCalledWith(
+        `Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.`
+      );
+      expect(saveCacheSpy).toHaveBeenCalled();
+      expect(infoSpy).toHaveBeenLastCalledWith(
+        `Cache saved with the key: ${npmFileHash}`
+      );
+      expect(setFailedSpy).not.toHaveBeenCalled();
+    });
   });
 
   afterEach(() => {
diff --git a/__tests__/cache-utils.test.ts b/__tests__/cache-utils.test.ts
index 0be153ba..0ceabeb0 100644
--- a/__tests__/cache-utils.test.ts
+++ b/__tests__/cache-utils.test.ts
@@ -14,6 +14,10 @@ describe('cache-utils', () => {
   function getPackagePath(name: string) {
     if (name === utils.supportedPackageManagers.npm.getCacheFolderCommand) {
       return `${commonPath}/npm`;
+    } else if (
+      name === utils.supportedPackageManagers.pnpm.getCacheFolderCommand
+    ) {
+      return `${commonPath}/pnpm`;
     } else {
       if (name === utils.supportedPackageManagers.yarn1.getCacheFolderCommand) {
         return `${commonPath}/yarn1`;
@@ -34,6 +38,7 @@ describe('cache-utils', () => {
   describe('getPackageManagerInfo', () => {
     it.each<[string, PackageManagerInfo | null]>([
       ['npm', utils.supportedPackageManagers.npm],
+      ['pnpm', utils.supportedPackageManagers.pnpm],
       ['yarn', utils.supportedPackageManagers.yarn1],
       ['yarn1', null],
       ['yarn2', null],
diff --git a/__tests__/data/pnpm-lock.yaml b/__tests__/data/pnpm-lock.yaml
new file mode 100644
index 00000000..f4c3963e
--- /dev/null
+++ b/__tests__/data/pnpm-lock.yaml
@@ -0,0 +1,360 @@
+lockfileVersion: 5.3
+
+specifiers:
+  express: ^4.17.1
+
+dependencies:
+  express: 4.17.1
+
+packages:
+
+  /accepts/1.3.7:
+    resolution: {integrity: sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      mime-types: 2.1.31
+      negotiator: 0.6.2
+    dev: false
+
+  /array-flatten/1.1.1:
+    resolution: {integrity: sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=}
+    dev: false
+
+  /body-parser/1.19.0:
+    resolution: {integrity: sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==}
+    engines: {node: '>= 0.8'}
+    dependencies:
+      bytes: 3.1.0
+      content-type: 1.0.4
+      debug: 2.6.9
+      depd: 1.1.2
+      http-errors: 1.7.2
+      iconv-lite: 0.4.24
+      on-finished: 2.3.0
+      qs: 6.7.0
+      raw-body: 2.4.0
+      type-is: 1.6.18
+    dev: false
+
+  /bytes/3.1.0:
+    resolution: {integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==}
+    engines: {node: '>= 0.8'}
+    dev: false
+
+  /content-disposition/0.5.3:
+    resolution: {integrity: sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      safe-buffer: 5.1.2
+    dev: false
+
+  /content-type/1.0.4:
+    resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /cookie-signature/1.0.6:
+    resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=}
+    dev: false
+
+  /cookie/0.4.0:
+    resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /debug/2.6.9:
+    resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+    dependencies:
+      ms: 2.0.0
+    dev: false
+
+  /depd/1.1.2:
+    resolution: {integrity: sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /destroy/1.0.4:
+    resolution: {integrity: sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=}
+    dev: false
+
+  /ee-first/1.1.1:
+    resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
+    dev: false
+
+  /encodeurl/1.0.2:
+    resolution: {integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=}
+    engines: {node: '>= 0.8'}
+    dev: false
+
+  /escape-html/1.0.3:
+    resolution: {integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=}
+    dev: false
+
+  /etag/1.8.1:
+    resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /express/4.17.1:
+    resolution: {integrity: sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==}
+    engines: {node: '>= 0.10.0'}
+    dependencies:
+      accepts: 1.3.7
+      array-flatten: 1.1.1
+      body-parser: 1.19.0
+      content-disposition: 0.5.3
+      content-type: 1.0.4
+      cookie: 0.4.0
+      cookie-signature: 1.0.6
+      debug: 2.6.9
+      depd: 1.1.2
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      etag: 1.8.1
+      finalhandler: 1.1.2
+      fresh: 0.5.2
+      merge-descriptors: 1.0.1
+      methods: 1.1.2
+      on-finished: 2.3.0
+      parseurl: 1.3.3
+      path-to-regexp: 0.1.7
+      proxy-addr: 2.0.7
+      qs: 6.7.0
+      range-parser: 1.2.1
+      safe-buffer: 5.1.2
+      send: 0.17.1
+      serve-static: 1.14.1
+      setprototypeof: 1.1.1
+      statuses: 1.5.0
+      type-is: 1.6.18
+      utils-merge: 1.0.1
+      vary: 1.1.2
+    dev: false
+
+  /finalhandler/1.1.2:
+    resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==}
+    engines: {node: '>= 0.8'}
+    dependencies:
+      debug: 2.6.9
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      on-finished: 2.3.0
+      parseurl: 1.3.3
+      statuses: 1.5.0
+      unpipe: 1.0.0
+    dev: false
+
+  /forwarded/0.2.0:
+    resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /fresh/0.5.2:
+    resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /http-errors/1.7.2:
+    resolution: {integrity: sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      depd: 1.1.2
+      inherits: 2.0.3
+      setprototypeof: 1.1.1
+      statuses: 1.5.0
+      toidentifier: 1.0.0
+    dev: false
+
+  /http-errors/1.7.3:
+    resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      depd: 1.1.2
+      inherits: 2.0.4
+      setprototypeof: 1.1.1
+      statuses: 1.5.0
+      toidentifier: 1.0.0
+    dev: false
+
+  /iconv-lite/0.4.24:
+    resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
+    engines: {node: '>=0.10.0'}
+    dependencies:
+      safer-buffer: 2.1.2
+    dev: false
+
+  /inherits/2.0.3:
+    resolution: {integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=}
+    dev: false
+
+  /inherits/2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+    dev: false
+
+  /ipaddr.js/1.9.1:
+    resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+    engines: {node: '>= 0.10'}
+    dev: false
+
+  /media-typer/0.3.0:
+    resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /merge-descriptors/1.0.1:
+    resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=}
+    dev: false
+
+  /methods/1.1.2:
+    resolution: {integrity: sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /mime-db/1.48.0:
+    resolution: {integrity: sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /mime-types/2.1.31:
+    resolution: {integrity: sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      mime-db: 1.48.0
+    dev: false
+
+  /mime/1.6.0:
+    resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+    engines: {node: '>=4'}
+    hasBin: true
+    dev: false
+
+  /ms/2.0.0:
+    resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=}
+    dev: false
+
+  /ms/2.1.1:
+    resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==}
+    dev: false
+
+  /negotiator/0.6.2:
+    resolution: {integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /on-finished/2.3.0:
+    resolution: {integrity: sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=}
+    engines: {node: '>= 0.8'}
+    dependencies:
+      ee-first: 1.1.1
+    dev: false
+
+  /parseurl/1.3.3:
+    resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+    engines: {node: '>= 0.8'}
+    dev: false
+
+  /path-to-regexp/0.1.7:
+    resolution: {integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=}
+    dev: false
+
+  /proxy-addr/2.0.7:
+    resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+    engines: {node: '>= 0.10'}
+    dependencies:
+      forwarded: 0.2.0
+      ipaddr.js: 1.9.1
+    dev: false
+
+  /qs/6.7.0:
+    resolution: {integrity: sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==}
+    engines: {node: '>=0.6'}
+    dev: false
+
+  /range-parser/1.2.1:
+    resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /raw-body/2.4.0:
+    resolution: {integrity: sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==}
+    engines: {node: '>= 0.8'}
+    dependencies:
+      bytes: 3.1.0
+      http-errors: 1.7.2
+      iconv-lite: 0.4.24
+      unpipe: 1.0.0
+    dev: false
+
+  /safe-buffer/5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+    dev: false
+
+  /safer-buffer/2.1.2:
+    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+    dev: false
+
+  /send/0.17.1:
+    resolution: {integrity: sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==}
+    engines: {node: '>= 0.8.0'}
+    dependencies:
+      debug: 2.6.9
+      depd: 1.1.2
+      destroy: 1.0.4
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      etag: 1.8.1
+      fresh: 0.5.2
+      http-errors: 1.7.3
+      mime: 1.6.0
+      ms: 2.1.1
+      on-finished: 2.3.0
+      range-parser: 1.2.1
+      statuses: 1.5.0
+    dev: false
+
+  /serve-static/1.14.1:
+    resolution: {integrity: sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==}
+    engines: {node: '>= 0.8.0'}
+    dependencies:
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      parseurl: 1.3.3
+      send: 0.17.1
+    dev: false
+
+  /setprototypeof/1.1.1:
+    resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==}
+    dev: false
+
+  /statuses/1.5.0:
+    resolution: {integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /toidentifier/1.0.0:
+    resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==}
+    engines: {node: '>=0.6'}
+    dev: false
+
+  /type-is/1.6.18:
+    resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      media-typer: 0.3.0
+      mime-types: 2.1.31
+    dev: false
+
+  /unpipe/1.0.0:
+    resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=}
+    engines: {node: '>= 0.8'}
+    dev: false
+
+  /utils-merge/1.0.1:
+    resolution: {integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=}
+    engines: {node: '>= 0.4.0'}
+    dev: false
+
+  /vary/1.1.2:
+    resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=}
+    engines: {node: '>= 0.8'}
+    dev: false
diff --git a/action.yml b/action.yml
index 213dc9d2..2552b8d5 100644
--- a/action.yml
+++ b/action.yml
@@ -20,7 +20,7 @@ inputs:
     description: Used to pull node distributions from node-versions.  Since there's a default, this is typically not supplied by the user.
     default: ${{ github.token }}
   cache:
-    description: 'Used to specify a package manager for caching in the default directory. Supported values: npm, yarn'
+    description: 'Used to specify a package manager for caching in the default directory. Supported values: npm, pnpm, yarn'
 # TODO: add input to control forcing to pull from cloud or dist. 
 #       escape valve for someone having issues or needing the absolute latest which isn't cached yet
 # Deprecated option, do not use. Will not be supported after October 1, 2019
diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js
index f470ef5b..fdc89282 100644
--- a/dist/cache-save/index.js
+++ b/dist/cache-save/index.js
@@ -4290,6 +4290,11 @@ exports.supportedPackageManagers = {
         lockFilePatterns: ['package-lock.json', 'yarn.lock'],
         getCacheFolderCommand: 'npm config get cache'
     },
+    pnpm: {
+        lockFilePatterns: ['pnpm-lock.yaml'],
+        getCacheFolderCommand: 'pnpm get store',
+        defaultCacheFolder: '~/.pnpm-store'
+    },
     yarn1: {
         lockFilePatterns: ['yarn.lock'],
         getCacheFolderCommand: 'yarn cache dir'
@@ -4304,7 +4309,7 @@ exports.getCommandOutput = (toolCommand) => __awaiter(void 0, void 0, void 0, fu
     if (stderr) {
         throw new Error(stderr);
     }
-    return stdout;
+    return stdout.trim();
 });
 const getPackageManagerVersion = (packageManager, command) => __awaiter(void 0, void 0, void 0, function* () {
     const stdOut = yield exports.getCommandOutput(`${packageManager} ${command}`);
@@ -4317,6 +4322,9 @@ exports.getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, vo
     if (packageManager === 'npm') {
         return exports.supportedPackageManagers.npm;
     }
+    else if (packageManager === 'pnpm') {
+        return exports.supportedPackageManagers.pnpm;
+    }
     else if (packageManager === 'yarn') {
         const yarnVersion = yield getPackageManagerVersion('yarn', '--version');
         core.debug(`Consumed yarn version is ${yarnVersion}`);
@@ -4332,7 +4340,14 @@ exports.getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, vo
     }
 });
 exports.getCacheDirectoryPath = (packageManagerInfo, packageManager) => __awaiter(void 0, void 0, void 0, function* () {
-    const stdOut = yield exports.getCommandOutput(packageManagerInfo.getCacheFolderCommand);
+    let stdOut = yield exports.getCommandOutput(packageManagerInfo.getCacheFolderCommand);
+    // pnpm returns 'undefined' if no custom store path is set
+    if (stdOut === 'undefined') {
+        stdOut = '';
+    }
+    if (!stdOut && packageManagerInfo.defaultCacheFolder) {
+        stdOut = packageManagerInfo.defaultCacheFolder;
+    }
     if (!stdOut) {
         throw new Error(`Could not get cache folder path for ${packageManager}`);
     }
@@ -5270,6 +5285,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
 var LockType;
 (function (LockType) {
     LockType["Npm"] = "npm";
+    LockType["Pnpm"] = "pnpm";
     LockType["Yarn"] = "yarn";
 })(LockType = exports.LockType || (exports.LockType = {}));
 var State;
diff --git a/dist/setup/index.js b/dist/setup/index.js
index ab46cecf..69ec4c9c 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -8652,6 +8652,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
 var LockType;
 (function (LockType) {
     LockType["Npm"] = "npm";
+    LockType["Pnpm"] = "pnpm";
     LockType["Yarn"] = "yarn";
 })(LockType = exports.LockType || (exports.LockType = {}));
 var State;
@@ -51587,6 +51588,11 @@ exports.supportedPackageManagers = {
         lockFilePatterns: ['package-lock.json', 'yarn.lock'],
         getCacheFolderCommand: 'npm config get cache'
     },
+    pnpm: {
+        lockFilePatterns: ['pnpm-lock.yaml'],
+        getCacheFolderCommand: 'pnpm get store',
+        defaultCacheFolder: '~/.pnpm-store'
+    },
     yarn1: {
         lockFilePatterns: ['yarn.lock'],
         getCacheFolderCommand: 'yarn cache dir'
@@ -51601,7 +51607,7 @@ exports.getCommandOutput = (toolCommand) => __awaiter(void 0, void 0, void 0, fu
     if (stderr) {
         throw new Error(stderr);
     }
-    return stdout;
+    return stdout.trim();
 });
 const getPackageManagerVersion = (packageManager, command) => __awaiter(void 0, void 0, void 0, function* () {
     const stdOut = yield exports.getCommandOutput(`${packageManager} ${command}`);
@@ -51614,6 +51620,9 @@ exports.getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, vo
     if (packageManager === 'npm') {
         return exports.supportedPackageManagers.npm;
     }
+    else if (packageManager === 'pnpm') {
+        return exports.supportedPackageManagers.pnpm;
+    }
     else if (packageManager === 'yarn') {
         const yarnVersion = yield getPackageManagerVersion('yarn', '--version');
         core.debug(`Consumed yarn version is ${yarnVersion}`);
@@ -51629,7 +51638,14 @@ exports.getPackageManagerInfo = (packageManager) => __awaiter(void 0, void 0, vo
     }
 });
 exports.getCacheDirectoryPath = (packageManagerInfo, packageManager) => __awaiter(void 0, void 0, void 0, function* () {
-    const stdOut = yield exports.getCommandOutput(packageManagerInfo.getCacheFolderCommand);
+    let stdOut = yield exports.getCommandOutput(packageManagerInfo.getCacheFolderCommand);
+    // pnpm returns 'undefined' if no custom store path is set
+    if (stdOut === 'undefined') {
+        stdOut = '';
+    }
+    if (!stdOut && packageManagerInfo.defaultCacheFolder) {
+        stdOut = packageManagerInfo.defaultCacheFolder;
+    }
     if (!stdOut) {
         throw new Error(`Could not get cache folder path for ${packageManager}`);
     }
diff --git a/src/cache-utils.ts b/src/cache-utils.ts
index ced58c6c..e8f68951 100644
--- a/src/cache-utils.ts
+++ b/src/cache-utils.ts
@@ -8,6 +8,7 @@ type SupportedPackageManagers = {
 export interface PackageManagerInfo {
   lockFilePatterns: Array<string>;
   getCacheFolderCommand: string;
+  defaultCacheFolder?: string;
 }
 
 export const supportedPackageManagers: SupportedPackageManagers = {
@@ -15,6 +16,11 @@ export const supportedPackageManagers: SupportedPackageManagers = {
     lockFilePatterns: ['package-lock.json', 'yarn.lock'],
     getCacheFolderCommand: 'npm config get cache'
   },
+  pnpm: {
+    lockFilePatterns: ['pnpm-lock.yaml'],
+    getCacheFolderCommand: 'pnpm get store',
+    defaultCacheFolder: '~/.pnpm-store'
+  },
   yarn1: {
     lockFilePatterns: ['yarn.lock'],
     getCacheFolderCommand: 'yarn cache dir'
@@ -32,7 +38,7 @@ export const getCommandOutput = async (toolCommand: string) => {
     throw new Error(stderr);
   }
 
-  return stdout;
+  return stdout.trim();
 };
 
 const getPackageManagerVersion = async (
@@ -51,6 +57,8 @@ const getPackageManagerVersion = async (
 export const getPackageManagerInfo = async (packageManager: string) => {
   if (packageManager === 'npm') {
     return supportedPackageManagers.npm;
+  } else if (packageManager === 'pnpm') {
+    return supportedPackageManagers.pnpm;
   } else if (packageManager === 'yarn') {
     const yarnVersion = await getPackageManagerVersion('yarn', '--version');
 
@@ -70,9 +78,16 @@ export const getCacheDirectoryPath = async (
   packageManagerInfo: PackageManagerInfo,
   packageManager: string
 ) => {
-  const stdOut = await getCommandOutput(
-    packageManagerInfo.getCacheFolderCommand
-  );
+  let stdOut = await getCommandOutput(packageManagerInfo.getCacheFolderCommand);
+
+  // pnpm returns 'undefined' if no custom store path is set
+  if (stdOut === 'undefined') {
+    stdOut = '';
+  }
+
+  if (!stdOut && packageManagerInfo.defaultCacheFolder) {
+    stdOut = packageManagerInfo.defaultCacheFolder;
+  }
 
   if (!stdOut) {
     throw new Error(`Could not get cache folder path for ${packageManager}`);
diff --git a/src/constants.ts b/src/constants.ts
index b7c45de6..021418c2 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,5 +1,6 @@
 export enum LockType {
   Npm = 'npm',
+  Pnpm = 'pnpm',
   Yarn = 'yarn'
 }