Implement automatic bundle install and caching

* Not yet enabled by default.
This commit is contained in:
Benoit Daloze
2020-06-14 13:22:16 +02:00
parent 281d1c54fe
commit d57231c9a3
6 changed files with 3873 additions and 197 deletions
+1
View File
@@ -39,6 +39,7 @@ jobs:
- uses: ./
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: ruby -v
- name: build compiler
+22 -2
View File
@@ -116,8 +116,27 @@ Otherwise, the latest Bundler version is installed (except for Ruby 2.2 and 2.3
This behavior can be customized, see [action.yml](action.yml) for details about the `bundler` input.
### Caching `bundle install`
### Caching `bundle install` automatically
This action provides a way to automatically run `bundle install` and cache the result:
```yaml
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
```
This caching speeds up installing gems significantly and avoids too many requests to RubyGems.org.
It needs a `Gemfile` under the [`working-directory`](#working-directory).
The caching works whether there is a `Gemfile.lock` or not.
If there is a `Gemfile.lock`, `bundle config --local deployment true` is used.
To perform caching, this action will use `bundle config --local path vendor/bundle`.
Therefore, the Bundler `path` should not be changed in your workflow for the cache to work.
### Caching `bundle install` manually
You can also cache gems manually,
but this is not recommended because it is verbose and very difficult to use a correct cache key.
You can cache the installed gems with these two steps:
```yaml
@@ -140,7 +159,8 @@ When using `.ruby-version`, replace `${{ matrix.ruby }}` with `${{ hashFiles('.r
When using `.tool-versions`, replace `${{ matrix.ruby }}` with `${{ hashFiles('.tool-versions') }}`.
This uses the [cache action](https://github.com/actions/cache).
The code above is a more complete version of the [Ruby - Bundler example](https://github.com/actions/cache/blob/master/examples.md#ruby---bundler).
The code above is a more complete version of the [Ruby - Bundler example](https://github.com/actions/cache/blob/master/examples.md#ruby---\
bundler).
Make sure to include `use-ruby` in the `key` to avoid conflicting with previous caches.
### Working Directory
+4
View File
@@ -13,6 +13,10 @@ inputs:
description: 'The version of Bundler to install. Either none, 1, 2, latest or Gemfile.lock. The default tries Gemfile.lock and otherwise uses latest.'
required: false
default: 'default'
bundler-cache:
description: 'Run "bundle install", and cache the result automatically. Either true or false.'
required: false
default: 'false'
working-directory:
description: 'The working directory to use for resolving paths for .ruby-version, .tool-versions and Gemfile.lock.'
required: false
+11
View File
@@ -1,5 +1,8 @@
const os = require('os')
const fs = require('fs')
const util = require('util')
const stream = require('stream')
const crypto = require('crypto')
const core = require('@actions/core')
const { performance } = require('perf_hooks')
@@ -20,6 +23,14 @@ export function isHeadVersion(rubyVersion) {
return rubyVersion === 'head' || rubyVersion === 'debug' || rubyVersion === 'mingw' || rubyVersion === 'mswin'
}
export async function hashFile(file) {
// See https://github.com/actions/runner/blob/master/src/Misc/expressionFunc/hashFiles/src/hashFiles.ts
const hash = crypto.createHash('sha256')
const pipeline = util.promisify(stream.pipeline)
await pipeline(fs.createReadStream(file), hash)
return hash.digest('hex')
}
export function getVirtualEnvironmentName() {
const platform = os.platform()
if (platform === 'linux') {
Generated Vendored
+3751 -195
View File
File diff suppressed because it is too large Load Diff
+84
View File
@@ -3,11 +3,13 @@ const fs = require('fs')
const path = require('path')
const core = require('@actions/core')
const exec = require('@actions/exec')
const cache = require('@actions/cache')
const common = require('./common')
const inputDefaults = {
'ruby-version': 'default',
'bundler': 'default',
'bundler-cache': 'true',
'working-directory': '.',
}
@@ -50,6 +52,11 @@ export async function setupRuby(options) {
if (inputs['bundler'] !== 'none') {
await common.measure('Installing Bundler', async () =>
installBundler(inputs['bundler'], platform, rubyPrefix, engine, version))
if (inputs['bundler-cache'] === 'true') {
await common.measure('bundle install', async () =>
bundleInstall(platform, engine, version))
}
}
core.setOutput('ruby-prefix', rubyPrefix)
@@ -194,4 +201,81 @@ async function installBundler(bundlerVersionInput, platform, rubyPrefix, engine,
}
}
async function bundleInstall(platform, engine, version) {
if (!fs.existsSync('Gemfile')) {
console.log('No Gemfile, skipping "bundle install" and caching')
return
}
// config
const path = 'vendor/bundle'
const hasGemfileLock = fs.existsSync('Gemfile.lock');
if (hasGemfileLock) {
await exec.exec('bundle', ['config', '--local', 'deployment', 'true'])
}
await exec.exec('bundle', ['config', '--local', 'path', path])
// cache key
const paths = [path]
const baseKey = await computeBaseKey(platform, engine, version)
let key = baseKey
let restoreKeys
if (hasGemfileLock) {
key += `-Gemfile.lock-${await common.hashFile('Gemfile.lock')}`
// If only Gemfile.lock we can reuse some of the cache (but it will keep old gem versions in the cache)
restoreKeys = [`${baseKey}-Gemfile.lock-`]
} else {
// Only exact key, to never mix native gems of different platforms or Ruby versions
restoreKeys = []
}
console.log(`Cache key: ${key}`)
// restore cache & install
const cachedKey = await cache.restoreCache(paths, key, restoreKeys)
if (cachedKey) {
console.log(`Found cache for key: ${cachedKey}`)
}
let alreadyInstalled = false
if (cachedKey === key) {
const exitCode = await exec.exec('bundle', ['check'], { ignoreReturnCode: true })
alreadyInstalled = (exitCode === 0)
}
if (!alreadyInstalled) {
await exec.exec('bundle', ['install', '--jobs', '4'])
// Error handling from https://github.com/actions/cache/blob/master/src/save.ts
console.log('Saving cache')
try {
await cache.saveCache(paths, key)
} catch (error) {
if (error.name === cache.ValidationError.name) {
throw error;
} else if (error.name === cache.ReserveCacheError.name) {
core.info(error.message);
} else {
core.info(`[warning]${error.message}`)
}
}
}
}
async function computeBaseKey(platform, engine, version) {
let baseKey = `setup-ruby-bundle-install-${platform}-${engine}-${version}`
if (engine === 'ruby' && common.isHeadVersion(version)) {
let revision = '';
await exec.exec('ruby', ['-e', 'print RUBY_REVISION'], {
silent: true,
listeners: {
stdout: (data) => {
revision += data.toString();
}
}
});
baseKey += `-revision-${revision}`
}
return baseKey
}
if (__filename.endsWith('index.js')) { run() }