mirror of
https://github.com/gradle/gradle-build-action.git
synced 2024-11-22 16:20:59 -05:00
Optional dependencies cache
and bonus configuration cache Merge branch 'eskatos/caching'
This commit is contained in:
commit
682d7347f7
20 changed files with 493 additions and 48 deletions
|
@ -37,7 +37,7 @@
|
||||||
"@typescript-eslint/prefer-includes": "error",
|
"@typescript-eslint/prefer-includes": "error",
|
||||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||||
"@typescript-eslint/promise-function-async": "error",
|
"@typescript-eslint/promise-function-async": "error",
|
||||||
"@typescript-eslint/require-array-sort-compare": "error",
|
"@typescript-eslint/require-array-sort-compare": ["error", {"ignoreStringArrays": true}],
|
||||||
"@typescript-eslint/restrict-plus-operands": "error",
|
"@typescript-eslint/restrict-plus-operands": "error",
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error", "never"],
|
"@typescript-eslint/semi": ["error", "never"],
|
||||||
|
|
10
.github/workflows/dev.yml
vendored
10
.github/workflows/dev.yml
vendored
|
@ -22,10 +22,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
wrapper-directory: __tests__/data/basic
|
wrapper-directory: __tests__/data/basic
|
||||||
build-root-directory: __tests__/data/basic
|
build-root-directory: __tests__/data/basic
|
||||||
arguments: help
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
arguments: test
|
||||||
- name: Test dist download
|
- name: Test dist download
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.5
|
gradle-version: 6.6-milestone-1
|
||||||
build-root-directory: __tests__/data/basic
|
build-root-directory: __tests__/data/basic
|
||||||
arguments: help
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
arguments: test --configuration-cache
|
||||||
|
|
10
.github/workflows/prod.yml
vendored
10
.github/workflows/prod.yml
vendored
|
@ -21,10 +21,14 @@ jobs:
|
||||||
with:
|
with:
|
||||||
wrapper-directory: __tests__/data/basic
|
wrapper-directory: __tests__/data/basic
|
||||||
build-root-directory: __tests__/data/basic
|
build-root-directory: __tests__/data/basic
|
||||||
arguments: help
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
arguments: test
|
||||||
- name: Test dist download
|
- name: Test dist download
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
gradle-version: 6.5
|
gradle-version: 6.6-milestone-1
|
||||||
build-root-directory: __tests__/data/basic
|
build-root-directory: __tests__/data/basic
|
||||||
arguments: help
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
arguments: test --configuration-cache
|
||||||
|
|
65
README.md
65
README.md
|
@ -8,8 +8,6 @@ You might also be interested by the related [Gradle Plugin](https://github.com/e
|
||||||
|
|
||||||
The following workflow will run `./gradlew build` using the wrapper from the repository on ubuntu, macos and windows. The only prerequisite is to have Java installed, you can define the version you need to run the build using the `actions/setup-java` action.
|
The following workflow will run `./gradlew build` using the wrapper from the repository on ubuntu, macos and windows. The only prerequisite is to have Java installed, you can define the version you need to run the build using the `actions/setup-java` action.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# .github/workflows/gradle-build-pr.yml
|
# .github/workflows/gradle-build-pr.yml
|
||||||
name: Run Gradle on PRs
|
name: Run Gradle on PRs
|
||||||
|
@ -120,7 +118,68 @@ jobs:
|
||||||
arguments: build --dry-run # just test build configuration
|
arguments: build --dry-run # just test build configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
# Build scans
|
## Caching
|
||||||
|
|
||||||
|
This action provides 3 levels of caching to help speed up your GitHub Actions:
|
||||||
|
|
||||||
|
- `wrapper` caches the local [wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) installation, saving time downloading and unpacking Gradle distributions ;
|
||||||
|
- `dependencies` caches the [dependencies](https://docs.gradle.org/current/userguide/dependency_resolution.html#sub:cache_copy), saving time downloading dependencies ;
|
||||||
|
- `configuration` caches the [build configuration](https://docs.gradle.org/nightly/userguide/configuration_cache.html), saving time configuring the build.
|
||||||
|
|
||||||
|
Only the first one, caching the wrapper installation, is enabled by default.
|
||||||
|
Future versions of this action will enable all caching by default.
|
||||||
|
|
||||||
|
You can control which level is enabled as follows:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
wrapper-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
The wrapper installation cache is simple and can't be configured further.
|
||||||
|
|
||||||
|
The dependencies and configuration cache will compute a cache key in a best effort manner.
|
||||||
|
Keep reading to learn how to better control how they work.
|
||||||
|
|
||||||
|
### Configuring the dependencies and configuration caches
|
||||||
|
|
||||||
|
Both the dependencies and configuration caches use the same default configuration:
|
||||||
|
|
||||||
|
They use the following inputs to calculate the cache key:
|
||||||
|
```text
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
They restore cached state even if there isn't an exact match.
|
||||||
|
|
||||||
|
If the defaults don't suit your needs you can override them with the following inputs:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies-cache-key: |
|
||||||
|
**/gradle.properties
|
||||||
|
gradle/dependency-locking/**
|
||||||
|
dependencies-cache-exact: true
|
||||||
|
configuration-cache-key: |
|
||||||
|
**/gradle.properties
|
||||||
|
gradle/dependency-locking/**
|
||||||
|
configuration-cache-exact: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Coming up with a good cache key isn't trivial and depends on your build.
|
||||||
|
The above example isn't realistic.
|
||||||
|
Stick to the defaults unless you know what you are doing.
|
||||||
|
|
||||||
|
If you happen to use Gradle [dependency locking](https://docs.gradle.org/current/userguide/dependency_locking.html) you can make the dependencies cache more precise with the following configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
dependencies-cache-key: gradle/dependency-locking/**
|
||||||
|
dependencies-cache-exact: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build scans
|
||||||
|
|
||||||
If your build publishes a [build scan](https://gradle.com/build-scans/) the `gradle-command-action` action will emit the link to the published build scan as an output named `build-scan-url`.
|
If your build publishes a [build scan](https://gradle.com/build-scans/) the `gradle-command-action` action will emit the link to the published build scan as an output named `build-scan-url`.
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import * as cache from '../src/cache'
|
import * as cacheWrapper from '../src/cache-wrapper'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
describe('cache', () => {
|
describe('cache', () => {
|
||||||
describe('can extract gradle wrapper slug', () => {
|
describe('can extract gradle wrapper slug', () => {
|
||||||
it('from wrapper properties file', async () => {
|
it('from wrapper properties file', async () => {
|
||||||
const version = cache.extractGradleWrapperSlugFrom(
|
const version = cacheWrapper.extractGradleWrapperSlugFrom(
|
||||||
path.resolve(
|
path.resolve(
|
||||||
'__tests__/data/basic/gradle/wrapper/gradle-wrapper.properties'
|
'__tests__/data/basic/gradle/wrapper/gradle-wrapper.properties'
|
||||||
)
|
)
|
||||||
|
@ -12,19 +12,19 @@ describe('cache', () => {
|
||||||
expect(version).toBe('6.5-bin')
|
expect(version).toBe('6.5-bin')
|
||||||
})
|
})
|
||||||
it('for -bin dist', async () => {
|
it('for -bin dist', async () => {
|
||||||
const version = cache.extractGradleWrapperSlugFromDistUri(
|
const version = cacheWrapper.extractGradleWrapperSlugFromDistUri(
|
||||||
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-bin.zip'
|
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-bin.zip'
|
||||||
)
|
)
|
||||||
expect(version).toBe('6.5-bin')
|
expect(version).toBe('6.5-bin')
|
||||||
})
|
})
|
||||||
it('for -all dist', async () => {
|
it('for -all dist', async () => {
|
||||||
const version = cache.extractGradleWrapperSlugFromDistUri(
|
const version = cacheWrapper.extractGradleWrapperSlugFromDistUri(
|
||||||
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-all.zip'
|
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-all.zip'
|
||||||
)
|
)
|
||||||
expect(version).toBe('6.5-all')
|
expect(version).toBe('6.5-all')
|
||||||
})
|
})
|
||||||
it('for milestone', async () => {
|
it('for milestone', async () => {
|
||||||
const version = cache.extractGradleWrapperSlugFromDistUri(
|
const version = cacheWrapper.extractGradleWrapperSlugFromDistUri(
|
||||||
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.6-milestone-1-all.zip'
|
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.6-milestone-1-all.zip'
|
||||||
)
|
)
|
||||||
expect(version).toBe('6.6-milestone-1-all')
|
expect(version).toBe('6.6-milestone-1-all')
|
39
__tests__/crypto-utils.test.ts
Normal file
39
__tests__/crypto-utils.test.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as cryptoUtils from '../src/crypto-utils'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
describe('crypto-utils', () => {
|
||||||
|
describe('can hash', () => {
|
||||||
|
it('a directory', async () => {
|
||||||
|
const hash = await cryptoUtils.hashFiles(
|
||||||
|
path.resolve('__tests__/data/basic/gradle')
|
||||||
|
)
|
||||||
|
expect(hash).toBe(
|
||||||
|
process.platform === 'win32'
|
||||||
|
? '3364336e94e746ce65a31748a6371b7efd7d499e18ad605c74c91cde0edc0a44'
|
||||||
|
: '4ebb65b45e6f6796d5ec6ace96e9471cc6573d294c54f99c4920fe5328e75bab'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('a directory with a glob', async () => {
|
||||||
|
const hash = await cryptoUtils.hashFiles(
|
||||||
|
path.resolve('__tests__/data/basic/'),
|
||||||
|
['gradle/**']
|
||||||
|
)
|
||||||
|
expect(hash).toBe(
|
||||||
|
process.platform === 'win32'
|
||||||
|
? '3364336e94e746ce65a31748a6371b7efd7d499e18ad605c74c91cde0edc0a44'
|
||||||
|
: '4ebb65b45e6f6796d5ec6ace96e9471cc6573d294c54f99c4920fe5328e75bab'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('a directory with globs', async () => {
|
||||||
|
const hash = await cryptoUtils.hashFiles(
|
||||||
|
path.resolve('__tests__/data/basic/'),
|
||||||
|
['**/*.gradle', 'gradle/**']
|
||||||
|
)
|
||||||
|
expect(hash).toBe(
|
||||||
|
process.platform === 'win32'
|
||||||
|
? 'd9b66fded38f79f601ce745d64ed726a8df8c0b242b02bcd2c1d331f54742ad6'
|
||||||
|
: 'aa72a837158799fbadd1c4aff94fcc2b5bb9dc6ad8d12f6337d047d4b0c8f79e'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,6 +1,11 @@
|
||||||
/*
|
plugins {
|
||||||
* This file was generated by the Gradle 'init' task.
|
id 'java'
|
||||||
*
|
}
|
||||||
* This is a general purpose Gradle build.
|
|
||||||
* Learn how to create Gradle builds at https://guides.gradle.org/creating-new-gradle-builds
|
repositories {
|
||||||
*/
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation('junit:junit:4.12')
|
||||||
|
}
|
||||||
|
|
10
__tests__/data/basic/src/test/java/basic/BasicTest.java
Normal file
10
__tests__/data/basic/src/test/java/basic/BasicTest.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package basic;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class BasicTest {
|
||||||
|
@Test
|
||||||
|
public void test() {
|
||||||
|
assert true;
|
||||||
|
}
|
||||||
|
}
|
21
action.yml
21
action.yml
|
@ -20,6 +20,27 @@ inputs:
|
||||||
arguments:
|
arguments:
|
||||||
description: Gradle command line arguments, see gradle --help
|
description: Gradle command line arguments, see gradle --help
|
||||||
required: false
|
required: false
|
||||||
|
wrapper-cache-enabled:
|
||||||
|
description: Whether caching wrapper installation is enabled or not, default to 'true'
|
||||||
|
required: false
|
||||||
|
dependencies-cache-enabled:
|
||||||
|
description: Whether caching dependencies is enabled or not, default to 'false'
|
||||||
|
required: false
|
||||||
|
dependencies-cache-key:
|
||||||
|
description: Globs of files to hash in the build root directory, separated by new lines, use best-effort if unset
|
||||||
|
required: false
|
||||||
|
dependencies-cache-exact:
|
||||||
|
description: Whether to restore only if exact match, default to 'false'
|
||||||
|
required: false
|
||||||
|
configuration-cache-enabled:
|
||||||
|
description: Whether caching build configuration is enabled or not, default to 'false'
|
||||||
|
required: false
|
||||||
|
configuration-cache-key:
|
||||||
|
description: Globs of files to hash in the build root directory, separated by new lines, use best-effort if unset
|
||||||
|
required: false
|
||||||
|
configuration-cache-exact:
|
||||||
|
description: Whether to restore only if exact match, default to 'false'
|
||||||
|
required: false
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
build-scan-url:
|
build-scan-url:
|
||||||
|
|
2
dist/main/index.js
vendored
2
dist/main/index.js
vendored
File diff suppressed because one or more lines are too long
2
dist/post/index.js
vendored
2
dist/post/index.js
vendored
File diff suppressed because one or more lines are too long
|
@ -24,11 +24,12 @@
|
||||||
"author": "Paul Merlin <paul@nosphere.org>",
|
"author": "Paul Merlin <paul@nosphere.org>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@actions/cache": "0.2.1",
|
||||||
"@actions/core": "1.2.4",
|
"@actions/core": "1.2.4",
|
||||||
"@actions/exec": "1.0.4",
|
"@actions/exec": "1.0.4",
|
||||||
|
"@actions/glob": "0.1.0",
|
||||||
"@actions/io": "1.0.2",
|
"@actions/io": "1.0.2",
|
||||||
"@actions/tool-cache": "1.5.5",
|
"@actions/tool-cache": "1.5.5",
|
||||||
"@actions/cache": "0.2.1",
|
|
||||||
"string-argv": "0.3.1",
|
"string-argv": "0.3.1",
|
||||||
"typed-rest-client": "1.7.3",
|
"typed-rest-client": "1.7.3",
|
||||||
"unzipper": "0.10.11"
|
"unzipper": "0.10.11"
|
||||||
|
|
96
src/cache-configuration.ts
Normal file
96
src/cache-configuration.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as cache from '@actions/cache'
|
||||||
|
|
||||||
|
import * as github from './github-utils'
|
||||||
|
import * as crypto from './crypto-utils'
|
||||||
|
|
||||||
|
import {inputCacheKeyGlobs, tryDeleteFiles} from './cache-dependencies'
|
||||||
|
|
||||||
|
const CONFIGURATION_CACHE_PATH = 'CONFIGURATION_CACHE_PATH'
|
||||||
|
const CONFIGURATION_CACHE_KEY = 'CONFIGURATION_CACHE_KEY'
|
||||||
|
const CONFIGURATION_CACHE_RESULT = 'CONFIGURATION_CACHE_RESULT'
|
||||||
|
|
||||||
|
export async function restoreCachedConfiguration(
|
||||||
|
rootDir: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (isConfigurationCacheDisabled()) return
|
||||||
|
|
||||||
|
const cachePath = path.resolve(rootDir, '.gradle/configuration-cache')
|
||||||
|
core.saveState(CONFIGURATION_CACHE_PATH, cachePath)
|
||||||
|
|
||||||
|
const inputCacheExact = github.inputBoolean('configuration-cache-exact')
|
||||||
|
const cacheKeyGlobs = inputCacheKeyGlobs('configuration-cache-key')
|
||||||
|
|
||||||
|
const hash = await crypto.hashFiles(rootDir, cacheKeyGlobs)
|
||||||
|
const cacheKeyPrefix = 'configuration-'
|
||||||
|
const cacheKey = `${cacheKeyPrefix}${hash}`
|
||||||
|
core.saveState(CONFIGURATION_CACHE_KEY, cacheKey)
|
||||||
|
|
||||||
|
const cacheResult = await cache.restoreCache(
|
||||||
|
[cachePath],
|
||||||
|
cacheKey,
|
||||||
|
inputCacheExact ? [] : [cacheKeyPrefix]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!cacheResult) {
|
||||||
|
core.info(
|
||||||
|
'Configuration cache not found, expect task graph calculation.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.saveState(CONFIGURATION_CACHE_RESULT, cacheResult)
|
||||||
|
core.info(`Configuration restored from cache key: ${cacheResult}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cacheConfiguration(): Promise<void> {
|
||||||
|
if (isConfigurationCacheDisabled()) return
|
||||||
|
|
||||||
|
const cachePath = core.getState(CONFIGURATION_CACHE_PATH)
|
||||||
|
const cacheKey = core.getState(CONFIGURATION_CACHE_KEY)
|
||||||
|
const cacheResult = core.getState(CONFIGURATION_CACHE_RESULT)
|
||||||
|
|
||||||
|
if (!cachePath || !fs.existsSync(cachePath)) {
|
||||||
|
core.debug('No configuration to cache.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheResult && cacheKey === cacheResult) {
|
||||||
|
core.info(
|
||||||
|
`Configuration cache hit occurred on the cache key ${cacheKey}, not saving cache.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const locksDeleted = tryDeleteFiles([
|
||||||
|
path.resolve(cachePath, 'configuration-cache.lock')
|
||||||
|
])
|
||||||
|
if (!locksDeleted) {
|
||||||
|
core.warning(
|
||||||
|
'Unable to delete configuration lock files, try using --no-daemon or stopping the daemon last if you have several Gradle steps, not saving cache.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cache.saveCache([cachePath], cacheKey)
|
||||||
|
} 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConfigurationCacheDisabled(): boolean {
|
||||||
|
return !github.inputBoolean('configuration-cache-enabled', false)
|
||||||
|
}
|
119
src/cache-dependencies.ts
Normal file
119
src/cache-dependencies.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as os from 'os'
|
||||||
|
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as cache from '@actions/cache'
|
||||||
|
|
||||||
|
import * as github from './github-utils'
|
||||||
|
import * as crypto from './crypto-utils'
|
||||||
|
|
||||||
|
const DEPENDENCIES_CACHE_PATH = 'DEPENDENCIES_CACHE_PATH'
|
||||||
|
const DEPENDENCIES_CACHE_KEY = 'DEPENDENCIES_CACHE_KEY'
|
||||||
|
const DEPENDENCIES_CACHE_RESULT = 'DEPENDENCIES_CACHE_RESULT'
|
||||||
|
|
||||||
|
export async function restoreCachedDependencies(
|
||||||
|
rootDir: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (isDependenciesCacheDisabled()) return
|
||||||
|
|
||||||
|
const cachePath = path.resolve(os.homedir(), '.gradle/caches/modules-2')
|
||||||
|
core.saveState(DEPENDENCIES_CACHE_PATH, cachePath)
|
||||||
|
|
||||||
|
const inputCacheExact = github.inputBoolean('dependencies-cache-exact')
|
||||||
|
const cacheKeyGlobs = inputCacheKeyGlobs('dependencies-cache-key')
|
||||||
|
|
||||||
|
const hash = await crypto.hashFiles(rootDir, cacheKeyGlobs)
|
||||||
|
const cacheKeyPrefix = 'dependencies-'
|
||||||
|
const cacheKey = `${cacheKeyPrefix}${hash}`
|
||||||
|
core.saveState(DEPENDENCIES_CACHE_KEY, cacheKey)
|
||||||
|
|
||||||
|
const cacheResult = await cache.restoreCache(
|
||||||
|
[cachePath],
|
||||||
|
cacheKey,
|
||||||
|
inputCacheExact ? [] : [cacheKeyPrefix]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!cacheResult) {
|
||||||
|
core.info('Dependencies cache not found, expect dependencies download.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
core.saveState(DEPENDENCIES_CACHE_RESULT, cacheResult)
|
||||||
|
core.info(`Dependencies restored from cache key: ${cacheResult}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cacheDependencies(): Promise<void> {
|
||||||
|
if (isDependenciesCacheDisabled()) return
|
||||||
|
|
||||||
|
const cachePath = core.getState(DEPENDENCIES_CACHE_PATH)
|
||||||
|
const cacheKey = core.getState(DEPENDENCIES_CACHE_KEY)
|
||||||
|
const cacheResult = core.getState(DEPENDENCIES_CACHE_RESULT)
|
||||||
|
|
||||||
|
if (!cachePath || !fs.existsSync(cachePath)) {
|
||||||
|
core.debug('No dependencies to cache.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheResult && cacheKey === cacheResult) {
|
||||||
|
core.info(
|
||||||
|
`Dependencies cache hit occurred on the cache key ${cacheKey}, not saving cache.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const locksDeleted = tryDeleteFiles([
|
||||||
|
path.resolve(cachePath, 'modules-2.lock')
|
||||||
|
])
|
||||||
|
if (!locksDeleted) {
|
||||||
|
core.warning(
|
||||||
|
'Unable to delete dependencies lock files, try using --no-daemon or stopping the daemon last if you have several Gradle steps, not saving cache.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await cache.saveCache([cachePath], cacheKey)
|
||||||
|
} 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryDeleteFiles(filePaths: string[]): boolean {
|
||||||
|
let failure = false
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
} catch (error) {
|
||||||
|
failure = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !failure
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDependenciesCacheDisabled(): boolean {
|
||||||
|
return !github.inputBoolean('dependencies-cache-enabled', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inputCacheKeyGlobs(input: string): string[] {
|
||||||
|
const inputValue = github.inputArrayOrNull(input)
|
||||||
|
return inputValue
|
||||||
|
? inputValue
|
||||||
|
: [
|
||||||
|
'**/*.gradle',
|
||||||
|
'**/*.gradle.kts',
|
||||||
|
'**/gradle.properties',
|
||||||
|
'gradle/**'
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,19 +1,25 @@
|
||||||
import * as core from '@actions/core'
|
|
||||||
import * as cache from '@actions/cache'
|
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
|
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as cache from '@actions/cache'
|
||||||
|
|
||||||
|
import * as github from './github-utils'
|
||||||
|
|
||||||
const WRAPPER_CACHE_KEY = 'WRAPPER_CACHE_KEY'
|
const WRAPPER_CACHE_KEY = 'WRAPPER_CACHE_KEY'
|
||||||
const WRAPPER_CACHE_PATH = 'WRAPPER_CACHE_PATH'
|
const WRAPPER_CACHE_PATH = 'WRAPPER_CACHE_PATH'
|
||||||
const WRAPPER_CACHE_RESULT = 'WRAPPER_CACHE_RESULT'
|
const WRAPPER_CACHE_RESULT = 'WRAPPER_CACHE_RESULT'
|
||||||
|
|
||||||
export async function restoreCachedWrapperDist(
|
export async function restoreCachedWrapperDist(
|
||||||
executableDirectory: string
|
gradlewDirectory: string | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (isWrapperCacheDisabled()) return
|
||||||
|
if (gradlewDirectory == null) return
|
||||||
|
|
||||||
const wrapperSlug = extractGradleWrapperSlugFrom(
|
const wrapperSlug = extractGradleWrapperSlugFrom(
|
||||||
path.join(
|
path.join(
|
||||||
path.resolve(executableDirectory),
|
path.resolve(gradlewDirectory),
|
||||||
'gradle/wrapper/gradle-wrapper.properties'
|
'gradle/wrapper/gradle-wrapper.properties'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -46,11 +52,13 @@ export async function restoreCachedWrapperDist(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cacheWrapperDist(): Promise<void> {
|
export async function cacheWrapperDist(): Promise<void> {
|
||||||
|
if (isWrapperCacheDisabled()) return
|
||||||
|
|
||||||
const cacheKey = core.getState(WRAPPER_CACHE_KEY)
|
const cacheKey = core.getState(WRAPPER_CACHE_KEY)
|
||||||
const cachePath = core.getState(WRAPPER_CACHE_PATH)
|
const cachePath = core.getState(WRAPPER_CACHE_PATH)
|
||||||
const cacheResult = core.getState(WRAPPER_CACHE_RESULT)
|
const cacheResult = core.getState(WRAPPER_CACHE_RESULT)
|
||||||
|
|
||||||
if (!cachePath) {
|
if (!cachePath || !fs.existsSync(cachePath)) {
|
||||||
core.debug('No wrapper installation to cache.')
|
core.debug('No wrapper installation to cache.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -95,3 +103,7 @@ export function extractGradleWrapperSlugFromDistUri(
|
||||||
const match = distUri.match(regex)
|
const match = distUri.match(regex)
|
||||||
return match ? match[1] : null
|
return match ? match[1] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWrapperCacheDisabled(): boolean {
|
||||||
|
return !github.inputBoolean('wrapper-cache-enabled', true)
|
||||||
|
}
|
44
src/crypto-utils.ts
Normal file
44
src/crypto-utils.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as stream from 'stream'
|
||||||
|
import * as util from 'util'
|
||||||
|
|
||||||
|
import * as glob from '@actions/glob'
|
||||||
|
|
||||||
|
export async function hashFiles(
|
||||||
|
baseDir: string,
|
||||||
|
globs: string[] = ['**'],
|
||||||
|
followSymbolicLinks = false
|
||||||
|
): Promise<string | null> {
|
||||||
|
let hasMatch = false
|
||||||
|
type FileHashes = Record<string, Buffer>
|
||||||
|
const hashes: FileHashes = {}
|
||||||
|
for await (const globPattern of globs) {
|
||||||
|
const globMatch = `${baseDir}${path.sep}${globPattern}`
|
||||||
|
const globber = await glob.create(globMatch, {followSymbolicLinks})
|
||||||
|
for await (const file of globber.globGenerator()) {
|
||||||
|
// console.log(file)
|
||||||
|
if (!file.startsWith(`${baseDir}${path.sep}`)) {
|
||||||
|
// console.log(`Ignore '${file}' since it is not under '${baseDir}'.`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (fs.statSync(file).isDirectory()) {
|
||||||
|
// console.log(`Skip directory '${file}'.`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const hash = crypto.createHash('sha256')
|
||||||
|
const pipeline = util.promisify(stream.pipeline)
|
||||||
|
await pipeline(fs.createReadStream(file), hash)
|
||||||
|
hashes[path.relative(baseDir, file)] = hash.digest()
|
||||||
|
hasMatch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasMatch) return null
|
||||||
|
const result = crypto.createHash('sha256')
|
||||||
|
for (const file of Object.keys(hashes).sort()) {
|
||||||
|
result.update(hashes[file])
|
||||||
|
}
|
||||||
|
result.end()
|
||||||
|
return result.digest('hex')
|
||||||
|
}
|
|
@ -1,10 +1,15 @@
|
||||||
import * as exec from '@actions/exec'
|
import * as exec from '@actions/exec'
|
||||||
|
import * as cacheDependencies from './cache-dependencies'
|
||||||
|
import * as cacheConfiguration from './cache-configuration'
|
||||||
|
|
||||||
export async function execute(
|
export async function execute(
|
||||||
executable: string,
|
executable: string,
|
||||||
root: string,
|
root: string,
|
||||||
argv: string[]
|
argv: string[]
|
||||||
): Promise<BuildResult> {
|
): Promise<BuildResult> {
|
||||||
|
await cacheDependencies.restoreCachedDependencies(root)
|
||||||
|
await cacheConfiguration.restoreCachedConfiguration(root)
|
||||||
|
|
||||||
let publishing = false
|
let publishing = false
|
||||||
let buildScanUrl: string | undefined
|
let buildScanUrl: string | undefined
|
||||||
|
|
||||||
|
|
24
src/github-utils.ts
Normal file
24
src/github-utils.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
|
export function inputOrNull(name: string): string | null {
|
||||||
|
const inputString = core.getInput(name, {required: false})
|
||||||
|
if (inputString.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return inputString
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inputArrayOrNull(name: string): string[] | null {
|
||||||
|
const string = inputOrNull(name)
|
||||||
|
if (!string) return null
|
||||||
|
return string
|
||||||
|
.split('\n')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s !== '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inputBoolean(name: string, defaultValue = false): boolean {
|
||||||
|
const string = inputOrNull(name)
|
||||||
|
if (!string) return defaultValue
|
||||||
|
return string === 'true'
|
||||||
|
}
|
32
src/main.ts
32
src/main.ts
|
@ -2,7 +2,8 @@ import * as core from '@actions/core'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {parseArgsStringToArgv} from 'string-argv'
|
import {parseArgsStringToArgv} from 'string-argv'
|
||||||
|
|
||||||
import * as cache from './cache'
|
import * as github from './github-utils'
|
||||||
|
import * as cacheWrapper from './cache-wrapper'
|
||||||
import * as execution from './execution'
|
import * as execution from './execution'
|
||||||
import * as gradlew from './gradlew'
|
import * as gradlew from './gradlew'
|
||||||
import * as provision from './provision'
|
import * as provision from './provision'
|
||||||
|
@ -33,29 +34,34 @@ export async function run(): Promise<void> {
|
||||||
run()
|
run()
|
||||||
|
|
||||||
async function resolveGradleExecutable(baseDirectory: string): Promise<string> {
|
async function resolveGradleExecutable(baseDirectory: string): Promise<string> {
|
||||||
const gradleVersion = inputOrNull('gradle-version')
|
const gradleVersion = github.inputOrNull('gradle-version')
|
||||||
if (gradleVersion !== null && gradleVersion !== 'wrapper') {
|
if (gradleVersion !== null && gradleVersion !== 'wrapper') {
|
||||||
return path.resolve(await provision.gradleVersion(gradleVersion))
|
return path.resolve(await provision.gradleVersion(gradleVersion))
|
||||||
}
|
}
|
||||||
|
|
||||||
const gradleExecutable = inputOrNull('gradle-executable')
|
const gradleExecutable = github.inputOrNull('gradle-executable')
|
||||||
if (gradleExecutable !== null) {
|
if (gradleExecutable !== null) {
|
||||||
|
if (gradleExecutable.endsWith(gradlew.wrapperFilename())) {
|
||||||
|
await cacheWrapper.restoreCachedWrapperDist(
|
||||||
|
path.resolve(gradleExecutable, '..')
|
||||||
|
)
|
||||||
|
}
|
||||||
return path.resolve(baseDirectory, gradleExecutable)
|
return path.resolve(baseDirectory, gradleExecutable)
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapperDirectory = inputOrNull('wrapper-directory')
|
const wrapperDirectory = github.inputOrNull('wrapper-directory')
|
||||||
const executableDirectory =
|
const gradlewDirectory =
|
||||||
wrapperDirectory !== null
|
wrapperDirectory !== null
|
||||||
? path.join(baseDirectory, wrapperDirectory)
|
? path.join(baseDirectory, wrapperDirectory)
|
||||||
: baseDirectory
|
: baseDirectory
|
||||||
|
|
||||||
await cache.restoreCachedWrapperDist(executableDirectory)
|
await cacheWrapper.restoreCachedWrapperDist(gradlewDirectory)
|
||||||
|
|
||||||
return path.resolve(executableDirectory, gradlew.wrapperFilename())
|
return path.resolve(gradlewDirectory, gradlew.wrapperFilename())
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBuildRootDirectory(baseDirectory: string): string {
|
function resolveBuildRootDirectory(baseDirectory: string): string {
|
||||||
const buildRootDirectory = inputOrNull('build-root-directory')
|
const buildRootDirectory = github.inputOrNull('build-root-directory')
|
||||||
const resolvedBuildRootDirectory =
|
const resolvedBuildRootDirectory =
|
||||||
buildRootDirectory === null
|
buildRootDirectory === null
|
||||||
? path.resolve(baseDirectory)
|
? path.resolve(baseDirectory)
|
||||||
|
@ -64,14 +70,6 @@ function resolveBuildRootDirectory(baseDirectory: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCommandLineArguments(): string[] {
|
function parseCommandLineArguments(): string[] {
|
||||||
const input = inputOrNull('arguments')
|
const input = github.inputOrNull('arguments')
|
||||||
return input === null ? [] : parseArgsStringToArgv(input)
|
return input === null ? [] : parseArgsStringToArgv(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
function inputOrNull(name: string): string | null {
|
|
||||||
const inputString = core.getInput(name)
|
|
||||||
if (inputString.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return inputString
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import * as cache from './cache'
|
import * as cacheWrapper from './cache-wrapper'
|
||||||
|
import * as cacheDependencies from './cache-dependencies'
|
||||||
|
import * as cacheConfiguration from './cache-configuration'
|
||||||
|
|
||||||
// Invoked by GitHub Actions
|
// Invoked by GitHub Actions
|
||||||
export async function run(): Promise<void> {
|
export async function run(): Promise<void> {
|
||||||
await cache.cacheWrapperDist()
|
await cacheWrapper.cacheWrapperDist()
|
||||||
|
await cacheDependencies.cacheDependencies()
|
||||||
|
await cacheConfiguration.cacheConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|
Loading…
Reference in a new issue