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

build: inline external Github Actions to unblock CI #12241

Merged
merged 5 commits into from
Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/cached-dependencies/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
indent_size = 2
3 changes: 3 additions & 0 deletions .github/actions/cached-dependencies/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
lib/
node_modules/
26 changes: 26 additions & 0 deletions .github/actions/cached-dependencies/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = {
plugins: ['jest', '@typescript-eslint'],
extends: ['plugin:jest/all'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 9,
sourceType: 'module',
},
rules: {
'eslint-comments/no-use': 'off',
'import/no-namespace': 'off',
'no-unused-vars': 'off',
'no-console': 'off',
'jest/prefer-expect-assertions': 'off',
'jest/no-disabled-tests': 'warn',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'warn',
'jest/valid-expect': 'error',
},
env: {
node: true,
es6: true,
'jest/globals': true,
},
};
6 changes: 6 additions & 0 deletions .github/actions/cached-dependencies/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
lib
coverage
node_modules

!dist
!dist/cache
3 changes: 3 additions & 0 deletions .github/actions/cached-dependencies/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
lib/
node_modules/
11 changes: 11 additions & 0 deletions .github/actions/cached-dependencies/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"parser": "typescript"
}
210 changes: 210 additions & 0 deletions .github/actions/cached-dependencies/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# cached-dependencies

Enable **multi-layer cache** and **shortcut commands** in any workflows.

Manage multiple cache targets in one step. Use either the built-in cache configs for npm, yarn, and pip, or write your own. Create a bash command library to easily reduce redudencies across workflows. Most useful for building webapps that require multi-stage building processes.
villebro marked this conversation as resolved.
Show resolved Hide resolved

This is your all-in-one action for everything related to setting up dependencies with cache.

## Inputs

- **run**: bash commands to run, allows shortcut commands
- **caches**: path to a JS module that defines cache targets, defaults to `.github/workflows/caches.js`
- **bashlib**: path to a BASH scripts that defines shortcut commands, defaults to `.github/workflows/bashlib.sh`
- **parallel**: whether to run the commands in parallel with node subprocesses

## Examples

Following workflow sets up dependencies for a typical Python web app with both `~/.pip` and `~/.npm` cache configured in one simple step:

```yaml
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
uses: cached-dependencies
with:
run: |
npm-install
npm run build

pip-install
python ./bin/manager.py fill_test_data
```

Here we used predefined `npm-install` and `pip-install` commands to install dependencies with correponding caches.

You may also replace `npm-install` with `yarn-install` to install npm pacakges with `yarn.lock`.

```yaml
- name: Install dependencies
uses: cached-dependencies
with:
run: |
yarn-install
yarn build

pip-install
python ./bin/manager.py fill_test_data
```

See below for more details.

## Usage

### Cache configs

