Merge branch 'master' into releases/v1

This commit is contained in:
Paul Merlin 2020-06-15 16:40:55 +02:00
commit 27da3e28b3
20 changed files with 497 additions and 52 deletions

View file

@ -37,7 +37,7 @@
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "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",
"semi": "off",
"@typescript-eslint/semi": ["error", "never"],

View file

@ -22,10 +22,14 @@ jobs:
with:
wrapper-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
uses: ./
with:
gradle-version: 6.5
gradle-version: 6.6-milestone-1
build-root-directory: __tests__/data/basic
arguments: help
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: test --configuration-cache

View file

@ -21,10 +21,14 @@ jobs:
with:
wrapper-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
uses: ./
with:
gradle-version: 6.5
gradle-version: 6.6-milestone-1
build-root-directory: __tests__/data/basic
arguments: help
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: test --configuration-cache

View file

@ -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.
```yaml
# .github/workflows/gradle-build-pr.yml
name: Run Gradle on PRs
@ -21,7 +19,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
@ -83,7 +81,7 @@ If you need to pass environment variables, simply use the GitHub Actions workflo
```yaml
- uses: eskatos/gradle-command-action@v1
with:
gradle-version: 5.6.2
gradle-version: 6.5
```
`gradle-version` can be set to any valid Gradle version.
@ -110,7 +108,7 @@ jobs:
gradle-rc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
@ -120,7 +118,68 @@ jobs:
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`.
@ -139,7 +198,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11

View file

