mirror of
https://code.forgejo.org/actions/setup-node.git
synced 2024-11-05 11:35:48 +00:00
Detect cached folders from multiple directories (#735)
* Add project-dir * Fix find lock file * Remove package-dir input * format & resolve conflicts * Add unit tests * build dist * Apply change request fixes * handle non-dir cache-dependency-path * bump cache version * run checks * Handle globs in cacheDependencyPath * refactor, introduce `cacheDependencyPathToProjectsDirectories` it is necessary for the next PR related yarn optimization * Changes requests * Apply fixes * review fixes * add e2e * Add unique * review updates * review updates second stage * Review fixes 3 * imporve e2e tests
This commit is contained in:
parent
698d50532e
commit
8170e22e8f
16 changed files with 3445 additions and 1496 deletions
27
.github/workflows/e2e-cache.yml
vendored
27
.github/workflows/e2e-cache.yml
vendored
|
@ -134,3 +134,30 @@ jobs:
|
||||||
- name: Verify node and yarn
|
- name: Verify node and yarn
|
||||||
run: __tests__/verify-node.sh "${{ matrix.node-version }}"
|
run: __tests__/verify-node.sh "${{ matrix.node-version }}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
yarn-subprojects:
|
||||||
|
name: Test yarn subprojects
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [12, 14, 16]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: prepare sub-projects
|
||||||
|
run: __tests__/prepare-subprojects.sh
|
||||||
|
|
||||||
|
# expect
|
||||||
|
# - no errors
|
||||||
|
# - log
|
||||||
|
# ##[debug]Cache Paths:
|
||||||
|
# ##[debug]["sub2/.yarn/cache","sub3/.yarn/cache","../../../.cache/yarn/v6"]
|
||||||
|
- name: Setup Node
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'yarn'
|
||||||
|
cache-dependency-path: |
|
||||||
|
**/*.lock
|
||||||
|
yarn.lock
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import * as fs from 'fs';
|
import fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as io from '@actions/io';
|
import * as io from '@actions/io';
|
||||||
import * as auth from '../src/authutil';
|
import * as auth from '../src/authutil';
|
||||||
|
import * as cacheUtils from '../src/cache-utils';
|
||||||
|
|
||||||
let rcFile: string;
|
let rcFile: string;
|
||||||
|
|
||||||
|
|
|
@ -32,13 +32,13 @@ describe('cache-restore', () => {
|
||||||
|
|
||||||
function findCacheFolder(command: string) {
|
function findCacheFolder(command: string) {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case utils.supportedPackageManagers.npm.getCacheFolderCommand:
|
case 'npm config get cache':
|
||||||
return npmCachePath;
|
return npmCachePath;
|
||||||
case utils.supportedPackageManagers.pnpm.getCacheFolderCommand:
|
case 'pnpm store path --silent':
|
||||||
return pnpmCachePath;
|
return pnpmCachePath;
|
||||||
case utils.supportedPackageManagers.yarn1.getCacheFolderCommand:
|
case 'yarn cache dir':
|
||||||
return yarn1CachePath;
|
return yarn1CachePath;
|
||||||
case utils.supportedPackageManagers.yarn2.getCacheFolderCommand:
|
case 'yarn config get cacheFolder':
|
||||||
return yarn2CachePath;
|
return yarn2CachePath;
|
||||||
default:
|
default:
|
||||||
return 'packge/not/found';
|
return 'packge/not/found';
|
||||||
|
@ -108,7 +108,7 @@ describe('cache-restore', () => {
|
||||||
it.each([['npm7'], ['npm6'], ['pnpm6'], ['yarn1'], ['yarn2'], ['random']])(
|
it.each([['npm7'], ['npm6'], ['pnpm6'], ['yarn1'], ['yarn2'], ['random']])(
|
||||||
'Throw an error because %s is not supported',
|
'Throw an error because %s is not supported',
|
||||||
async packageManager => {
|
async packageManager => {
|
||||||
await expect(restoreCache(packageManager)).rejects.toThrow(
|
await expect(restoreCache(packageManager, '')).rejects.toThrow(
|
||||||
`Caching for '${packageManager}' is not supported`
|
`Caching for '${packageManager}' is not supported`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -132,7 +132,7 @@ describe('cache-restore', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await restoreCache(packageManager);
|
await restoreCache(packageManager, '');
|
||||||
expect(hashFilesSpy).toHaveBeenCalled();
|
expect(hashFilesSpy).toHaveBeenCalled();
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
expect(infoSpy).toHaveBeenCalledWith(
|
||||||
`Cache restored from key: node-cache-${platform}-${packageManager}-${fileHash}`
|
`Cache restored from key: node-cache-${platform}-${packageManager}-${fileHash}`
|
||||||
|
@ -163,7 +163,7 @@ describe('cache-restore', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
restoreCacheSpy.mockImplementationOnce(() => undefined);
|
restoreCacheSpy.mockImplementationOnce(() => undefined);
|
||||||
await restoreCache(packageManager);
|
await restoreCache(packageManager, '');
|
||||||
expect(hashFilesSpy).toHaveBeenCalled();
|
expect(hashFilesSpy).toHaveBeenCalled();
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
expect(infoSpy).toHaveBeenCalledWith(
|
||||||
`${packageManager} cache is not found`
|
`${packageManager} cache is not found`
|
||||||
|
|
|
@ -107,18 +107,20 @@ describe('run', () => {
|
||||||
describe('Validate unchanged cache is not saved', () => {
|
describe('Validate unchanged cache is not saved', () => {
|
||||||
it('should not save cache for yarn1', async () => {
|
it('should not save cache for yarn1', async () => {
|
||||||
inputs['cache'] = 'yarn';
|
inputs['cache'] = 'yarn';
|
||||||
getStateSpy.mockImplementation(() => yarnFileHash);
|
getStateSpy.mockImplementation(key =>
|
||||||
getCommandOutputSpy
|
key === State.CachePrimaryKey || key === State.CacheMatchedKey
|
||||||
.mockImplementationOnce(() => '1.2.3')
|
? yarnFileHash
|
||||||
.mockImplementationOnce(() => `${commonPath}/yarn1`);
|
: key === State.CachePaths
|
||||||
|
? '["/foo/bar"]'
|
||||||
|
: 'not expected'
|
||||||
|
);
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(2);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn1`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 1.2.3');
|
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
expect(infoSpy).toHaveBeenCalledWith(
|
||||||
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
|
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
|
||||||
);
|
);
|
||||||
|
@ -127,18 +129,20 @@ describe('run', () => {
|
||||||
|
|
||||||
it('should not save cache for yarn2', async () => {
|
it('should not save cache for yarn2', async () => {
|
||||||
inputs['cache'] = 'yarn';
|
inputs['cache'] = 'yarn';
|
||||||
getStateSpy.mockImplementation(() => yarnFileHash);
|
getStateSpy.mockImplementation(key =>
|
||||||
getCommandOutputSpy
|
key === State.CachePrimaryKey || key === State.CacheMatchedKey
|
||||||
.mockImplementationOnce(() => '2.2.3')
|
? yarnFileHash
|
||||||
.mockImplementationOnce(() => `${commonPath}/yarn2`);
|
: key === State.CachePaths
|
||||||
|
? '["/foo/bar"]'
|
||||||
|
: 'not expected'
|
||||||
|
);
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(2);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn2`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 2.2.3');
|
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
expect(infoSpy).toHaveBeenCalledWith(
|
||||||
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
|
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
|
||||||
);
|
);
|
||||||
|
@ -147,35 +151,40 @@ describe('run', () => {
|
||||||
|
|
||||||
it('should not save cache for npm', async () => {
|
it('should not save cache for npm', async () => {
|
||||||
inputs['cache'] = 'npm';
|
inputs['cache'] = 'npm';
|
||||||
getStateSpy.mockImplementation(() => npmFileHash);
|
getStateSpy.mockImplementation(key =>
|
||||||
|
key === State.CachePrimaryKey || key === State.CacheMatchedKey
|
||||||
|
? yarnFileHash
|
||||||
|
: key === State.CachePaths
|
||||||
|
? '["/foo/bar"]'
|
||||||
|
: 'not expected'
|
||||||
|
);
|
||||||
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`);
|
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`);
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
|
||||||
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
|
|
||||||
);
|
|
||||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not save cache for pnpm', async () => {
|
it('should not save cache for pnpm', async () => {
|
||||||
inputs['cache'] = 'pnpm';
|
inputs['cache'] = 'pnpm';
|
||||||
getStateSpy.mockImplementation(() => pnpmFileHash);
|
getStateSpy.mockImplementation(key =>
|
||||||
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/pnpm`);
|
key === State.CachePrimaryKey || key === State.CacheMatchedKey
|
||||||
|
? yarnFileHash
|
||||||
|
: key === State.CachePaths
|
||||||
|
? '["/foo/bar"]'
|
||||||
|
: 'not expected'
|
||||||
|
);
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`pnpm path is ${commonPath}/pnpm`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(infoSpy).toHaveBeenCalledWith(
|
|
||||||
`Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.`
|
|
||||||
);
|
|
||||||
expect(setFailedSpy).not.toHaveBeenCalled();
|
expect(setFailedSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -183,24 +192,22 @@ describe('run', () => {
|
||||||
describe('action saves the cache', () => {
|
describe('action saves the cache', () => {
|
||||||
it('saves cache from yarn 1', async () => {
|
it('saves cache from yarn 1', async () => {
|
||||||
inputs['cache'] = 'yarn';
|
inputs['cache'] = 'yarn';
|
||||||
getStateSpy.mockImplementation((name: string) => {
|
getStateSpy.mockImplementation((key: string) =>
|
||||||
if (name === State.CacheMatchedKey) {
|
key === State.CacheMatchedKey
|
||||||
return yarnFileHash;
|
? yarnFileHash
|
||||||
} else {
|
: key === State.CachePrimaryKey
|
||||||
return npmFileHash;
|
? npmFileHash
|
||||||
}
|
: key === State.CachePaths
|
||||||
});
|
? '["/foo/bar"]'
|
||||||
getCommandOutputSpy
|
: 'not expected'
|
||||||
.mockImplementationOnce(() => '1.2.3')
|
);
|
||||||
.mockImplementationOnce(() => `${commonPath}/yarn1`);
|
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(2);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn1`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 1.2.3');
|
|
||||||
expect(infoSpy).not.toHaveBeenCalledWith(
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
||||||
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
|
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
|
||||||
);
|
);
|
||||||
|
@ -213,24 +220,22 @@ describe('run', () => {
|
||||||
|
|
||||||
it('saves cache from yarn 2', async () => {
|
it('saves cache from yarn 2', async () => {
|
||||||
inputs['cache'] = 'yarn';
|
inputs['cache'] = 'yarn';
|
||||||
getStateSpy.mockImplementation((name: string) => {
|
getStateSpy.mockImplementation((key: string) =>
|
||||||
if (name === State.CacheMatchedKey) {
|
key === State.CacheMatchedKey
|
||||||
return yarnFileHash;
|
? yarnFileHash
|
||||||
} else {
|
: key === State.CachePrimaryKey
|
||||||
return npmFileHash;
|
? npmFileHash
|
||||||
}
|
: key === State.CachePaths
|
||||||
});
|
? '["/foo/bar"]'
|
||||||
getCommandOutputSpy
|
: 'not expected'
|
||||||
.mockImplementationOnce(() => '2.2.3')
|
);
|
||||||
.mockImplementationOnce(() => `${commonPath}/yarn2`);
|
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(2);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`yarn path is ${commonPath}/yarn2`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith('Consumed yarn version is 2.2.3');
|
|
||||||
expect(infoSpy).not.toHaveBeenCalledWith(
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
||||||
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
|
`Cache hit occurred on the primary key ${yarnFileHash}, not saving cache.`
|
||||||
);
|
);
|
||||||
|
@ -243,21 +248,22 @@ describe('run', () => {
|
||||||
|
|
||||||
it('saves cache from npm', async () => {
|
it('saves cache from npm', async () => {
|
||||||
inputs['cache'] = 'npm';
|
inputs['cache'] = 'npm';
|
||||||
getStateSpy.mockImplementation((name: string) => {
|
getStateSpy.mockImplementation((key: string) =>
|
||||||
if (name === State.CacheMatchedKey) {
|
key === State.CacheMatchedKey
|
||||||
return npmFileHash;
|
? npmFileHash
|
||||||
} else {
|
: key === State.CachePrimaryKey
|
||||||
return yarnFileHash;
|
? yarnFileHash
|
||||||
}
|
: key === State.CachePaths
|
||||||
});
|
? '["/foo/bar"]'
|
||||||
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`);
|
: 'not expected'
|
||||||
|
);
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(infoSpy).not.toHaveBeenCalledWith(
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
||||||
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
|
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
|
||||||
);
|
);
|
||||||
|
@ -270,21 +276,22 @@ describe('run', () => {
|
||||||
|
|
||||||
it('saves cache from pnpm', async () => {
|
it('saves cache from pnpm', async () => {
|
||||||
inputs['cache'] = 'pnpm';
|
inputs['cache'] = 'pnpm';
|
||||||
getStateSpy.mockImplementation((name: string) => {
|
getStateSpy.mockImplementation((key: string) =>
|
||||||
if (name === State.CacheMatchedKey) {
|
key === State.CacheMatchedKey
|
||||||
return pnpmFileHash;
|
? pnpmFileHash
|
||||||
} else {
|
: key === State.CachePrimaryKey
|
||||||
return npmFileHash;
|
? npmFileHash
|
||||||
}
|
: key === State.CachePaths
|
||||||
});
|
? '["/foo/bar"]'
|
||||||
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/pnpm`);
|
: 'not expected'
|
||||||
|
);
|
||||||
|
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`pnpm path is ${commonPath}/pnpm`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(infoSpy).not.toHaveBeenCalledWith(
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
||||||
`Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.`
|
`Cache hit occurred on the primary key ${pnpmFileHash}, not saving cache.`
|
||||||
);
|
);
|
||||||
|
@ -297,14 +304,15 @@ describe('run', () => {
|
||||||
|
|
||||||
it('save with -1 cacheId , should not fail workflow', async () => {
|
it('save with -1 cacheId , should not fail workflow', async () => {
|
||||||
inputs['cache'] = 'npm';
|
inputs['cache'] = 'npm';
|
||||||
getStateSpy.mockImplementation((name: string) => {
|
getStateSpy.mockImplementation((key: string) =>
|
||||||
if (name === State.CacheMatchedKey) {
|
key === State.CacheMatchedKey
|
||||||
return npmFileHash;
|
? npmFileHash
|
||||||
} else {
|
: key === State.CachePrimaryKey
|
||||||
return yarnFileHash;
|
? yarnFileHash
|
||||||
}
|
: key === State.CachePaths
|
||||||
});
|
? '["/foo/bar"]'
|
||||||
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`);
|
: 'not expected'
|
||||||
|
);
|
||||||
saveCacheSpy.mockImplementation(() => {
|
saveCacheSpy.mockImplementation(() => {
|
||||||
return -1;
|
return -1;
|
||||||
});
|
});
|
||||||
|
@ -312,9 +320,9 @@ describe('run', () => {
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(infoSpy).not.toHaveBeenCalledWith(
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
||||||
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
|
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
|
||||||
);
|
);
|
||||||
|
@ -327,14 +335,15 @@ describe('run', () => {
|
||||||
|
|
||||||
it('saves with error from toolkit, should fail workflow', async () => {
|
it('saves with error from toolkit, should fail workflow', async () => {
|
||||||
inputs['cache'] = 'npm';
|
inputs['cache'] = 'npm';
|
||||||
getStateSpy.mockImplementation((name: string) => {
|
getStateSpy.mockImplementation((key: string) =>
|
||||||
if (name === State.CacheMatchedKey) {
|
key === State.CacheMatchedKey
|
||||||
return npmFileHash;
|
? npmFileHash
|
||||||
} else {
|
: key === State.CachePrimaryKey
|
||||||
return yarnFileHash;
|
? yarnFileHash
|
||||||
}
|
: key === State.CachePaths
|
||||||
});
|
? '["/foo/bar"]'
|
||||||
getCommandOutputSpy.mockImplementationOnce(() => `${commonPath}/npm`);
|
: 'not expected'
|
||||||
|
);
|
||||||
saveCacheSpy.mockImplementation(() => {
|
saveCacheSpy.mockImplementation(() => {
|
||||||
throw new cache.ValidationError('Validation failed');
|
throw new cache.ValidationError('Validation failed');
|
||||||
});
|
});
|
||||||
|
@ -342,9 +351,9 @@ describe('run', () => {
|
||||||
await run();
|
await run();
|
||||||
|
|
||||||
expect(getInputSpy).toHaveBeenCalled();
|
expect(getInputSpy).toHaveBeenCalled();
|
||||||
expect(getStateSpy).toHaveBeenCalledTimes(2);
|
expect(getStateSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(debugSpy).toHaveBeenCalledWith(`npm path is ${commonPath}/npm`);
|
expect(debugSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(infoSpy).not.toHaveBeenCalledWith(
|
expect(infoSpy).not.toHaveBeenCalledWith(
|
||||||
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
|
`Cache hit occurred on the primary key ${npmFileHash}, not saving cache.`
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,17 @@ import * as core from '@actions/core';
|
||||||
import * as cache from '@actions/cache';
|
import * as cache from '@actions/cache';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import * as utils from '../src/cache-utils';
|
import * as utils from '../src/cache-utils';
|
||||||
import {PackageManagerInfo, isCacheFeatureAvailable} from '../src/cache-utils';
|
import {
|
||||||
|
PackageManagerInfo,
|
||||||
|
isCacheFeatureAvailable,
|
||||||
|
supportedPackageManagers,
|
||||||
|
getCommandOutput
|
||||||
|
} from '../src/cache-utils';
|
||||||
|
import fs from 'fs';
|
||||||
|
import * as cacheUtils from '../src/cache-utils';
|
||||||
|
import * as glob from '@actions/glob';
|
||||||
|
import {Globber} from '@actions/glob';
|
||||||
|
import {MockGlobber} from './mock/glob-mock';
|
||||||
|
|
||||||
describe('cache-utils', () => {
|
describe('cache-utils', () => {
|
||||||
const versionYarn1 = '1.2.3';
|
const versionYarn1 = '1.2.3';
|
||||||
|
@ -30,7 +40,7 @@ describe('cache-utils', () => {
|
||||||
it.each<[string, PackageManagerInfo | null]>([
|
it.each<[string, PackageManagerInfo | null]>([
|
||||||
['npm', utils.supportedPackageManagers.npm],
|
['npm', utils.supportedPackageManagers.npm],
|
||||||
['pnpm', utils.supportedPackageManagers.pnpm],
|
['pnpm', utils.supportedPackageManagers.pnpm],
|
||||||
['yarn', utils.supportedPackageManagers.yarn1],
|
['yarn', utils.supportedPackageManagers.yarn],
|
||||||
['yarn1', null],
|
['yarn1', null],
|
||||||
['yarn2', null],
|
['yarn2', null],
|
||||||
['npm7', null]
|
['npm7', null]
|
||||||
|
@ -72,4 +82,261 @@ describe('cache-utils', () => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getCacheDirectoriesPaths', () => {
|
||||||
|
let existsSpy: jest.SpyInstance;
|
||||||
|
let lstatSpy: jest.SpyInstance;
|
||||||
|
let globCreateSpy: jest.SpyInstance;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
existsSpy = jest.spyOn(fs, 'existsSync');
|
||||||
|
existsSpy.mockImplementation(() => true);
|
||||||
|
|
||||||
|
lstatSpy = jest.spyOn(fs, 'lstatSync');
|
||||||
|
lstatSpy.mockImplementation(arg => ({
|
||||||
|
isDirectory: () => true
|
||||||
|
}));
|
||||||
|
|
||||||
|
globCreateSpy = jest.spyOn(glob, 'create');
|
||||||
|
|
||||||
|
globCreateSpy.mockImplementation(
|
||||||
|
(pattern: string): Promise<Globber> =>
|
||||||
|
MockGlobber.create(['/foo', '/bar'])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
existsSpy.mockRestore();
|
||||||
|
lstatSpy.mockRestore();
|
||||||
|
globCreateSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[supportedPackageManagers.npm, ''],
|
||||||
|
[supportedPackageManagers.npm, '/dir/file.lock'],
|
||||||
|
[supportedPackageManagers.npm, '/**/file.lock'],
|
||||||
|
[supportedPackageManagers.pnpm, ''],
|
||||||
|
[supportedPackageManagers.pnpm, '/dir/file.lock'],
|
||||||
|
[supportedPackageManagers.pnpm, '/**/file.lock']
|
||||||
|
])(
|
||||||
|
'getCacheDirectoriesPaths should return one dir for non yarn',
|
||||||
|
async (packageManagerInfo, cacheDependency) => {
|
||||||
|
getCommandOutputSpy.mockImplementation(() => 'foo');
|
||||||
|
|
||||||
|
const dirs = await cacheUtils.getCacheDirectories(
|
||||||
|
packageManagerInfo,
|
||||||
|
cacheDependency
|
||||||
|
);
|
||||||
|
expect(dirs).toEqual(['foo']);
|
||||||
|
// to do not call for a version
|
||||||
|
// call once for get cache folder
|
||||||
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(1);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('getCacheDirectoriesPaths should return one dir for yarn without cacheDependency', async () => {
|
||||||
|
getCommandOutputSpy.mockImplementation(() => 'foo');
|
||||||
|
|
||||||
|
const dirs = await cacheUtils.getCacheDirectories(
|
||||||
|
supportedPackageManagers.yarn,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
expect(dirs).toEqual(['foo']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[supportedPackageManagers.npm, ''],
|
||||||
|
[supportedPackageManagers.npm, '/dir/file.lock'],
|
||||||
|
[supportedPackageManagers.npm, '/**/file.lock'],
|
||||||
|
[supportedPackageManagers.pnpm, ''],
|
||||||
|
[supportedPackageManagers.pnpm, '/dir/file.lock'],
|
||||||
|
[supportedPackageManagers.pnpm, '/**/file.lock'],
|
||||||
|
[supportedPackageManagers.yarn, ''],
|
||||||
|
[supportedPackageManagers.yarn, '/dir/file.lock'],
|
||||||
|
[supportedPackageManagers.yarn, '/**/file.lock']
|
||||||
|
])(
|
||||||
|
'getCacheDirectoriesPaths should throw for getCommandOutput returning empty',
|
||||||
|
async (packageManagerInfo, cacheDependency) => {
|
||||||
|
getCommandOutputSpy.mockImplementation((command: string) =>
|
||||||
|
// return empty string to indicate getCacheFolderPath failed
|
||||||
|
// --version still works
|
||||||
|
command.includes('version') ? '1.' : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
cacheUtils.getCacheDirectories(packageManagerInfo, cacheDependency)
|
||||||
|
).rejects.toThrow(); //'Could not get cache folder path for /dir');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[supportedPackageManagers.yarn, '/dir/file.lock'],
|
||||||
|
[supportedPackageManagers.yarn, '/**/file.lock']
|
||||||
|
])(
|
||||||
|
'getCacheDirectoriesPaths should nothrow in case of having not directories',
|
||||||
|
async (packageManagerInfo, cacheDependency) => {
|
||||||
|
lstatSpy.mockImplementation(arg => ({
|
||||||
|
isDirectory: () => false
|
||||||
|
}));
|
||||||
|
|
||||||
|
await cacheUtils.getCacheDirectories(
|
||||||
|
packageManagerInfo,
|
||||||
|
cacheDependency
|
||||||
|
);
|
||||||
|
expect(warningSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(warningSpy).toHaveBeenCalledWith(
|
||||||
|
`No existing directories found containing cache-dependency-path="${cacheDependency}"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(['1.1.1', '2.2.2'])(
|
||||||
|
'getCacheDirectoriesPaths yarn v%s should return one dir without cacheDependency',
|
||||||
|
async version => {
|
||||||
|
getCommandOutputSpy.mockImplementationOnce(() => version);
|
||||||
|
getCommandOutputSpy.mockImplementationOnce(() => `foo${version}`);
|
||||||
|
|
||||||
|
const dirs = await cacheUtils.getCacheDirectories(
|
||||||
|
supportedPackageManagers.yarn,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
expect(dirs).toEqual([`foo${version}`]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(['1.1.1', '2.2.2'])(
|
||||||
|
'getCacheDirectoriesPaths yarn v%s should return 2 dirs with globbed cacheDependency',
|
||||||
|
async version => {
|
||||||
|
let dirNo = 1;
|
||||||
|
getCommandOutputSpy.mockImplementation((command: string) =>
|
||||||
|
command.includes('version') ? version : `file_${version}_${dirNo++}`
|
||||||
|
);
|
||||||
|
globCreateSpy.mockImplementation(
|
||||||
|
(pattern: string): Promise<Globber> =>
|
||||||
|
MockGlobber.create(['/tmp/dir1/file', '/tmp/dir2/file'])
|
||||||
|
);
|
||||||
|
|
||||||
|
const dirs = await cacheUtils.getCacheDirectories(
|
||||||
|
supportedPackageManagers.yarn,
|
||||||
|
'/tmp/**/file'
|
||||||
|
);
|
||||||
|
expect(dirs).toEqual([`file_${version}_1`, `file_${version}_2`]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(['1.1.1', '2.2.2'])(
|
||||||
|
'getCacheDirectoriesPaths yarn v%s should return 2 dirs with globbed cacheDependency expanding to duplicates',
|
||||||
|
async version => {
|
||||||
|
let dirNo = 1;
|
||||||
|
getCommandOutputSpy.mockImplementation((command: string) =>
|
||||||
|
command.includes('version') ? version : `file_${version}_${dirNo++}`
|
||||||
|
);
|
||||||
|
globCreateSpy.mockImplementation(
|
||||||
|
(pattern: string): Promise<Globber> =>
|
||||||
|
MockGlobber.create([
|
||||||
|
'/tmp/dir1/file',
|
||||||
|
'/tmp/dir2/file',
|
||||||
|
'/tmp/dir1/file'
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const dirs = await cacheUtils.getCacheDirectories(
|
||||||
|
supportedPackageManagers.yarn,
|
||||||
|
'/tmp/**/file'
|
||||||
|
);
|
||||||
|
expect(dirs).toEqual([`file_${version}_1`, `file_${version}_2`]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(['1.1.1', '2.2.2'])(
|
||||||
|
'getCacheDirectoriesPaths yarn v%s should return 2 uniq dirs despite duplicate cache directories',
|
||||||
|
async version => {
|
||||||
|
let dirNo = 1;
|
||||||
|
getCommandOutputSpy.mockImplementation((command: string) =>
|
||||||
|
command.includes('version')
|
||||||
|
? version
|
||||||
|
: `file_${version}_${dirNo++ % 2}`
|
||||||
|
);
|
||||||
|
globCreateSpy.mockImplementation(
|
||||||
|
(pattern: string): Promise<Globber> =>
|
||||||
|
MockGlobber.create([
|
||||||
|
'/tmp/dir1/file',
|
||||||
|
'/tmp/dir2/file',
|
||||||
|
'/tmp/dir3/file'
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const dirs = await cacheUtils.getCacheDirectories(
|
||||||
|
supportedPackageManagers.yarn,
|
||||||
|
'/tmp/**/file'
|
||||||
|
);
|
||||||
|
expect(dirs).toEqual([`file_${version}_1`, `file_${version}_0`]);
|
||||||
|
expect(getCommandOutputSpy).toHaveBeenCalledTimes(6);
|
||||||
|
expect(getCommandOutputSpy).toHaveBeenCalledWith(
|
||||||
|
'yarn --version',
|
||||||
|
'/tmp/dir1'
|
||||||
|
);
|
||||||
|
expect(getCommandOutputSpy).toHaveBeenCalledWith(
|
||||||
|
'yarn --version',
|
||||||
|
'/tmp/dir2'
|
||||||
|
);
|
||||||
|
expect(getCommandOutputSpy).toHaveBeenCalledWith(
|
||||||
|
'yarn --version',
|
||||||
|
'/tmp/dir3'
|
||||||
|
);
|
||||||
|
expect(getCommandOutputSpy).toHaveBeenCalledWith(
|
||||||
|
version.startsWith('1.')
|
||||||
|
? 'yarn cache dir'
|
||||||
|
: 'yarn config get cacheFolder',
|
||||||
|
'/tmp/dir1'
|
||||||
|
);
|
||||||
|
expect(getCommandOutputSpy).toHaveBeenCalledWith(
|
||||||
|
version.startsWith('1.')
|
||||||
|
? 'yarn cache dir'
|
||||||
|
: 'yarn config get cacheFolder',
|
||||||
|
'/tmp/dir2'
|
||||||
|
);
|
||||||
|
expect(getCommandOutputSpy).toHaveBeenCalledWith(
|
||||||
|
version.startsWith('1.')
|
||||||
|
? 'yarn cache dir'
|
||||||
|
: 'yarn config get cacheFolder',
|
||||||
|
'/tmp/dir3'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(['1.1.1', '2.2.2'])(
|
||||||
|
'getCacheDirectoriesPaths yarn v%s should return 4 dirs with multiple globs',
|
||||||
|
async version => {
|
||||||
|
// simulate wrong indents
|
||||||
|
const cacheDependencyPath = `/tmp/dir1/file
|
||||||
|
/tmp/dir2/file
|
||||||
|
/tmp/**/file
|
||||||
|
`;
|
||||||
|
globCreateSpy.mockImplementation(
|
||||||
|
(pattern: string): Promise<Globber> =>
|
||||||
|
MockGlobber.create([
|
||||||
|
'/tmp/dir1/file',
|
||||||
|
'/tmp/dir2/file',
|
||||||
|
'/tmp/dir3/file',
|
||||||
|
'/tmp/dir4/file'
|
||||||
|
])
|
||||||
|
);
|
||||||
|
let dirNo = 1;
|
||||||
|
getCommandOutputSpy.mockImplementation((command: string) =>
|
||||||
|
command.includes('version') ? version : `file_${version}_${dirNo++}`
|
||||||
|
);
|
||||||
|
const dirs = await cacheUtils.getCacheDirectories(
|
||||||
|
supportedPackageManagers.yarn,
|
||||||
|
cacheDependencyPath
|
||||||
|
);
|
||||||
|
expect(dirs).toEqual([
|
||||||
|
`file_${version}_1`,
|
||||||
|
`file_${version}_2`,
|
||||||
|
`file_${version}_3`,
|
||||||
|
`file_${version}_4`
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
18
__tests__/mock/glob-mock.test.ts
Normal file
18
__tests__/mock/glob-mock.test.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {MockGlobber} from './glob-mock';
|
||||||
|
|
||||||
|
describe('mocked globber tests', () => {
|
||||||
|
it('globber should return generator', async () => {
|
||||||
|
const globber = new MockGlobber(['aaa', 'bbb', 'ccc']);
|
||||||
|
const generator = globber.globGenerator();
|
||||||
|
const result: string[] = [];
|
||||||
|
for await (const itemPath of generator) {
|
||||||
|
result.push(itemPath);
|
||||||
|
}
|
||||||
|
expect(result).toEqual(['aaa', 'bbb', 'ccc']);
|
||||||
|
});
|
||||||
|
it('globber should return glob', async () => {
|
||||||
|
const globber = new MockGlobber(['aaa', 'bbb', 'ccc']);
|
||||||
|
const result: string[] = await globber.glob();
|
||||||
|
expect(result).toEqual(['aaa', 'bbb', 'ccc']);
|
||||||
|
});
|
||||||
|
});
|
29
__tests__/mock/glob-mock.ts
Normal file
29
__tests__/mock/glob-mock.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import {Globber} from '@actions/glob';
|
||||||
|
|
||||||
|
export class MockGlobber implements Globber {
|
||||||
|
private readonly expected: string[];
|
||||||
|
constructor(expected: string[]) {
|
||||||
|
this.expected = expected;
|
||||||
|
}
|
||||||
|
getSearchPaths(): string[] {
|
||||||
|
return this.expected.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
async glob(): Promise<string[]> {
|
||||||
|
const result: string[] = [];
|
||||||
|
for await (const itemPath of this.globGenerator()) {
|
||||||
|
result.push(itemPath);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async *globGenerator(): AsyncGenerator<string, void> {
|
||||||
|
for (const e of this.expected) {
|
||||||
|
yield e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async create(expected: string[]): Promise<MockGlobber> {
|
||||||
|
return new MockGlobber(expected);
|
||||||
|
}
|
||||||
|
}
|
48
__tests__/prepare-subprojects.sh
Executable file
48
__tests__/prepare-subprojects.sh
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
#!/bin/sh -e
|
||||||
|
export YARN_ENABLE_IMMUTABLE_INSTALLS=false
|
||||||
|
rm package.json
|
||||||
|
rm package-lock.json
|
||||||
|
echo "create yarn2 project in the sub2"
|
||||||
|
mkdir sub2
|
||||||
|
cd sub2
|
||||||
|
cat <<EOT >package.json
|
||||||
|
{
|
||||||
|
"name": "subproject",
|
||||||
|
"dependencies": {
|
||||||
|
"random": "^3.0.6",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
yarn set version 2.4.3
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
echo "create yarn3 project in the sub3"
|
||||||
|
cd ..
|
||||||
|
mkdir sub3
|
||||||
|
cd sub3
|
||||||
|
cat <<EOT >package.json
|
||||||
|
{
|
||||||
|
"name": "subproject",
|
||||||
|
"dependencies": {
|
||||||
|
"random": "^3.0.6",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
yarn set version 3.5.1
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
echo "create yarn1 project in the root"
|
||||||
|
cd ..
|
||||||
|
cat <<EOT >package.json
|
||||||
|
{
|
||||||
|
"name": "subproject",
|
||||||
|
"dependencies": {
|
||||||
|
"random": "^3.0.6",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
yarn set version 1.22.19
|
||||||
|
yarn install
|
|
@ -25,7 +25,7 @@ inputs:
|
||||||
description: 'Used to specify a package manager for caching in the default directory. Supported values: npm, yarn, pnpm.'
|
description: 'Used to specify a package manager for caching in the default directory. Supported values: npm, yarn, pnpm.'
|
||||||
cache-dependency-path:
|
cache-dependency-path:
|
||||||
description: 'Used to specify the path to a dependency file: package-lock.json, yarn.lock, etc. Supports wildcards or a list of file names for caching multiple dependencies.'
|
description: 'Used to specify the path to a dependency file: package-lock.json, yarn.lock, etc. Supports wildcards or a list of file names for caching multiple dependencies.'
|
||||||
# TODO: add input to control forcing to pull from cloud or dist.
|
# 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
|
# escape valve for someone having issues or needing the absolute latest which isn't cached yet
|
||||||
outputs:
|
outputs:
|
||||||
cache-hit:
|
cache-hit:
|
||||||
|
|
1792
dist/cache-save/index.js
vendored
1792
dist/cache-save/index.js
vendored
File diff suppressed because it is too large
Load diff
2289
dist/setup/index.js
vendored
2289
dist/setup/index.js
vendored
File diff suppressed because it is too large
Load diff
|
@ -6,14 +6,14 @@ import fs from 'fs';
|
||||||
|
|
||||||
import {State} from './constants';
|
import {State} from './constants';
|
||||||
import {
|
import {
|
||||||
getCacheDirectoryPath,
|
getCacheDirectories,
|
||||||
getPackageManagerInfo,
|
getPackageManagerInfo,
|
||||||
PackageManagerInfo
|
PackageManagerInfo
|
||||||
} from './cache-utils';
|
} from './cache-utils';
|
||||||
|
|
||||||
export const restoreCache = async (
|
export const restoreCache = async (
|
||||||
packageManager: string,
|
packageManager: string,
|
||||||
cacheDependencyPath?: string
|
cacheDependencyPath: string
|
||||||
) => {
|
) => {
|
||||||
const packageManagerInfo = await getPackageManagerInfo(packageManager);
|
const packageManagerInfo = await getPackageManagerInfo(packageManager);
|
||||||
if (!packageManagerInfo) {
|
if (!packageManagerInfo) {
|
||||||
|
@ -21,10 +21,11 @@ export const restoreCache = async (
|
||||||
}
|
}
|
||||||
const platform = process.env.RUNNER_OS;
|
const platform = process.env.RUNNER_OS;
|
||||||
|
|
||||||
const cachePath = await getCacheDirectoryPath(
|
const cachePaths = await getCacheDirectories(
|
||||||
packageManagerInfo,
|
packageManagerInfo,
|
||||||
packageManager
|
cacheDependencyPath
|
||||||
);
|
);
|
||||||
|
core.saveState(State.CachePaths, cachePaths);
|
||||||
const lockFilePath = cacheDependencyPath
|
const lockFilePath = cacheDependencyPath
|
||||||
? cacheDependencyPath
|
? cacheDependencyPath
|
||||||
: findLockFile(packageManagerInfo);
|
: findLockFile(packageManagerInfo);
|
||||||
|
@ -41,7 +42,7 @@ export const restoreCache = async (
|
||||||
|
|
||||||
core.saveState(State.CachePrimaryKey, primaryKey);
|
core.saveState(State.CachePrimaryKey, primaryKey);
|
||||||
|
|
||||||
const cacheKey = await cache.restoreCache([cachePath], primaryKey);
|
const cacheKey = await cache.restoreCache(cachePaths, primaryKey);
|
||||||
core.setOutput('cache-hit', Boolean(cacheKey));
|
core.setOutput('cache-hit', Boolean(cacheKey));
|
||||||
|
|
||||||
if (!cacheKey) {
|
if (!cacheKey) {
|
||||||
|
@ -56,6 +57,7 @@ export const restoreCache = async (
|
||||||
const findLockFile = (packageManager: PackageManagerInfo) => {
|
const findLockFile = (packageManager: PackageManagerInfo) => {
|
||||||
const lockFiles = packageManager.lockFilePatterns;
|
const lockFiles = packageManager.lockFilePatterns;
|
||||||
const workspace = process.env.GITHUB_WORKSPACE!;
|
const workspace = process.env.GITHUB_WORKSPACE!;
|
||||||
|
|
||||||
const rootContent = fs.readdirSync(workspace);
|
const rootContent = fs.readdirSync(workspace);
|
||||||
|
|
||||||
const lockFile = lockFiles.find(item => rootContent.includes(item));
|
const lockFile = lockFiles.find(item => rootContent.includes(item));
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as cache from '@actions/cache';
|
import * as cache from '@actions/cache';
|
||||||
import fs from 'fs';
|
|
||||||
import {State} from './constants';
|
import {State} from './constants';
|
||||||
import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils';
|
import {getPackageManagerInfo} from './cache-utils';
|
||||||
|
|
||||||
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
||||||
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
||||||
|
@ -24,6 +23,7 @@ export async function run() {
|
||||||
const cachePackages = async (packageManager: string) => {
|
const cachePackages = async (packageManager: string) => {
|
||||||
const state = core.getState(State.CacheMatchedKey);
|
const state = core.getState(State.CacheMatchedKey);
|
||||||
const primaryKey = core.getState(State.CachePrimaryKey);
|
const primaryKey = core.getState(State.CachePrimaryKey);
|
||||||
|
const cachePaths = JSON.parse(core.getState(State.CachePaths) || '[]');
|
||||||
|
|
||||||
const packageManagerInfo = await getPackageManagerInfo(packageManager);
|
const packageManagerInfo = await getPackageManagerInfo(packageManager);
|
||||||
if (!packageManagerInfo) {
|
if (!packageManagerInfo) {
|
||||||
|
@ -31,14 +31,12 @@ const cachePackages = async (packageManager: string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachePath = await getCacheDirectoryPath(
|
if (cachePaths.length === 0) {
|
||||||
packageManagerInfo,
|
// TODO: core.getInput has a bug - it can return undefined despite its definition (tests only?)
|
||||||
packageManager
|
// export declare function getInput(name: string, options?: InputOptions): string;
|
||||||
);
|
const cacheDependencyPath = core.getInput('cache-dependency-path') || '';
|
||||||
|
|
||||||
if (!fs.existsSync(cachePath)) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cache folder path is retrieved for ${packageManager} but doesn't exist on disk: ${cachePath}`
|
`Cache folder paths are not retrieved for ${packageManager} with cache-dependency-path = ${cacheDependencyPath}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +47,7 @@ const cachePackages = async (packageManager: string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheId = await cache.saveCache([cachePath], primaryKey);
|
const cacheId = await cache.saveCache(cachePaths, primaryKey);
|
||||||
if (cacheId == -1) {
|
if (cacheId == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,79 @@
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as exec from '@actions/exec';
|
import * as exec from '@actions/exec';
|
||||||
import * as cache from '@actions/cache';
|
import * as cache from '@actions/cache';
|
||||||
|
import * as glob from '@actions/glob';
|
||||||
type SupportedPackageManagers = {
|
import path from 'path';
|
||||||
[prop: string]: PackageManagerInfo;
|
import fs from 'fs';
|
||||||
};
|
import {unique} from './util';
|
||||||
|
|
||||||
export interface PackageManagerInfo {
|
export interface PackageManagerInfo {
|
||||||
|
name: string;
|
||||||
lockFilePatterns: Array<string>;
|
lockFilePatterns: Array<string>;
|
||||||
getCacheFolderCommand: string;
|
getCacheFolderPath: (projectDir?: string) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SupportedPackageManagers {
|
||||||
|
npm: PackageManagerInfo;
|
||||||
|
pnpm: PackageManagerInfo;
|
||||||
|
yarn: PackageManagerInfo;
|
||||||
|
}
|
||||||
export const supportedPackageManagers: SupportedPackageManagers = {
|
export const supportedPackageManagers: SupportedPackageManagers = {
|
||||||
npm: {
|
npm: {
|
||||||
|
name: 'npm',
|
||||||
lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'],
|
lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'],
|
||||||
getCacheFolderCommand: 'npm config get cache'
|
getCacheFolderPath: () =>
|
||||||
|
getCommandOutputNotEmpty(
|
||||||
|
'npm config get cache',
|
||||||
|
'Could not get npm cache folder path'
|
||||||
|
)
|
||||||
},
|
},
|
||||||
pnpm: {
|
pnpm: {
|
||||||
|
name: 'pnpm',
|
||||||
lockFilePatterns: ['pnpm-lock.yaml'],
|
lockFilePatterns: ['pnpm-lock.yaml'],
|
||||||
getCacheFolderCommand: 'pnpm store path --silent'
|
getCacheFolderPath: () =>
|
||||||
|
getCommandOutputNotEmpty(
|
||||||
|
'pnpm store path --silent',
|
||||||
|
'Could not get pnpm cache folder path'
|
||||||
|
)
|
||||||
},
|
},
|
||||||
yarn1: {
|
yarn: {
|
||||||
|
name: 'yarn',
|
||||||
lockFilePatterns: ['yarn.lock'],
|
lockFilePatterns: ['yarn.lock'],
|
||||||
getCacheFolderCommand: 'yarn cache dir'
|
getCacheFolderPath: async projectDir => {
|
||||||
},
|
const yarnVersion = await getCommandOutputNotEmpty(
|
||||||
yarn2: {
|
`yarn --version`,
|
||||||
lockFilePatterns: ['yarn.lock'],
|
'Could not retrieve version of yarn',
|
||||||
getCacheFolderCommand: 'yarn config get cacheFolder'
|
projectDir
|
||||||
|
);
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
`Consumed yarn version is ${yarnVersion} (working dir: "${
|
||||||
|
projectDir || ''
|
||||||
|
}")`
|
||||||
|
);
|
||||||
|
|
||||||
|
const stdOut = yarnVersion.startsWith('1.')
|
||||||
|
? await getCommandOutput('yarn cache dir', projectDir)
|
||||||
|
: await getCommandOutput('yarn config get cacheFolder', projectDir);
|
||||||
|
|
||||||
|
if (!stdOut) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not get yarn cache folder path for ${projectDir}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return stdOut;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommandOutput = async (toolCommand: string) => {
|
export const getCommandOutput = async (
|
||||||
|
toolCommand: string,
|
||||||
|
cwd?: string
|
||||||
|
): Promise<string> => {
|
||||||
let {stdout, stderr, exitCode} = await exec.getExecOutput(
|
let {stdout, stderr, exitCode} = await exec.getExecOutput(
|
||||||
toolCommand,
|
toolCommand,
|
||||||
undefined,
|
undefined,
|
||||||
{ignoreReturnCode: true}
|
{ignoreReturnCode: true, ...(cwd && {cwd})}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (exitCode) {
|
if (exitCode) {
|
||||||
|
@ -47,16 +86,15 @@ export const getCommandOutput = async (toolCommand: string) => {
|
||||||
return stdout.trim();
|
return stdout.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPackageManagerVersion = async (
|
export const getCommandOutputNotEmpty = async (
|
||||||
packageManager: string,
|
toolCommand: string,
|
||||||
command: string
|
error: string,
|
||||||
) => {
|
cwd?: string
|
||||||
const stdOut = await getCommandOutput(`${packageManager} ${command}`);
|
): Promise<string> => {
|
||||||
|
const stdOut = getCommandOutput(toolCommand, cwd);
|
||||||
if (!stdOut) {
|
if (!stdOut) {
|
||||||
throw new Error(`Could not retrieve version of ${packageManager}`);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stdOut;
|
return stdOut;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,35 +104,102 @@ export const getPackageManagerInfo = async (packageManager: string) => {
|
||||||
} else if (packageManager === 'pnpm') {
|
} else if (packageManager === 'pnpm') {
|
||||||
return supportedPackageManagers.pnpm;
|
return supportedPackageManagers.pnpm;
|
||||||
} else if (packageManager === 'yarn') {
|
} else if (packageManager === 'yarn') {
|
||||||
const yarnVersion = await getPackageManagerVersion('yarn', '--version');
|
return supportedPackageManagers.yarn;
|
||||||
|
|
||||||
core.debug(`Consumed yarn version is ${yarnVersion}`);
|
|
||||||
|
|
||||||
if (yarnVersion.startsWith('1.')) {
|
|
||||||
return supportedPackageManagers.yarn1;
|
|
||||||
} else {
|
|
||||||
return supportedPackageManagers.yarn2;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCacheDirectoryPath = async (
|
/**
|
||||||
|
* Expands (converts) the string input `cache-dependency-path` to list of directories that
|
||||||
|
* may be project roots
|
||||||
|
* @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 list of directories and possible
|
||||||
|
*/
|
||||||
|
const getProjectDirectoriesFromCacheDependencyPath = async (
|
||||||
|
cacheDependencyPath: string
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const globber = await glob.create(cacheDependencyPath);
|
||||||
|
const cacheDependenciesPaths = await globber.glob();
|
||||||
|
|
||||||
|
const existingDirectories: string[] = cacheDependenciesPaths
|
||||||
|
.map(path.dirname)
|
||||||
|
.filter(unique())
|
||||||
|
.filter(directory => fs.lstatSync(directory).isDirectory());
|
||||||
|
|
||||||
|
if (!existingDirectories.length)
|
||||||
|
core.warning(
|
||||||
|
`No existing directories found containing cache-dependency-path="${cacheDependencyPath}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
return existingDirectories;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the cache directories configured for the repo if cache-dependency-path is not empty
|
||||||
|
* @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM
|
||||||
|
* @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 list of files on which the cache depends
|
||||||
|
*/
|
||||||
|
const getCacheDirectoriesFromCacheDependencyPath = async (
|
||||||
packageManagerInfo: PackageManagerInfo,
|
packageManagerInfo: PackageManagerInfo,
|
||||||
packageManager: string
|
cacheDependencyPath: string
|
||||||
) => {
|
): Promise<string[]> => {
|
||||||
const stdOut = await getCommandOutput(
|
const projectDirectories = await getProjectDirectoriesFromCacheDependencyPath(
|
||||||
packageManagerInfo.getCacheFolderCommand
|
cacheDependencyPath
|
||||||
);
|
);
|
||||||
|
const cacheFoldersPaths = await Promise.all(
|
||||||
|
projectDirectories.map(async projectDirectory => {
|
||||||
|
const cacheFolderPath =
|
||||||
|
packageManagerInfo.getCacheFolderPath(projectDirectory);
|
||||||
|
core.debug(
|
||||||
|
`${packageManagerInfo.name}'s cache folder "${cacheFolderPath}" configured for the directory "${projectDirectory}"`
|
||||||
|
);
|
||||||
|
return cacheFolderPath;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// uniq in order to do not cache the same directories twice
|
||||||
|
return cacheFoldersPaths.filter(unique());
|
||||||
|
};
|
||||||
|
|
||||||
if (!stdOut) {
|
/**
|
||||||
throw new Error(`Could not get cache folder path for ${packageManager}`);
|
* Finds the cache directories configured for the repo ignoring cache-dependency-path
|
||||||
|
* @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM
|
||||||
|
* @return list of files on which the cache depends
|
||||||
|
*/
|
||||||
|
const getCacheDirectoriesForRootProject = async (
|
||||||
|
packageManagerInfo: PackageManagerInfo
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const cacheFolderPath = await packageManagerInfo.getCacheFolderPath();
|
||||||
|
core.debug(
|
||||||
|
`${packageManagerInfo.name}'s cache folder "${cacheFolderPath}" configured for the root directory`
|
||||||
|
);
|
||||||
|
return [cacheFolderPath];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function to find the cache directories configured for the repo
|
||||||
|
* currently it handles only the case of PM=yarn && cacheDependencyPath is not empty
|
||||||
|
* @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM
|
||||||
|
* @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 list of files on which the cache depends
|
||||||
|
*/
|
||||||
|
export const getCacheDirectories = async (
|
||||||
|
packageManagerInfo: PackageManagerInfo,
|
||||||
|
cacheDependencyPath: string
|
||||||
|
): Promise<string[]> => {
|
||||||
|
// For yarn, if cacheDependencyPath is set, ask information about cache folders in each project
|
||||||
|
// folder satisfied by cacheDependencyPath https://github.com/actions/setup-node/issues/488
|
||||||
|
if (packageManagerInfo.name === 'yarn' && cacheDependencyPath) {
|
||||||
|
return getCacheDirectoriesFromCacheDependencyPath(
|
||||||
|
packageManagerInfo,
|
||||||
|
cacheDependencyPath
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return getCacheDirectoriesForRootProject(packageManagerInfo);
|
||||||
core.debug(`${packageManager} path is ${stdOut}`);
|
|
||||||
|
|
||||||
return stdOut.trim();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isGhes(): boolean {
|
export function isGhes(): boolean {
|
||||||
|
|
|
@ -6,7 +6,8 @@ export enum LockType {
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
CachePrimaryKey = 'CACHE_KEY',
|
CachePrimaryKey = 'CACHE_KEY',
|
||||||
CacheMatchedKey = 'CACHE_RESULT'
|
CacheMatchedKey = 'CACHE_RESULT',
|
||||||
|
CachePaths = 'CACHE_PATHS'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Outputs {
|
export enum Outputs {
|
||||||
|
|
|
@ -61,3 +61,12 @@ async function getToolVersion(tool: string, options: string[]) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const unique = () => {
|
||||||
|
const encountered = new Set();
|
||||||
|
return (value: unknown): boolean => {
|
||||||
|
if (encountered.has(value)) return false;
|
||||||
|
encountered.add(value);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue