Compare commits

...

10 Commits

Author SHA1 Message Date
Daz DeBoer
b3afdc78a7 Truncate Gradle args when constructing cache key (#71)
Cache keys have a hard limit of 512 characters, so we need to ensure that we don't generate a key longer than this.

- Remove excess whitespace
- Truncate to 400 characters

Fixes #70
2021-08-24 12:46:48 -06:00
Daz DeBoer
e0c2736e35 Include Gradle invocation arguments in cache keys (#69)
This permits a new cache entry to be persisted when a subsequent Gradle invocation does more work that an earlier invocation.

Fixes #68
2021-08-22 17:11:05 -06:00
Daz DeBoer
a63892c289 Log cache entry on save 2021-08-22 14:58:36 -06:00
Daz DeBoer
d432f2086c Provide a better description for the action 2021-08-22 14:48:38 -06:00
Daz DeBoer
eaad2cd2bb Merge pull request #67 from gradle/dd/v15
Allow caching to be enabled read-only
2021-08-22 14:45:57 -06:00
Daz DeBoer
a148b21183 Improve prod workflow
- Use a separate job to test read-only cache
- Use dependency jobs to avoid cache race conditions
2021-08-22 14:42:01 -06:00
Daz DeBoer
e7422f245c Fix typo in README 2021-08-22 14:42:01 -06:00
Daz DeBoer
c86093d76a Build distribution 2021-08-22 14:28:35 -06:00
Daz DeBoer
a693ccda4b Allow use of caches 'read-only'
To avoid evicting useful entries, some pipeline may benefit from using existing
cache entries without writing any changes back to the cache.

Fixes #62
2021-08-22 14:28:34 -06:00
Daz DeBoer
543cacb256 Allow manual trigger of prod workflow 2021-08-20 14:00:30 -06:00
10 changed files with 127 additions and 28 deletions

View File

@@ -4,9 +4,25 @@ name: prod
on: on:
pull_request: pull_request:
push: push:
workflow_dispatch:
jobs: jobs:
basic-build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Build using Gradle wrapper
uses: ./
with:
build-root-directory: __tests__/samples/basic
arguments: test
gradle-execution: gradle-execution:
needs: basic-build
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
@@ -17,11 +33,6 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Test use Gradle wrapper
uses: ./
with:
build-root-directory: __tests__/samples/basic
arguments: test
- name: Test use defined Gradle version - name: Test use defined Gradle version
uses: ./ uses: ./
with: with:
@@ -48,6 +59,7 @@ jobs:
arguments: help arguments: help
dependencies-cache: dependencies-cache:
needs: basic-build
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
@@ -63,6 +75,7 @@ jobs:
dependencies-cache-enabled: true dependencies-cache-enabled: true
configuration-cache: configuration-cache:
needs: basic-build
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
@@ -79,8 +92,26 @@ jobs:
dependencies-cache-enabled: true dependencies-cache-enabled: true
# Configuration cache requires dependencies cache, since it assumes dependencies are already downloaded. # Configuration cache requires dependencies cache, since it assumes dependencies are already downloaded.
cache-read-only:
needs: basic-build
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Test cache-read-only
uses: ./
with:
build-root-directory: __tests__/samples/basic
arguments: test --no-daemon
dependencies-cache-enabled: true
configuration-cache-enabled: true
cache-read-only: true
failures: # These build invocations are informational only, and are expected to fail failures: # These build invocations are informational only, and are expected to fail
needs: basic-build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources

View File

@@ -144,7 +144,7 @@ The distributions cache is simple and can't be configured further.
The dependencies and configuration cache will compute a cache key in a best effort manner. 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. Keep reading to learn how to better control how they work.
Note that enabling configuration cache without thee dependencies cache is not permitted, since a hit in the configuration cache assumes that dependencies are already present in the local dependencies cache. Note that enabling configuration cache without the dependencies cache is not permitted, since a hit in the configuration cache assumes that dependencies are already present in the local dependencies cache.
### Configuring the dependencies and configuration caches ### Configuring the dependencies and configuration caches
@@ -187,6 +187,18 @@ dependencies-cache-key: gradle/dependency-locks/**
dependencies-cache-exact: true dependencies-cache-exact: true
``` ```
### Using the caches read-only
Cache storage space is limited for GitHub actions, and writing new cache entries can trigger the deletion of exising entries.
In some circumstances, it makes sense for a Gradle invocation to use any existing cache entries but not to write and changes back.
For example, you may want to write cache entries for builds on your `main` branch, but not for any PR build invocations.
Use the following configuration to avoid writing cache entries for the action invocation:
```yaml
cache-read-only: true
```
## Build scans ## Build scans
If your build publishes a [build scan](https://gradle.com/build-scans/) the `gradle-build-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-build-action` action will emit the link to the published build scan as an output named `build-scan-url`.

View File

@@ -1,10 +1,10 @@
import * as cryptoUtils from '../src/crypto-utils' import * as cacheUtils from '../src/cache-utils'
import * as path from 'path' import * as path from 'path'
describe('crypto-utils', () => { describe('cacheUtils-utils', () => {
describe('can hash', () => { describe('can hash', () => {
it('a directory', async () => { it('a directory', async () => {
const hash = await cryptoUtils.hashFiles( const hash = await cacheUtils.hashFiles(
path.resolve('__tests__/data/crypto-utils-test/gradle') path.resolve('__tests__/data/crypto-utils-test/gradle')
) )
expect(hash).toBe( expect(hash).toBe(
@@ -14,7 +14,7 @@ describe('crypto-utils', () => {
) )
}) })
it('a directory with a glob', async () => { it('a directory with a glob', async () => {
const hash = await cryptoUtils.hashFiles( const hash = await cacheUtils.hashFiles(
path.resolve('__tests__/data/crypto-utils-test/'), path.resolve('__tests__/data/crypto-utils-test/'),
['gradle/**'] ['gradle/**']
) )
@@ -25,7 +25,7 @@ describe('crypto-utils', () => {
) )
}) })
it('a directory with globs', async () => { it('a directory with globs', async () => {
const hash = await cryptoUtils.hashFiles( const hash = await cacheUtils.hashFiles(
path.resolve('__tests__/data/crypto-utils-test/'), path.resolve('__tests__/data/crypto-utils-test/'),
['**/*.gradle', 'gradle/**'] ['**/*.gradle', 'gradle/**']
) )
@@ -36,4 +36,30 @@ describe('crypto-utils', () => {
) )
}) })
}) })
describe('can truncate args', () => {
test('handles zero-length string', () => {
expect(cacheUtils.truncateArgs('')).toBe('')
})
test('leaves short string untouched', () => {
expect(
cacheUtils.truncateArgs('short string that-should-be-untouched')
).toBe('short string that-should-be-untouched')
})
test('truncates long string', () => {
const longString = 'a'.repeat(500)
expect(cacheUtils.truncateArgs(longString)).toBe('a'.repeat(400))
})
test('trims leading and trailing whitespace', () => {
expect(cacheUtils.truncateArgs(' this is an arg ')).toBe(
'this is an arg'
)
})
test('removes repeated whitespace', () => {
expect(
cacheUtils.truncateArgs(
' this one has long \t\n\t\r spaces '
)
).toBe('this one has long spaces')
})
})
}) })

View File

@@ -1,5 +1,5 @@
name: "Gradle Build Action" name: "Gradle Build Action"
description: 'Execute Gradle Build' description: 'Executes a Gradle build, caching useful state in the GitHub actions cache'
# https://help.github.com/en/articles/metadata-syntax-for-github-actions # https://help.github.com/en/articles/metadata-syntax-for-github-actions
@@ -51,6 +51,10 @@ inputs:
description: Whether to restore only if exact match, default to 'false' description: Whether to restore only if exact match, default to 'false'
required: false required: false
default: false default: false
cache-read-only:
description: When 'true', existing entries will be read from the cache but no entries will be written
required: false
default: false
outputs: outputs:
build-scan-url: 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

@@ -1,10 +1,10 @@
import path from 'path' import * as path from 'path'
import fs from 'fs' import * as fs from 'fs'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as cache from '@actions/cache' import * as cache from '@actions/cache'
import * as crypto from './crypto-utils' import * as cacheUtils from './cache-utils'
import { import {
inputCacheKeyGlobs, inputCacheKeyGlobs,
@@ -32,17 +32,22 @@ export async function restoreCachedConfiguration(
core.saveState(CONFIGURATION_CACHE_PATH, cachePath) core.saveState(CONFIGURATION_CACHE_PATH, cachePath)
const inputCacheExact = core.getBooleanInput('configuration-cache-exact') const inputCacheExact = core.getBooleanInput('configuration-cache-exact')
const cacheKeyGlobs = inputCacheKeyGlobs('configuration-cache-key') const cacheKeyPrefix = 'configuration|'
const args = core.getInput('arguments')
const argsKey = cacheUtils.truncateArgs(args)
const cacheKeyWithArgs = `${cacheKeyPrefix}${argsKey}|`
const cacheKeyGlobs = inputCacheKeyGlobs('configuration-cache-key')
const hash = await cacheUtils.hashFiles(rootDir, cacheKeyGlobs)
const cacheKey = `${cacheKeyWithArgs}${hash}`
const hash = await crypto.hashFiles(rootDir, cacheKeyGlobs)
const cacheKeyPrefix = 'configuration-'
const cacheKey = `${cacheKeyPrefix}${hash}`
core.saveState(CONFIGURATION_CACHE_KEY, cacheKey) core.saveState(CONFIGURATION_CACHE_KEY, cacheKey)
const cacheResult = await cache.restoreCache( const cacheResult = await cache.restoreCache(
[cachePath], [cachePath],
cacheKey, cacheKey,
inputCacheExact ? [] : [cacheKeyPrefix] inputCacheExact ? [] : [cacheKeyWithArgs, cacheKeyPrefix]
) )
if (!cacheResult) { if (!cacheResult) {
@@ -86,6 +91,8 @@ export async function cacheConfiguration(): Promise<void> {
return return
} }
core.info(`Will cache configuration with key ${cacheKey}`)
try { try {
await cache.saveCache([cachePath], cacheKey) await cache.saveCache([cachePath], cacheKey)
} catch (error) { } catch (error) {

View File

@@ -5,7 +5,7 @@ import * as os from 'os'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as cache from '@actions/cache' import * as cache from '@actions/cache'
import * as crypto from './crypto-utils' import * as cacheUtils from './cache-utils'
const DEPENDENCIES_CACHE_PATH = 'DEPENDENCIES_CACHE_PATH' const DEPENDENCIES_CACHE_PATH = 'DEPENDENCIES_CACHE_PATH'
const DEPENDENCIES_CACHE_KEY = 'DEPENDENCIES_CACHE_KEY' const DEPENDENCIES_CACHE_KEY = 'DEPENDENCIES_CACHE_KEY'
@@ -21,17 +21,22 @@ export async function restoreCachedDependencies(
core.saveState(DEPENDENCIES_CACHE_PATH, cachePath) core.saveState(DEPENDENCIES_CACHE_PATH, cachePath)
const inputCacheExact = core.getBooleanInput('dependencies-cache-exact') const inputCacheExact = core.getBooleanInput('dependencies-cache-exact')
const cacheKeyGlobs = inputCacheKeyGlobs('dependencies-cache-key') const cacheKeyPrefix = 'dependencies|'
const args = core.getInput('arguments')
const argsKey = cacheUtils.truncateArgs(args)
const cacheKeyWithArgs = `${cacheKeyPrefix}${argsKey}|`
const cacheKeyGlobs = inputCacheKeyGlobs('dependencies-cache-key')
const hash = await cacheUtils.hashFiles(rootDir, cacheKeyGlobs)
const cacheKey = `${cacheKeyWithArgs}${hash}`
const hash = await crypto.hashFiles(rootDir, cacheKeyGlobs)
const cacheKeyPrefix = 'dependencies-'
const cacheKey = `${cacheKeyPrefix}${hash}`
core.saveState(DEPENDENCIES_CACHE_KEY, cacheKey) core.saveState(DEPENDENCIES_CACHE_KEY, cacheKey)
const cacheResult = await cache.restoreCache( const cacheResult = await cache.restoreCache(
[cachePath], [cachePath],
cacheKey, cacheKey,
inputCacheExact ? [] : [cacheKeyPrefix] inputCacheExact ? [] : [cacheKeyWithArgs, cacheKeyPrefix]
) )
if (!cacheResult) { if (!cacheResult) {
@@ -73,6 +78,8 @@ export async function cacheDependencies(): Promise<void> {
return return
} }
core.info(`Will cache dependencies with key ${cacheKey}`)
try { try {
await cache.saveCache([cachePath], cacheKey) await cache.saveCache([cachePath], cacheKey)
} catch (error) { } catch (error) {

View File

@@ -11,3 +11,7 @@ export async function hashFiles(
.join('\n') .join('\n')
return glob.hashFiles(combinedPatterns, {followSymbolicLinks}) return glob.hashFiles(combinedPatterns, {followSymbolicLinks})
} }
export function truncateArgs(args: string): string {
return args.trim().replace(/\s+/g, ' ').substr(0, 400)
}

View File

@@ -1,12 +1,20 @@
import * as core from '@actions/core'
import * as cacheWrapper from './cache-wrapper' import * as cacheWrapper from './cache-wrapper'
import * as cacheDependencies from './cache-dependencies' import * as cacheDependencies from './cache-dependencies'
import * as cacheConfiguration from './cache-configuration' 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> {
if (isCacheReadOnly()) return
await cacheWrapper.cacheWrapperDist() await cacheWrapper.cacheWrapperDist()
await cacheDependencies.cacheDependencies() await cacheDependencies.cacheDependencies()
await cacheConfiguration.cacheConfiguration() await cacheConfiguration.cacheConfiguration()
} }
function isCacheReadOnly(): boolean {
return core.getBooleanInput('cache-read-only')
}
run() run()