Compare commits

..

37 Commits

Author SHA1 Message Date
Paul Merlin
90ccf054e6 Merge branch 'master' into releases/v1 2020-09-13 13:48:42 +02:00
Paul Merlin
cb2742a00b Protect RC provisioning from network problems and add logging 2020-09-13 13:30:54 +02:00
Paul Merlin
c8c53f54bb Let CI exercises using latest Gradle RC 2020-09-13 13:25:31 +02:00
Paul Merlin
863daedf86 Bump test build wrapper 2020-09-13 13:23:48 +02:00
Paul Merlin
55871cbb47 Simplify build scan url extraction 2020-09-13 13:09:14 +02:00
Paul Merlin
e3d60b3873 Protect wrapper cache restore against tar errors 2020-09-13 13:06:00 +02:00
Paul Merlin
2dd6cc1801 Upgrade dependencies 2020-09-13 12:37:40 +02:00
Paul Merlin
c6b57b9c8c Merge branch 'master' into releases/v1 2020-06-22 16:45:23 +02:00
Paul Merlin
f11e7d60d7 Short-circuit dependencies and configuration caching if cache dir already exists
This is relevant if you run this action several times in a single job.
This prevent doing unnecessary work starting with the second job using the action.
This prevent droping dependencies downloaded by the first job using the action.
This prevent Windows agents to fail unlinking already existing files.
2020-06-22 16:40:58 +02:00
Paul Merlin
7137b09ae2 Upgrade dependencies 2020-06-22 16:36:25 +02:00
Paul Merlin
053f389907 CI prod check workflow stops Gradle daemon at the end
to allow for dependency caching to work on windows
2020-06-15 20:08:27 +02:00
Paul Merlin
4f9b5202aa Faster CI 2020-06-15 20:03:30 +02:00
Paul Merlin
791b98c565 Merge branch 'master' into releases/v1 2020-06-15 19:35:42 +02:00
Paul Merlin
466a737d16 Only restore wrapper cache if dir doesn't exist 2020-06-15 19:33:04 +02:00
Paul Merlin
eb7eb78d95 Refine README 2020-06-15 16:50:15 +02:00
Paul Merlin
27da3e28b3 Merge branch 'master' into releases/v1 2020-06-15 16:40:55 +02:00
Paul Merlin
682d7347f7 Optional dependencies cache
and bonus configuration cache

Merge branch 'eskatos/caching'
2020-06-15 16:36:19 +02:00
Paul Merlin
3c43b6525f Add some documentation around caching 2020-06-15 16:23:01 +02:00
Paul Merlin
aefd8348d2 Enable configuration cache on CI 2020-06-15 16:03:04 +02:00
Paul Merlin
f2de61db4e Enable configuration cache on CI 2020-06-15 15:58:50 +02:00
Paul Merlin
4c7d97cca4 Add caching of Gradle build configuration cache 2020-06-15 15:58:20 +02:00
Paul Merlin
806543fb3a Enable dependencies cache on CI 2020-06-15 15:57:47 +02:00
Paul Merlin
2afa86ca9f Disable dependencies cache by default 2020-06-15 15:57:09 +02:00
Paul Merlin
ae50675399 Better warning message 2020-06-15 15:43:21 +02:00
Paul Merlin
d7a54a26c7 Don't drop gc.properties in dependencies cache
because it would disable Gradle cache cleanup
2020-06-15 15:41:53 +02:00
Paul Merlin
02a8a21e55 Add input to disable wrapper caching 2020-06-15 15:41:09 +02:00
Paul Merlin
3abad5567a Add input to disable dependencies caching altogether 2020-06-15 14:32:54 +02:00
Paul Merlin
7c8cc1a9ef Fix typo 2020-06-15 14:28:57 +02:00
Paul Merlin
355e9c1f86 Try delete dependencies lock files and disable caching if not possible 2020-06-15 14:22:41 +02:00
Paul Merlin
26dd4cb9bb Make file hashing more robust 2020-06-15 14:09:44 +02:00
Paul Merlin
317ca35dca Refine dependencies caching 2020-06-15 13:16:51 +02:00
Paul Merlin
053762c1c1 Add input to make the dependencies caching strict 2020-06-15 13:05:36 +02:00
Paul Merlin
95e20daa83 Automatic caching of dependencies
in a best effort manner by default
allowing to specify files to hash for computing the cache key
2020-06-15 13:04:42 +02:00
Paul Merlin
fcc1683d01 Extract github actions utils 2020-06-14 19:05:03 +02:00
Paul Merlin
059f2e7791 Rename cache.ts to cache-wrapper.ts 2020-06-14 19:04:18 +02:00
Paul Merlin
692fda9de7 Cache wrapper install when gradle-executable points to a wrapper 2020-06-14 18:01:54 +02:00
Paul Merlin
e8885a31b8 Refine README 2020-06-14 12:57:10 +02:00
23 changed files with 2913 additions and 1614 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

