Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pnpm caching support #278

Merged
merged 12 commits into from Jul 20, 2021
35 changes: 34 additions & 1 deletion .github/workflows/e2e-cache.yml
Expand Up @@ -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.10.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 }}
Expand Down Expand Up @@ -98,4 +131,4 @@ jobs:
run: yarn install
- name: Verify node and yarn
run: __tests__/verify-node.sh "${{ matrix.node-version }}"
shell: bash
shell: bash
28 changes: 25 additions & 3 deletions README.md
Expand Up @@ -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/yarn/pnpm dependencies
- Registering problem matchers for error output
- Configuring authentication for GPR or npm

Expand Down Expand Up @@ -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`, `yarn`, `pnpm`. The `cache` input is optional, and caching is turned off by default.

**Caching npm dependencies:**
```yaml
Expand All @@ -66,7 +66,29 @@ steps:
- run: yarn install
- run: yarn test
```
Yarn caching handles both yarn versions: 1 or 2.
Yarn caching handles both yarn versions: 1 or 2.

**Caching pnpm (v6.10+) dependencies:**
```yaml
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# NOTE: pnpm caching support requires pnpm version >= 6.10.0

steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@646cdf48217256a3d0b80361c5a50727664284f2
with:
version: 6.10.0
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'pnpm'
- run: pnpm install
- run: pnpm test
```

> At the moment, only `lock` files in the project root are supported.
Expand Down
16 changes: 13 additions & 3 deletions __tests__/cache-restore.test.ts
Expand Up @@ -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
};
Expand All @@ -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:
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand All @@ -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) => {
Expand Down Expand Up @@ -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) => {
Expand Down
55 changes: 55 additions & 0 deletions __tests__/cache-save.test.ts
@@ -1,6 +1,7 @@
import * as core from '@actions/core';
import * as cache from '@actions/cache';
import * as glob from '@actions/glob';
import fs from 'fs';
import path from 'path';

import * as utils from '../src/cache-utils';
Expand All @@ -12,6 +13,8 @@ describe('run', () => {
'b8a0bae5243251f7c07dd52d1f78ff78281dfefaded700a176261b6b54fa245b';
const npmFileHash =
'abf7c9b306a3149dcfba4673e2362755503bcceaab46f0e4e6fee0ade493e20c';
const pnpmFileHash =
'26309058093e84713f38869c50cf1cee9b08155ede874ec1b44ce3fca8c68c70';
const commonPath = '/some/random/path';
process.env['GITHUB_WORKSPACE'] = path.join(__dirname, 'data');

Expand All @@ -26,6 +29,7 @@ describe('run', () => {
let saveCacheSpy: jest.SpyInstance;
let getCommandOutputSpy: jest.SpyInstance;
let hashFilesSpy: jest.SpyInstance;
let existsSpy: jest.SpyInstance;

beforeEach(() => {
getInputSpy = jest.spyOn(core, 'getInput');
Expand Down Expand Up @@ -61,10 +65,17 @@ describe('run', () => {
}
});

existsSpy = jest.spyOn(fs, 'existsSync');
existsSpy.mockImplementation(() => true);

// utils
getCommandOutputSpy = jest.spyOn(utils, 'getCommandOutput');
});

afterEach(() => {
existsSpy.mockRestore();
});

describe('Package manager validation', () => {
it('Package manager is not provided, skip caching', async () => {
inputs['cache'] = '';
Expand Down Expand Up @@ -150,6 +161,23 @@ describe('run', () => {
);
expect(setFailedSpy).not.toHaveBeenCalled();
});

it('should not save cache for pnpm', 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', () => {
Expand Down Expand Up @@ -239,6 +267,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(() => {
Expand Down
5 changes: 5 additions & 0 deletions __tests__/cache-utils.test.ts
Expand Up @@ -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`;
Expand All @@ -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],
Expand Down