Under the hood, we use [@actions/cache](https://github.com/marketplace/actions/cache) to manage cache storage. But instead of defining only one cache at a time and specify them in workflow YAMLs, you manage all caches in a spearate JS file: `.github/workflows/caches.js`.

Here is [the default configuration](https://github.com/ktmud/cached-dependencies/blob/master/src/cache/caches.ts) for Linux:

```js
module.exports = {
pip: {
path: [`${process.env.HOME}/.cache/pip`],
hashFiles: ['requirements*.txt'],
keyPrefix: 'pip-',
restoreKeys: 'pip-',
},
npm: {
path: [`${HOME}/.npm`],
hashFiles: [
`package-lock.json`,
`*/*/package-lock.json`,
`!node_modules/*/package-lock.json`,
],
},
yarn: {
path: [`${HOME}/.npm`],
// */* is for supporting lerna monorepo with depth=2
hashFiles: [`yarn.lock`, `*/*/yarn.lock`, `!node_modules/*/yarn.lock`],
},
}
```

In which `hashFiles` and `keyPrefix` will be used to compute the primary cache key used in [@actions/cache](https://github.com/marketplace/actions/cache). `keyPrefix` will default to `${cacheName}-` and `restoreKeys` will default to `keyPrefix` if not specified.

It is recommended to always use absolute paths in these configs so you can share them across different worflows more easily (in case you the action is called from different working directories).

#### Speficy when to restore and save

With the predefined `cache-store` and `cache-save` bash commands, you have full flexibility on when to restore and save cache:

```yaml
steps:
- uses: actions/checkout@v2
- uses: cached-dependencies
with:
run: |
cache-restore npm
npm install
cache-save npm

cache-restore pip
pip install -r requirements.txt
cache-save pip
```

### Shortcut commands

All predefined shortcut commands can be found [here](https://github.com/ktmud/cached-dependencies/blob/master/src/scripts/bashlib.sh). You can also customize them or add new ones in `.github/workflows/bashlib.sh`.

For example, if you want to install additional packages for before saving `pip` cache, simply add this to the `bashlib.sh` file:

```bash
# override the default `pip-install` command
pip-install() {
cd $GITHUB_WORKSPACE

cache-restore pip

echo "::group::pip install"
pip install -r requirements.txt # prod requirements
pip install -r requirements-dev.txt # dev requirements
pip install -e ".[postgres,mysql]" # current pacakge with some extras
echo "::endgroup::"

cache-save pip
}
```

### Default setup command

When `run` is not provided:

```yaml
jobs:
name: Build
steps:
- name: Install dependencies
uses: ktmud/cached-depdencies@v1
```

You must provide a `default-setup-command` in the bashlib. For example,

```bash
default-setup-command() {
pip-install & npm-install
}
```

This will start installing pip and npm dependencies at the same time.

### Customize config locations

Both the two config files, `.github/workflows/bashlib.sh` and `.github/workflows/caches.js`, can be placed in other locations:

```yaml
- uses: cached-dependencies
with:
caches: ${{ github.workspace }}/.github/configs/caches.js
bashlib: ${{ github.workspace }}/.github/configs/bashlib.sh
```

### Run commands in parallel

When `parallel` is set to `true`, the `run` input will be split into an array of commands and passed to `Promise.all(...)` to execute in parallel. For example,

```yaml
- uses: cached-dependencies
with:
parallel: true
run: |
pip-install
npm-install
```

is equivalent to

```yaml
- uses: cached-dependencies
with:
run: |
pip-install & npm-install
```

If one or more of your commands must spread across multiple lines, you can add a new line between the parallel commands. Each command within a parallel group will still run sequentially.

```yaml
- uses: cached-dependencies
with:
run: |
cache-restore pip
pip install requirements*.txt
# additional pip packages
pip install package1 package2 pacakge2
cache-save pip

npm-install

cache-restore cypress
cd cypress/ && npm install
cache-save cypress
```

## License

This project is released under [the MIT License](LICENSE).
124 changes: 124 additions & 0 deletions .github/actions/cached-dependencies/__tests__/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import * as core from '@actions/core';
import * as cache from '../src/cache';
import * as inputsUtils from '../src/utils/inputs';
import * as actionUtils from '@actions/cache/src/utils/actionUtils';
import defaultCaches from '../src/cache/caches';
import { setInputs, getInput, maybeArrayToString } from '../src/utils/inputs';
import { Inputs, InputName, GitHubEvent, EnvVariable } from '../src/constants';
import caches, { npmExpectedHash } from './fixtures/caches';

describe('patch core states', () => {
it('should log error if states file invalid', () => {
const logWarningMock = jest.spyOn(actionUtils, 'logWarning');
fs.writeFileSync(`${os.tmpdir()}/cached--states.json`, 'INVALID_JSON', {
encoding: 'utf-8',
});
core.getState('haha');
expect(logWarningMock).toHaveBeenCalledTimes(2);
});
it('should persist state', () => {
core.saveState('test', '100');
expect(core.getState('test')).toStrictEqual('100');
});
});

describe('cache runner', () => {
it('should use default cache config', async () => {
await cache.loadCustomCacheConfigs();
// but `npm` actually come from `src/cache/caches.ts`
const inputs = await cache.getCacheInputs('npm');
expect(inputs?.[InputName.Path]).toStrictEqual(
maybeArrayToString(defaultCaches.npm.path),
);
expect(inputs?.[InputName.RestoreKeys]).toStrictEqual('npm-');
});

it('should override cache config', async () => {
setInputs({
[InputName.Caches]: path.resolve(__dirname, 'fixtures/caches'),
});
await cache.loadCustomCacheConfigs();

const inputs = await cache.getCacheInputs('npm');
expect(inputs?.[InputName.Path]).toStrictEqual(
maybeArrayToString(caches.npm.path),
);
expect(inputs?.[InputName.Key]).toStrictEqual(`npm-${npmExpectedHash}`);
expect(inputs?.[InputName.RestoreKeys]).toStrictEqual(
maybeArrayToString(caches.npm.restoreKeys),
);
});

it('should apply inputs and restore cache', async () => {
setInputs({
[InputName.Caches]: path.resolve(__dirname, 'fixtures/caches'),
[EnvVariable.GitHubEventName]: GitHubEvent.PullRequest,
});

const setInputsMock = jest.spyOn(inputsUtils, 'setInputs');
const inputs = await cache.getCacheInputs('npm');
const result = await cache.run('restore', 'npm');

expect(result).toBeUndefined();

// before run
expect(setInputsMock).toHaveBeenNthCalledWith(1, inputs);

// after run
expect(setInputsMock).toHaveBeenNthCalledWith(2, {
[InputName.Key]: '',
[InputName.Path]: '',
[InputName.RestoreKeys]: '',
});

// inputs actually restored to original value
expect(getInput(InputName.Key)).toStrictEqual('');

// pretend still in execution context
setInputs(inputs as Inputs);

// `core.getState` should return the primary key
expect(core.getState('CACHE_KEY')).toStrictEqual(inputs?.[InputName.Key]);

setInputsMock.mockRestore();
});

it('should run saveCache', async () => {
// call to save should also work
const logWarningMock = jest.spyOn(actionUtils, 'logWarning');

setInputs({
[InputName.Parallel]: 'true',
});
await cache.run('save', 'npm');
expect(logWarningMock).toHaveBeenCalledWith(
'Cache Service Url not found, unable to restore cache.',
);
});

it('should exit on invalid args', async () => {
// other calls do generate errors
const processExitMock = jest
.spyOn(process, 'exit')
// @ts-ignore
.mockImplementation(() => {});

// incomplete arguments
await cache.run();
await cache.run('save');

// bad arguments
await cache.run('save', 'unknown-cache');
await cache.run('unknown-action', 'unknown-cache');

setInputs({
[InputName.Caches]: 'non-existent',
});
await cache.run('save', 'npm');

expect(processExitMock).toHaveBeenCalledTimes(5);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash

default-setup-command() {
print-cachescript-path
}
Loading