@ -1,10 +1,10 @@
import * as cache from '../src/cache'
import * as cacheWrapper from '../src/cache-wrapper'
import * as path from 'path'
describe('cache', () => {
describe('can extract gradle wrapper slug', () => {
it('from wrapper properties file', async () => {
const version = cache.extractGradleWrapperSlugFrom(
const version = cacheWrapper.extractGradleWrapperSlugFrom(
path.resolve(
'__tests__/data/basic/gradle/wrapper/gradle-wrapper.properties'
)
@ -12,19 +12,19 @@ describe('cache', () => {
expect(version).toBe('6.5-bin')
})
it('for -bin dist', async () => {
const version = cache.extractGradleWrapperSlugFromDistUri(
const version = cacheWrapper.extractGradleWrapperSlugFromDistUri(
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-bin.zip'
)
expect(version).toBe('6.5-bin')
})
it('for -all dist', async () => {
const version = cache.extractGradleWrapperSlugFromDistUri(
const version = cacheWrapper.extractGradleWrapperSlugFromDistUri(
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-all.zip'
)
expect(version).toBe('6.5-all')
})
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'
)
expect(version).toBe('6.6-milestone-1-all')

View 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'
)
})
})
})

View file

@ -1,6 +1,11 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This is a general purpose Gradle build.
* Learn how to create Gradle builds at https://guides.gradle.org/creating-new-gradle-builds
*/
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation('junit:junit:4.12')
}

View file

@ -0,0 +1,10 @@
package basic;
import org.junit.Test;
public class BasicTest {
@Test
public void test() {
assert true;
}
}

View file

@ -20,6 +20,27 @@ inputs:
arguments:
description: Gradle command line arguments, see gradle --help
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:
build-scan-url:

2
dist/main/index.js vendored

File diff suppressed because one or more lines are too long

2
dist/post/index.js vendored

File diff suppressed because one or more lines are too long

View file

@ -24,11 +24,12 @@
"author": "Paul Merlin <paul@nosphere.org>",
"license": "MIT",
"dependencies": {
"@actions/cache": "0.2.1",
"@actions/core": "1.2.4",
"@actions/exec": "1.0.4",
"@actions/glob": "0.1.0",
"@actions/io": "1.0.2",
"@actions/tool-cache": "1.5.5",
"@actions/cache": "0.2.1",
"string-argv": "0.3.1",
"typed-rest-client": "1.7.3",
"unzipper": "0.10.11"

View 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
View 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/**'
]
}

View file

@ -1,19 +1,25 @@
import * as core from '@actions/core'
import * as cache from '@actions/cache'
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'
const WRAPPER_CACHE_KEY = 'WRAPPER_CACHE_KEY'
const WRAPPER_CACHE_PATH = 'WRAPPER_CACHE_PATH'
const WRAPPER_CACHE_RESULT = 'WRAPPER_CACHE_RESULT'
export async function restoreCachedWrapperDist(
executableDirectory: string
gradlewDirectory: string | null
): Promise<void> {
if (isWrapperCacheDisabled()) return
if (gradlewDirectory == null) return
const wrapperSlug = extractGradleWrapperSlugFrom(
path.join(
path.resolve(executableDirectory),
path.resolve(gradlewDirectory),
'gradle/wrapper/gradle-wrapper.properties'
)
)
@ -46,11 +52,13 @@ export async function restoreCachedWrapperDist(
}
export async function cacheWrapperDist(): Promise<void> {
if (isWrapperCacheDisabled()) return
const cacheKey = core.getState(WRAPPER_CACHE_KEY)
const cachePath = core.getState(WRAPPER_CACHE_PATH)
const cacheResult = core.getState(WRAPPER_CACHE_RESULT)
if (!cachePath) {
if (!cachePath || !fs.existsSync(cachePath)) {
core.debug('No wrapper installation to cache.')
return
}
@ -95,3 +103,7 @@ export function extractGradleWrapperSlugFromDistUri(
const match = distUri.match(regex)
return match ? match[1] : null
}
function isWrapperCacheDisabled(): boolean {
return !github.inputBoolean('wrapper-cache-enabled', true)
}

44
src/crypto-utils.ts Normal file
View 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')
}

View file

@ -1,10 +1,15 @@
import * as exec from '@actions/exec'
import * as cacheDependencies from './cache-dependencies'
import * as cacheConfiguration from './cache-configuration'
export async function execute(
executable: string,
root: string,
argv: string[]
): Promise<BuildResult> {
await cacheDependencies.restoreCachedDependencies(root)
await cacheConfiguration.restoreCachedConfiguration(root)
let publishing = false
let buildScanUrl: string | undefined

24
src/github-utils.ts Normal file
View 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'
}

View file

@ -2,7 +2,8 @@ import * as core from '@actions/core'
import * as path from 'path'
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 gradlew from './gradlew'
import * as provision from './provision'
@ -33,29 +34,34 @@ export async function run(): Promise<void> {
run()
async function resolveGradleExecutable(baseDirectory: string): Promise<string> {
const gradleVersion = inputOrNull('gradle-version')
const gradleVersion = github.inputOrNull('gradle-version')
if (gradleVersion !== null && gradleVersion !== 'wrapper') {
return path.resolve(await provision.gradleVersion(gradleVersion))
}
const gradleExecutable = inputOrNull('gradle-executable')
const gradleExecutable = github.inputOrNull('gradle-executable')
if (gradleExecutable !== null) {
if (gradleExecutable.endsWith(gradlew.wrapperFilename())) {
await cacheWrapper.restoreCachedWrapperDist(
path.resolve(gradleExecutable, '..')
)
}
return path.resolve(baseDirectory, gradleExecutable)
}
const wrapperDirectory = inputOrNull('wrapper-directory')
const executableDirectory =
const wrapperDirectory = github.inputOrNull('wrapper-directory')
const gradlewDirectory =
wrapperDirectory !== null
? path.join(baseDirectory, wrapperDirectory)
: 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 {
const buildRootDirectory = inputOrNull('build-root-directory')
const buildRootDirectory = github.inputOrNull('build-root-directory')
const resolvedBuildRootDirectory =
buildRootDirectory === null
? path.resolve(baseDirectory)
@ -64,14 +70,6 @@ function resolveBuildRootDirectory(baseDirectory: string): string {
}
function parseCommandLineArguments(): string[] {
const input = inputOrNull('arguments')
const input = github.inputOrNull('arguments')
return input === null ? [] : parseArgsStringToArgv(input)
}
function inputOrNull(name: string): string | null {
const inputString = core.getInput(name)
if (inputString.length === 0) {
return null
}
return inputString
}

View file

@ -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
export async function run(): Promise<void> {
await cache.cacheWrapperDist()
await cacheWrapper.cacheWrapperDist()
await cacheDependencies.cacheDependencies()
await cacheConfiguration.cacheConfiguration()
}
run()