@@ -1,3 +1,4 @@
# make sure the build works and doesn't produce spurious changes
name: dev
on:
@@ -6,10 +7,7 @@ on:
jobs:
check:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v2
@@ -17,15 +15,10 @@ jobs:
run: |
npm install
npm run all
- name: Test wrapper
uses: ./
with:
wrapper-directory: __tests__/data/basic
build-root-directory: __tests__/data/basic
arguments: help
- name: Test dist download
uses: ./
with:
gradle-version: 6.5
build-root-directory: __tests__/data/basic
arguments: help
- name: Check for uncommitted changes
# Ensure no changes, but ignore node_modules dir since dev/fresh ci deps installed.
run: |
git diff --exit-code --stat -- . ':!node_modules' \
|| (echo "##[error] found changed files after build. please 'npm run all'" \
"and check in all changes" \
&& exit 1)

View File

@@ -2,10 +2,8 @@
name: prod
on:
pull_request:
push:
branches:
- master
- 'releases/*'
jobs:
check:
@@ -21,10 +19,22 @@ 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: rc
build-root-directory: __tests__/data/basic
arguments: help
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: test --configuration-cache
- name: Stop Gradle daemon
uses: ./
with:
gradle-version: rc
build-root-directory: __tests__/data/basic
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: --stop

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,72 @@ 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
**/*.gradle
**/*.gradle.kts
**/gradle.properties
gradle/**
```
This is a good enough approximation.
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 +202,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,30 +1,30 @@
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'
)
)
expect(version).toBe('6.5-bin')
expect(version).toBe('6.6.1-bin')
})
it('for -bin dist', async () => {
const version = cache.extractGradleWrapperSlugFromDistUri(
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-bin.zip'
const version = cacheWrapper.extractGradleWrapperSlugFromDistUri(
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.6.1-bin.zip'
)
expect(version).toBe('6.5-bin')
expect(version).toBe('6.6.1-bin')
})
it('for -all dist', async () => {
const version = cache.extractGradleWrapperSlugFromDistUri(
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-all.zip'
const version = cacheWrapper.extractGradleWrapperSlugFromDistUri(
'distributionUrl=https\\://services.gradle.org/distributions/gradle-6.6.1-all.zip'
)
expect(version).toBe('6.5-all')
expect(version).toBe('6.6.1-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'
: '63b9f14f65d014e585099c9c274b9dcbddf5cfd1a8978e5a24efb89ff9304348'
)
})
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'
: '63b9f14f65d014e585099c9c274b9dcbddf5cfd1a8978e5a24efb89ff9304348'
)
})
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'
: 'f42cd10636f09799f4e01cc84e7ae906cc1d9140f1446f8dcd054d19cbc44c2b'
)
})
})
})

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

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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

3869
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,29 +24,30 @@
"author": "Paul Merlin <paul@nosphere.org>",
"license": "MIT",
"dependencies": {
"@actions/core": "1.2.4",
"@actions/cache": "1.0.2",
"@actions/core": "1.2.5",
"@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",
"@actions/tool-cache": "1.6.0",
"string-argv": "0.3.1",
"typed-rest-client": "1.7.3",
"unzipper": "0.10.11"
},
"devDependencies": {
"@types/jest": "26.0.0",
"@types/jest": "26.0.13",
"@types/node": "12.12.6",
"@types/unzipper": "0.10.3",
"@typescript-eslint/parser": "3.2.0",
"@typescript-eslint/parser": "4.1.0",
"@zeit/ncc": "0.22.3",
"eslint": "7.2.0",
"eslint-plugin-github": "4.0.1",
"eslint-plugin-jest": "23.13.2",
"jest": "26.0.1",
"jest-circus": "26.0.1",
"eslint": "7.9.0",
"eslint-plugin-github": "4.1.1",
"eslint-plugin-jest": "24.0.1",
"jest": "26.4.2",
"jest-circus": "26.4.2",
"js-yaml": "3.14.0",
"prettier": "2.0.5",
"ts-jest": "26.1.0",
"typescript": "3.8.3"
"prettier": "2.1.1",
"ts-jest": "26.3.0",
"typescript": "4.0.2"
}
}

View File

@@ -0,0 +1,97 @@
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')
if (fs.existsSync(cachePath)) return
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)
}

120
src/cache-dependencies.ts Normal file
View File

@@ -0,0 +1,120 @@
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')
if (fs.existsSync(cachePath)) return
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,56 +1,74 @@
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'
)
)
if (!wrapperSlug) return
const wrapperCacheKey = `wrapper-${wrapperSlug}`
const wrapperCachePath = path.join(
const wrapperCachePath = path.resolve(
os.homedir(),
`.gradle/wrapper/dists/gradle-${wrapperSlug}`
)
if (fs.existsSync(wrapperCachePath)) return
core.saveState(WRAPPER_CACHE_KEY, wrapperCacheKey)
core.saveState(WRAPPER_CACHE_PATH, wrapperCachePath)
const restoredKey = await cache.restoreCache(
[wrapperCachePath],
wrapperCacheKey
)
try {
const restoredKey = await cache.restoreCache(
[wrapperCachePath],
wrapperCacheKey
)
if (!restoredKey) {
if (!restoredKey) {
core.info(
'Wrapper installation cache not found, expect a Gradle distribution download.'
)
return
}
core.saveState(WRAPPER_CACHE_RESULT, restoredKey)
core.info(
'Wrapper installation cache not found, expect a Gradle distribution download.'
`Wrapper installation restored from cache key: ${restoredKey}`
)
return
} catch (error) {
core.info(
`Wrapper installation cache restore failed, expect a Gradle distribution download\n ${error}`
)
return
}
core.saveState(WRAPPER_CACHE_RESULT, restoredKey)
core.info(`Wrapper installation restored from cache key: ${restoredKey}`)
return
}
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 +113,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
@@ -13,12 +18,9 @@ export async function execute(
ignoreReturnCode: true,
listeners: {
stdline: (line: string) => {
if (line.startsWith('Publishing build scan...')) {
if (line.includes('Publishing build scan...')) {
publishing = true
}
if (publishing && line.length === 0) {
publishing = false
}
if (publishing && line.startsWith('http')) {
buildScanUrl = line.trim()
publishing = false

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()

View File

@@ -41,9 +41,10 @@ async function gradleReleaseCandidate(): Promise<string> {
const versionInfo = await gradleVersionDeclaration(
`${gradleVersionsBaseUrl}/release-candidate`
)
if (versionInfo) {
if (versionInfo && versionInfo.version && versionInfo.downloadUrl) {
return provisionGradle(versionInfo.version, versionInfo.downloadUrl)
}
core.info('No current release-candidate found, will fallback to current')
return gradleCurrent()
}