Compare commits

...

27 Commits

Author SHA1 Message Date
Daz DeBoer
4137be6a8b Minor improvement to logging in post-action
- Ensure that "Caching Gradle state" group always has 1 message
- Only print cache report when entries were restored or saved
2021-11-05 08:35:45 -06:00
Daz DeBoer
4e899835b3 Avoid failing build on distributions cache errors
- Warn and continue on failure to restore a Gradle distribution from cache
- Warn and continue on failure to save a Gradle distribution to cache
- Extract common functionality for consistent handling of cache failures

Fixes #116
2021-11-05 07:01:48 -06:00
Jonathan Leitschuh
3812292b26 Add CodeQL Analysis (#114) 2021-11-02 09:56:02 -06:00
Daz DeBoer
c12283ec57 Add a section explaining advantages 2021-11-01 12:35:15 -06:00
Daz DeBoer
f2dc0d8256 Add section on optimizing cache effectiveness 2021-11-01 08:17:33 -06:00
Daz DeBoer
717db318c1 Fix logging of cache key for Gradle User Home 2021-10-31 20:49:29 -06:00
Daz DeBoer
2a57ddf74a Merge pull request #111 from gradle/dd/cache-report
Add a report to assist with diagnosing and optimizing cache performance
2021-10-30 21:53:01 +02:00
Daz DeBoer
230fd6b47f Build outputs 2021-10-30 13:46:29 -06:00
Daz DeBoer
472ac8a356 Report sizes of cache entries
Using the patched version of @actions/cache, we now report the total
size of cache entries restored/saved, as well as details of each one.
2021-10-30 13:45:39 -06:00
Daz DeBoer
3ba05ede1f Add patch for @actions/cache to expose entry size
There's no easy way to get the size of restored/saved cache entries
using the @actions/cache library. Rather than reimplement it, this commit
adds a patch that will be applied whenever 'npm install' is run.

The work is done by 'patch-package'.
2021-10-30 13:44:24 -06:00
Daz DeBoer
d785346c8c Use cache.description 2021-10-30 13:44:22 -06:00
Daz DeBoer
6ca4d4ade2 Improve formatting for caching report 2021-10-30 12:15:00 -06:00
Daz DeBoer
75cec40e58 Rename 'report' classes to 'listener'
- `CachingReport` -> `CacheListener`
- `CacheEntryReport` -> `CacheEntryListener`
2021-10-30 12:15:00 -06:00
Daz DeBoer
c317ccac62 Refactor: extract cache-base out of cache-utils 2021-10-30 12:15:00 -06:00
Daz DeBoer
a74bb0fad6 Monitor cache saves and add basic caching report
- Restore `CachingReport` instance in 'post' action
- Record keys for any entries saved
- Report caching activity as JSON in post action
2021-10-30 12:14:59 -06:00
Daz DeBoer
6ff2065a12 Upgrade Jest to address security warning 2021-10-30 12:11:59 -06:00
Daz DeBoer
727b4612ba Fix early return from bundle processing 2021-10-30 12:11:59 -06:00
Daz DeBoer
613f4ec588 Merge pull request #109 from gradle/dd/rc21
Fix issues for 2.0-rc.2
- Support multi-line strings for cache-tuning parameters #106 
- Include all downloaded files in `dependencies` bundle #100 
- Only restore configuration-cache if Gradle User Home is fully restored #107
2021-10-30 14:39:28 +02:00
Daz DeBoer
db6202adcd Build outputs 2021-10-30 06:29:25 -06:00
Daz DeBoer
f0f68e07c3 Better handling of .cache files that are not restored
- Remove any .cache file that is not restored
- Report on any .cache file that exists but has no config
2021-10-30 06:28:20 -06:00
Daz DeBoer
8ba5a0033b Track 'fully-restored' by tracking each cache restore
Instead of tracking a single 'fully-restored' flag, track the restore status of each
cache entry restore. If any of these are requested but not restored, then the overall
Gradle User Home cache is not fully restored.

Added special handling for the case when zero artifact bundles are set: this is used
in tests to simulate a not-fully-restored state.
2021-10-30 06:05:21 -06:00
Daz DeBoer
9edc2a11bd Only restore configuration-cache if Gradle Home is fully restored
Fixes #107
2021-10-29 09:34:36 -06:00
Daz DeBoer
079e4844d6 Split tests for caching
- Separate testing of Gradle Home caching from configuration-cache caching
- Add test for configuration-cache when Gradle Home is not fully restored
2021-10-29 09:34:35 -06:00
Daz DeBoer
4ebd000afd Bundle all downloaded dependency files
Previously, only .jar files were bundled, with other files (modules, POMs, zips, etc)
being left in Gradle User Home. All downloaded files are now included in the bundle.

Fixes #100
2021-10-29 08:03:03 -06:00
Daz DeBoer
063fc6a872 Allow source files to contain lines up to 120 characters
This avoids excessive line-feeds when reformatting code to 80 char lines.
2021-10-29 07:50:06 -06:00
Daz DeBoer
e3ada7e5c2 Use multiline input parameters instead of JSON input
The `gradle-home-cache-includes` and `gradle-home-cache-excludes` parameters were initially implemented
as JSON string inputs. This makes these inputs non-idiomatic and easier to get wrong.

This change converts them to multi-line input parameters.

Fixes #106
2021-10-29 07:29:57 -06:00
Daz DeBoer
d61e5be06a Remove note about Beta release from README 2021-10-27 16:27:21 -06:00
26 changed files with 5639 additions and 3016 deletions

View File

@@ -11,7 +11,7 @@
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",

70
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '25 23 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -28,13 +28,15 @@ jobs:
# Add "wrapper" to main cache entry and remove 'wrapper-zips' bundle
# Exclude build-cache from main cache entry
gradle-home-cache-includes: |
["caches", "notifications", "wrapper"]
caches
notifications
wrapper
gradle-home-cache-excludes: |
["caches/build-cache-1"]
caches/build-cache-1
gradle-home-cache-artifact-bundles: |
[
["generated-gradle-jars", "caches/*/generated-gradle-jars/*.jar"],
["dependency-jars", "caches/modules-*/files-*/**/*.jar"],
["dependencies", "caches/modules-*/files-*/*/*/*/*/"],
["instrumented-jars", "caches/jars-*/*/"],
["kotlin-dsl", "caches/*/kotlin-dsl/*/*/"]
]
@@ -57,13 +59,15 @@ jobs:
cache-read-only: true
# Need the same configuration when restoring state from cache
gradle-home-cache-includes: |
["caches", "notifications", "wrapper"]
caches
notifications
wrapper
gradle-home-cache-excludes: |
["caches/build-cache-1"]
caches/build-cache-1
gradle-home-cache-artifact-bundles: |
[
["generated-gradle-jars", "caches/*/generated-gradle-jars/*.jar"],
["dependency-jars", "caches/modules-*/files-*/**/*.jar"],
["dependencies", "caches/modules-*/files-*/*/*/*/*/"],
["instrumented-jars", "caches/jars-*/*/"],
["kotlin-dsl", "caches/*/kotlin-dsl/*/*/"]
]

View File

@@ -0,0 +1,65 @@
name: Test save/restore configuration-cache state
on:
pull_request:
push:
workflow_dispatch:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: ${{github.workflow}}#${{github.run_number}}-
GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true
jobs:
# Run initial Gradle builds to push initial cache entries
# These builds should start fresh without cache hits, due to the seed injected into the cache key above.
seed-build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Build with configuration-cache enabled
uses: ./
with:
build-root-directory: __tests__/samples/groovy-dsl
arguments: test --configuration-cache
# Test that the project-dot-gradle cache will cache and restore configuration-cache
configuration-cache:
needs: seed-build
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Execute Gradle build and verify cached configuration
uses: ./
env:
VERIFY_CACHED_CONFIGURATION: true
with:
build-root-directory: __tests__/samples/groovy-dsl
arguments: test --configuration-cache
cache-read-only: true
# Check that the build can run when no bundles are restored
no-bundles-restored:
needs: seed-build
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Execute Gradle build with no cache artifact bundles restored
uses: ./
with:
build-root-directory: __tests__/samples/groovy-dsl
arguments: test --configuration-cache
cache-read-only: true
gradle-home-cache-artifact-bundles: '[]'

View File

@@ -1,4 +1,4 @@
name: Test caching
name: Test save/restore Gradle Home directory
on:
pull_request:
@@ -24,11 +24,6 @@ jobs:
with:
build-root-directory: __tests__/samples/groovy-dsl
arguments: test
- name: Build with configuration-cache enabled
uses: ./
with:
build-root-directory: __tests__/samples/groovy-dsl
arguments: test --configuration-cache
# Test that the gradle-user-home cache will cache dependencies, by running build with --offline
dependencies-cache:
@@ -64,25 +59,6 @@ jobs:
arguments: test -DverifyCachedBuild=true
cache-read-only: true
# Test that the project-dot-gradle cache will cache and restore configuration-cache
configuration-cache:
needs: seed-build
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
- name: Execute Gradle build and verify cached configuration
uses: ./
env:
VERIFY_CACHED_CONFIGURATION: true
with:
build-root-directory: __tests__/samples/groovy-dsl
arguments: test --configuration-cache
cache-read-only: true
# Check that the build can run when no bundles are restored
no-bundles-restored:
needs: seed-build

View File

@@ -1,5 +1,5 @@
{
"printWidth": 80,
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": false,

View File

@@ -2,9 +2,6 @@
This GitHub Action can be used to execute a Gradle build on any platform supported by GitHub Actions.
**Note:** The following documentation is for `gradle/gradle-build-action@v2`, currently in Beta release.
You can view the documentation for the latest stable release (v1.5.1) [on the GitHub Marketplace](https://github.com/marketplace/actions/gradle-build-action?version=v1.5.1).
## Usage
The following workflow will run `./gradlew build` on ubuntu, macos and windows.
@@ -42,6 +39,19 @@ Each invocation will start its run with the filesystem state remaining from the
arguments: check
```
### Why is this better than running Gradle directly?
It is possible to directly invoke Gradle in your workflow, and the `setup-java` action provides a simple way to cache Gradle dependencies.
However, the `gradle-build-action` offers a number of advantages over this approach:
- Easily [run the build with different versions of Gradle](#download-install-and-use-a-specific-gradle-version) using the `gradle-version` parameter. Gradle distributions are automatically downloaded and cached.
- More sophisticated and more efficient caching of Gradle User Home between invocations, compared to `setup-java` and most custom configurations using `actions/cache`. [More details below](#caching).
- Detailed reporting of cache usage and cache configuration options allow you to [optimize the use of the GitHub actions cache](#optimizing-cache-effectiveness).
- [Automatic capture of build scan links](#build-scans) from the build, making these easier to locate for workflow run.
The `gradle-build-action` is designed to provide these benefits with minimal configuration.
## Gradle Execution
### Command-line arguments
@@ -161,7 +171,7 @@ cache-disabled: true
### Cache keys
For cached distributions outside of Gradle User Home, the cache key is unique to the downloaded distribution. This will not change over time.
For distributions downloaded to satisfy a `gradle-version` parametere are stored outside of Gradle User Home and cached separately. The cache key is unique to the downloaded distribution and will not change over time.
The state of the Gradle User Home and configuration-cache are highly dependent on the Gradle execution, so the cache key is composed of the current commit hash and the GitHub actions job id.
As such, the cache key is likely to change on each subsequent run of GitHub actions.
@@ -173,7 +183,6 @@ For example, this means that all jobs executing a particular version of the Grad
### 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 read any existing cache entries but not to write changes back.
For example, you may want to write cache entries for builds on your `main` branch, but not for any PR build invocations.
@@ -194,16 +203,23 @@ The contents to be cached can be fine tuned by including and excluding certain p
```yaml
# Cache downloaded JDKs in addition to the default directories.
gradle-home-cache-includes: |
["caches", "notifications", "jdks"]
caches
notifications
jdks
# Exclude the local build-cache from the directories cached.
gradle-home-cache-excludes: |
["caches/build-cache-1"]
caches/build-cache-1
```
You can specify any number of fixed paths or patterns to include or exclude.
File pattern support is documented at https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#patterns-to-match-file-paths.
### Cache debugging
### Cache debugging and analysis
Gradle User Home state will be restored from the cache during the first `gradle-build-action` step for any workflow job.
This state will be saved back to the cache at the end of the job, after all Gradle executions have completed.
A report of all cache entries restored and saved is printed to the action log when saving the cache entries.
This report can provide valuable insignt into how much cache space is being used.
It is possible to enable additional debug logging for cache operations. You do via the `GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED` environment variable:
@@ -214,6 +230,29 @@ env:
Note that this setting will also prevent certain cache operations from running in parallel, further assisting with debugging.
### Optimizing cache effectiveness
Cache storage space for GitHub actions is limited, and writing new cache entries can trigger the deletion of exising entries.
Eviction of shared cache entries can reduce cache effectiveness, slowing down your `gradle-build-action` steps.
There are a number of actions you can take if your cache use is less effective due to entry eviction.
#### Only write to the cache from the default branch
GitHub cache entries are not shared between builds on different branches. This means that identical cache entries will be stored separately for different branches.
The exception to the is cache entries for the default (`master`/`main`) branch can be read by actions invoked for other branches.
An easy way to reduce cache usage when you run builds on many different branches is to only permit your default branch to write to the cache,
with all other branch builds using `cache-read-only`. See [Using the caches read-only](#using-the-caches-read-only) for more details.
Similarly, you could use `cache-read-only` for certain jobs in the workflow, and instead have these jobs reuse the cache content from upstream jobs.
#### Exclude content from Gradle User Home cache
Each build is different, and some builds produce more Gradle User Home content than others.
[Cache debugging ](#cache-debugging-and-analysis) can provide insight into which cache entries are the largest,
and you can selectively [exclude content using `gradle-home-cache-exclude`](#gradle-user-home-cache-tuning).
## Build scans
If your build publishes a [build scan](https://gradle.com/build-scans/) the `gradle-build-action` action will:

View File

@@ -0,0 +1,95 @@
import {CacheEntryListener, CacheListener} from '../src/cache-base'
describe('caching report', () => {
describe('reports not fully restored', () => {
it('with one requested entry report', async () => {
const report = new CacheListener()
report.entry('foo').markRequested('1', ['2'])
report.entry('bar').markRequested('3').markRestored('4', 500)
expect(report.fullyRestored).toBe(false)
})
})
describe('reports fully restored', () => {
it('when empty', async () => {
const report = new CacheListener()
expect(report.fullyRestored).toBe(true)
})
it('with empty entry reports', async () => {
const report = new CacheListener()
report.entry('foo')
report.entry('bar')
expect(report.fullyRestored).toBe(true)
})
it('with restored entry report', async () => {
const report = new CacheListener()
report.entry('bar').markRequested('3').markRestored('4', 300)
expect(report.fullyRestored).toBe(true)
})
it('with multiple restored entry reportss', async () => {
const report = new CacheListener()
report.entry('foo').markRestored('4', 3300)
report.entry('bar').markRequested('3').markRestored('4', 333)
expect(report.fullyRestored).toBe(true)
})
})
describe('can be stringified and rehydrated', () => {
it('when empty', async () => {
const report = new CacheListener()
const stringRep = report.stringify()
const reportClone: CacheListener = CacheListener.rehydrate(stringRep)
expect(reportClone.cacheEntries).toEqual([])
// Can call methods on rehydrated
expect(reportClone.entry('foo')).toBeInstanceOf(CacheEntryListener)
})
it('with entry reports', async () => {
const report = new CacheListener()
report.entry('foo')
report.entry('bar')
report.entry('baz')
const stringRep = report.stringify()
const reportClone: CacheListener = CacheListener.rehydrate(stringRep)
expect(reportClone.cacheEntries.length).toBe(3)
expect(reportClone.cacheEntries[0].entryName).toBe('foo')
expect(reportClone.cacheEntries[1].entryName).toBe('bar')
expect(reportClone.cacheEntries[2].entryName).toBe('baz')
expect(reportClone.entry('foo')).toBe(reportClone.cacheEntries[0])
})
it('with rehydrated entry report', async () => {
const report = new CacheListener()
const entryReport = report.entry('foo')
entryReport.markRequested('1', ['2', '3'])
entryReport.markSaved('4', 100)
const stringRep = report.stringify()
const reportClone: CacheListener = CacheListener.rehydrate(stringRep)
const entryClone = reportClone.entry('foo')
expect(entryClone.requestedKey).toBe('1')
expect(entryClone.requestedRestoreKeys).toEqual(['2', '3'])
expect(entryClone.savedKey).toBe('4')
})
it('with live entry report', async () => {
const report = new CacheListener()
const entryReport = report.entry('foo')
entryReport.markRequested('1', ['2', '3'])
const stringRep = report.stringify()
const reportClone: CacheListener = CacheListener.rehydrate(stringRep)
const entryClone = reportClone.entry('foo')
// Check type and call method on rehydrated entry report
expect(entryClone).toBeInstanceOf(CacheEntryListener)
entryClone.markSaved('4', 100)
expect(entryClone.requestedKey).toBe('1')
expect(entryClone.requestedRestoreKeys).toEqual(['2', '3'])
expect(entryClone.savedKey).toBe('4')
})
})
})

View File

@@ -1,5 +1,4 @@
import * as cacheUtils from '../src/cache-utils'
import * as path from 'path'
describe('cacheUtils-utils', () => {
describe('can hash', () => {

View File

@@ -36,13 +36,13 @@ inputs:
description: Paths within Gradle User Home to cache.
required: false
default: |
["caches", "notifications"]
caches
notifications
gradle-home-cache-excludes:
description: Paths within Gradle User Home to exclude from cache.
required: false
default: |
[]
# e.g. Use the following setting to prevent the local build cache from being saved/restored
# gradle-home-cache-excludes: |
# ["caches/build-cache-1"]
@@ -62,7 +62,7 @@ inputs:
[
["generated-gradle-jars", "caches/*/generated-gradle-jars/*.jar"],
["wrapper-zips", "wrapper/dists/*/*/*.zip"],
["dependency-jars", "caches/modules-*/files-*/**/*.jar"],
["dependencies", "caches/modules-*/files-*/*/*/*/*/"],
["instrumented-jars", "caches/jars-*/*/"],
["kotlin-dsl", "caches/*/kotlin-dsl/*/*/"]
]

2
dist/main/index.js vendored

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

7370
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
"private": true,
"description": "Execute Gradle Build",
"scripts": {
"postinstall": "patch-package",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"lint": "eslint src/**/*.ts",
@@ -21,7 +22,6 @@
"github-actions",
"gradle"
],
"author": "Paul Merlin <paul@nosphere.org>",
"license": "MIT",
"dependencies": {
"@actions/cache": "1.0.7",
@@ -31,10 +31,11 @@
"@actions/glob": "0.2.0",
"@actions/http-client": "1.0.11",
"@actions/tool-cache": "1.7.1",
"patch-package": "6.4.7",
"string-argv": "0.3.1"
},
"devDependencies": {
"@types/jest": "26.0.23",
"@types/jest": "27.0.2",
"@types/node": "14.17.3",
"@types/unzipper": "0.10.4",
"@typescript-eslint/parser": "4.28.2",
@@ -42,11 +43,11 @@
"eslint": "7.30.0",
"eslint-plugin-github": "4.1.3",
"eslint-plugin-jest": "24.3.6",
"jest": "26.6.3",
"jest-circus": "26.6.3",
"jest": "27.3.1",
"jest-circus": "27.3.1",
"js-yaml": "3.14.1",
"prettier": "2.3.2",
"ts-jest": "26.5.6",
"ts-jest": "27.0.7",
"typescript": "4.3.5"
}
}

View File

@@ -0,0 +1,92 @@
diff --git a/node_modules/@actions/cache/lib/cache.d.ts b/node_modules/@actions/cache/lib/cache.d.ts
index 805a8e5..d3ab419 100644
--- a/node_modules/@actions/cache/lib/cache.d.ts
+++ b/node_modules/@actions/cache/lib/cache.d.ts
@@ -5,6 +5,11 @@ export declare class ValidationError extends Error {
export declare class ReserveCacheError extends Error {
constructor(message: string);
}
+export declare class CacheEntry {
+ key: string;
+ size?: number;
+ constructor(key: string, size?: number);
+}
/**
* Restores cache from keys
*
@@ -14,7 +19,7 @@ export declare class ReserveCacheError extends Error {
* @param downloadOptions cache download options
* @returns string returns the key for the cache hit, otherwise returns undefined
*/
-export declare function restoreCache(paths: string[], primaryKey: string, restoreKeys?: string[], options?: DownloadOptions): Promise<string | undefined>;
+export declare function restoreCache(paths: string[], primaryKey: string, restoreKeys?: string[], options?: DownloadOptions): Promise<CacheEntry | undefined>;
/**
* Saves a list of files with the specified key
*
@@ -23,4 +28,4 @@ export declare function restoreCache(paths: string[], primaryKey: string, restor
* @param options cache upload options
* @returns number returns cacheId if the cache was saved successfully and throws an error if save fails
*/
-export declare function saveCache(paths: string[], key: string, options?: UploadOptions): Promise<number>;
+export declare function saveCache(paths: string[], key: string, options?: UploadOptions): Promise<CacheEntry>;
diff --git a/node_modules/@actions/cache/lib/cache.js b/node_modules/@actions/cache/lib/cache.js
index df78fe0..540114f 100644
--- a/node_modules/@actions/cache/lib/cache.js
+++ b/node_modules/@actions/cache/lib/cache.js
@@ -37,6 +37,13 @@ class ReserveCacheError extends Error {
}
}
exports.ReserveCacheError = ReserveCacheError;
+class CacheEntry {
+ constructor(key, size) {
+ this.key = key;
+ this.size = size;
+ }
+}
+exports.CacheEntry = CacheEntry;
function checkPaths(paths) {
if (!paths || paths.length === 0) {
throw new ValidationError(`Path Validation Error: At least one directory or file path is required`);
@@ -84,6 +91,7 @@ function restoreCache(paths, primaryKey, restoreKeys, options) {
}
const archivePath = path.join(yield utils.createTempDirectory(), utils.getCacheFileName(compressionMethod));
core.debug(`Archive Path: ${archivePath}`);
+ const restoredEntry = new CacheEntry(cacheEntry.cacheKey);
try {
// Download the cache from the cache entry
yield cacheHttpClient.downloadCache(cacheEntry.archiveLocation, archivePath, options);
@@ -91,6 +99,7 @@ function restoreCache(paths, primaryKey, restoreKeys, options) {
yield tar_1.listTar(archivePath, compressionMethod);
}
const archiveFileSize = utils.getArchiveFileSizeIsBytes(archivePath);
+ restoredEntry.size = archiveFileSize;
core.info(`Cache Size: ~${Math.round(archiveFileSize / (1024 * 1024))} MB (${archiveFileSize} B)`);
yield tar_1.extractTar(archivePath, compressionMethod);
core.info('Cache restored successfully');
@@ -104,7 +113,7 @@ function restoreCache(paths, primaryKey, restoreKeys, options) {
core.debug(`Failed to delete archive: ${error}`);
}
}
- return cacheEntry.cacheKey;
+ return restoredEntry;
});
}
exports.restoreCache = restoreCache;
@@ -147,7 +156,7 @@ function saveCache(paths, key, options) {
}
core.debug(`Saving Cache (ID: ${cacheId})`);
yield cacheHttpClient.saveCache(cacheId, archivePath, options);
- return cacheId;
+ return new CacheEntry(key, archiveFileSize);
});
}
exports.saveCache = saveCache;
diff --git a/node_modules/@actions/cache/lib/cache.js.map b/node_modules/@actions/cache/lib/cache.js.map
index 05fc369..41b9189 100644
--- a/node_modules/@actions/cache/lib/cache.js.map
+++ b/node_modules/@actions/cache/lib/cache.js.map
@@ -1 +1 @@
-{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
\ No newline at end of file

244
src/cache-base.ts Normal file
View File

@@ -0,0 +1,244 @@
import * as core from '@actions/core'
import * as cache from '@actions/cache'
import * as github from '@actions/github'
import {isCacheDebuggingEnabled, getCacheKeyPrefix, hashStrings, handleCacheFailure} from './cache-utils'
const JOB_CONTEXT_PARAMETER = 'workflow-job-context'
function generateCacheKey(cacheName: string): CacheKey {
const cacheKeyPrefix = getCacheKeyPrefix()
// At the most general level, share caches for all executions on the same OS
const runnerOs = process.env['RUNNER_OS'] || ''
const cacheKeyForOs = `${cacheKeyPrefix}${cacheName}|${runnerOs}`
// Prefer caches that run this job
const cacheKeyForJob = `${cacheKeyForOs}|${github.context.job}`
// Prefer (even more) jobs that run this job with the same context (matrix)
const cacheKeyForJobContext = `${cacheKeyForJob}[${determineJobContext()}]`
// Exact match on Git SHA
const cacheKey = `${cacheKeyForJobContext}-${github.context.sha}`
return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForOs])
}
function determineJobContext(): string {
// By default, we hash the full `matrix` data for the run, to uniquely identify this job invocation
const workflowJobContext = core.getInput(JOB_CONTEXT_PARAMETER)
return hashStrings([workflowJobContext])
}
class CacheKey {
key: string
restoreKeys: string[]
constructor(key: string, restoreKeys: string[]) {
this.key = key
this.restoreKeys = restoreKeys
}
}
export class CacheListener {
cacheEntries: CacheEntryListener[] = []
get fullyRestored(): boolean {
return this.cacheEntries.every(x => !x.wasRequestedButNotRestored())
}
entry(name: string): CacheEntryListener {
for (const entry of this.cacheEntries) {
if (entry.entryName === name) {
return entry
}
}
const newEntry = new CacheEntryListener(name)
this.cacheEntries.push(newEntry)
return newEntry
}
stringify(): string {
return JSON.stringify(this)
}
static rehydrate(stringRep: string): CacheListener {
const rehydrated: CacheListener = Object.assign(new CacheListener(), JSON.parse(stringRep))
const entries = rehydrated.cacheEntries
for (let index = 0; index < entries.length; index++) {
const rawEntry = entries[index]
entries[index] = Object.assign(new CacheEntryListener(rawEntry.entryName), rawEntry)
}
return rehydrated
}
}
export class CacheEntryListener {
entryName: string
requestedKey: string | undefined
requestedRestoreKeys: string[] | undefined
restoredKey: string | undefined
restoredSize: number | undefined
savedKey: string | undefined
savedSize: number | undefined
constructor(entryName: string) {
this.entryName = entryName
}
wasRequestedButNotRestored(): boolean {
return this.requestedKey !== undefined && this.restoredKey === undefined
}
markRequested(key: string, restoreKeys: string[] = []): CacheEntryListener {
this.requestedKey = key
this.requestedRestoreKeys = restoreKeys
return this
}
markRestored(key: string, size: number | undefined): CacheEntryListener {
this.restoredKey = key
this.restoredSize = size
return this
}
markSaved(key: string, size: number | undefined): CacheEntryListener {
this.savedKey = key
this.savedSize = size
return this
}
}
export abstract class AbstractCache {
private cacheName: string
private cacheDescription: string
private cacheKeyStateKey: string
private cacheResultStateKey: string
protected readonly cacheDebuggingEnabled: boolean
constructor(cacheName: string, cacheDescription: string) {
this.cacheName = cacheName
this.cacheDescription = cacheDescription
this.cacheKeyStateKey = `CACHE_KEY_${cacheName}`
this.cacheResultStateKey = `CACHE_RESULT_${cacheName}`
this.cacheDebuggingEnabled = isCacheDebuggingEnabled()
}
async restore(listener: CacheListener): Promise<void> {
if (this.cacheOutputExists()) {
core.info(`${this.cacheDescription} already exists. Not restoring from cache.`)
return
}
const cacheKey = this.prepareCacheKey()
const entryReport = listener.entry(this.cacheDescription)
entryReport.markRequested(cacheKey.key, cacheKey.restoreKeys)
this.debug(
`Requesting ${this.cacheDescription} with
key:${cacheKey.key}
restoreKeys:[${cacheKey.restoreKeys}]`
)
const cacheResult = await this.restoreCache(this.getCachePath(), cacheKey.key, cacheKey.restoreKeys)
if (!cacheResult) {
core.info(`${this.cacheDescription} cache not found. Will start with empty.`)
return
}
core.saveState(this.cacheResultStateKey, cacheResult.key)
entryReport.markRestored(cacheResult.key, cacheResult.size)
core.info(`Restored ${this.cacheDescription} from cache key: ${cacheResult.key}`)
try {
await this.afterRestore(listener)
} catch (error) {
core.warning(`Restore ${this.cacheDescription} failed in 'afterRestore': ${error}`)
}
}
prepareCacheKey(): CacheKey {
const cacheKey = generateCacheKey(this.cacheName)
core.saveState(this.cacheKeyStateKey, cacheKey.key)
return cacheKey
}
protected async restoreCache(
cachePath: string[],
cacheKey: string,
cacheRestoreKeys: string[] = []
): Promise<cache.CacheEntry | undefined> {
try {
return await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys)
} catch (error) {
handleCacheFailure(error, `Failed to restore ${cacheKey}`)
return undefined
}
}
protected async afterRestore(_listener: CacheListener): Promise<void> {}
async save(listener: CacheListener): Promise<void> {
if (!this.cacheOutputExists()) {
core.info(`No ${this.cacheDescription} to cache.`)
return
}
const cacheKey = core.getState(this.cacheKeyStateKey)
const cacheResult = core.getState(this.cacheResultStateKey)
if (!cacheKey) {
core.info(`${this.cacheDescription} existed prior to cache restore. Not saving.`)
return
}
if (cacheResult && cacheKey === cacheResult) {
core.info(`Cache hit occurred on the cache key ${cacheKey}, not saving cache.`)
return
}
try {
await this.beforeSave(listener)
} catch (error) {
core.warning(`Save ${this.cacheDescription} failed in 'beforeSave': ${error}`)
return
}
core.info(`Caching ${this.cacheDescription} with cache key: ${cacheKey}`)
const cachePath = this.getCachePath()
const savedEntry = await this.saveCache(cachePath, cacheKey)
if (savedEntry) {
listener.entry(this.cacheDescription).markSaved(savedEntry.key, savedEntry.size)
}
return
}
protected async beforeSave(_listener: CacheListener): Promise<void> {}
protected async saveCache(cachePath: string[], cacheKey: string): Promise<cache.CacheEntry | undefined> {
try {
return await cache.saveCache(cachePath, cacheKey)
} catch (error) {
handleCacheFailure(error, `Failed to save cache entry ${cacheKey}`)
}
return undefined
}
protected debug(message: string): void {
if (this.cacheDebuggingEnabled) {
core.info(message)
} else {
core.debug(message)
}
}
protected abstract cacheOutputExists(): boolean
protected abstract getCachePath(): string[]
}

View File

@@ -5,12 +5,8 @@ import * as core from '@actions/core'
import * as glob from '@actions/glob'
import * as exec from '@actions/exec'
import {
AbstractCache,
getCacheKeyPrefix,
hashFileNames,
tryDelete
} from './cache-utils'
import {AbstractCache, CacheEntryListener, CacheListener} from './cache-base'
import {getCacheKeyPrefix, hashFileNames, tryDelete} from './cache-utils'
const META_FILE_DIR = '.gradle-build-action'
@@ -26,21 +22,37 @@ export class GradleUserHomeCache extends AbstractCache {
this.gradleUserHome = this.determineGradleUserHome(rootDir)
}
async afterRestore(): Promise<void> {
async afterRestore(listener: CacheListener): Promise<void> {
await this.reportGradleUserHomeSize('as restored from cache')
await this.restoreArtifactBundles()
await this.restoreArtifactBundles(listener)
await this.reportGradleUserHomeSize('after restoring common artifacts')
}
private async restoreArtifactBundles(): Promise<void> {
private async restoreArtifactBundles(listener: CacheListener): Promise<void> {
const processes: Promise<void>[] = []
for (const [bundle, pattern] of this.getArtifactBundles()) {
const p = this.restoreArtifactBundle(bundle, pattern)
// Run sequentially when debugging enabled
if (this.cacheDebuggingEnabled) {
await p
const bundleMetaFiles = await this.getBundleMetaFiles()
const bundlePatterns = this.getArtifactBundles()
// Iterate over all bundle meta files and try to restore
for (const bundleMetaFile of bundleMetaFiles) {
const bundle = path.basename(bundleMetaFile, '.cache')
const entryListener = listener.entry(bundle)
const bundlePattern = bundlePatterns.get(bundle)
// Handle case where the 'artifactBundlePatterns' have been changed
if (bundlePattern === undefined) {
core.info(`Found bundle metafile for ${bundle} but no such bundle defined`)
entryListener.markRequested('BUNDLE_NOT_CONFIGURED')
tryDelete(bundleMetaFile)
} else {
const p = this.restoreArtifactBundle(bundle, bundlePattern, bundleMetaFile, entryListener)
// Run sequentially when debugging enabled
if (this.cacheDebuggingEnabled) {
await p
}
processes.push(p)
}
processes.push(p)
}
await Promise.all(processes)
@@ -48,25 +60,20 @@ export class GradleUserHomeCache extends AbstractCache {
private async restoreArtifactBundle(
bundle: string,
artifactPath: string
bundlePattern: string,
bundleMetaFile: string,
listener: CacheEntryListener
): Promise<void> {
const bundleMetaFile = this.getBundleMetaFile(bundle)
if (fs.existsSync(bundleMetaFile)) {
const cacheKey = fs.readFileSync(bundleMetaFile, 'utf-8').trim()
const restoreKey = await this.restoreCache([artifactPath], cacheKey)
if (restoreKey) {
core.info(
`Restored ${bundle} with key ${cacheKey} to ${artifactPath}`
)
} else {
this.debug(
`Did not restore ${bundle} with key ${cacheKey} to ${artifactPath}`
)
}
const cacheKey = fs.readFileSync(bundleMetaFile, 'utf-8').trim()
listener.markRequested(cacheKey)
const restoredEntry = await this.restoreCache([bundlePattern], cacheKey)
if (restoredEntry) {
core.info(`Restored ${bundle} with key ${cacheKey} to ${bundlePattern}`)
listener.markRestored(restoredEntry.key, restoredEntry.size)
} else {
this.debug(
`No metafile found to restore ${bundle}: ${bundleMetaFile}`
)
core.info(`Did not restore ${bundle} with key ${cacheKey} to ${bundlePattern}`)
tryDelete(bundleMetaFile)
}
}
@@ -74,22 +81,25 @@ export class GradleUserHomeCache extends AbstractCache {
return path.resolve(this.gradleUserHome, META_FILE_DIR, `${name}.cache`)
}
async beforeSave(): Promise<void> {
private async getBundleMetaFiles(): Promise<string[]> {
const metaFiles = path.resolve(this.gradleUserHome, META_FILE_DIR, '*.cache')
const globber = await glob.create(metaFiles)
const bundleFiles = await globber.glob()
return bundleFiles
}
async beforeSave(listener: CacheListener): Promise<void> {
await this.reportGradleUserHomeSize('before saving common artifacts')
this.removeExcludedPaths()
await this.saveArtifactBundles()
await this.saveArtifactBundles(listener)
await this.reportGradleUserHomeSize(
"after saving common artifacts (only 'caches' and 'notifications' will be stored)"
)
}
private removeExcludedPaths(): void {
const rawPaths: string[] = JSON.parse(
core.getInput(EXCLUDE_PATHS_PARAMETER)
)
const resolvedPaths = rawPaths.map(x =>
path.resolve(this.gradleUserHome, x)
)
const rawPaths: string[] = core.getMultilineInput(EXCLUDE_PATHS_PARAMETER)
const resolvedPaths = rawPaths.map(x => path.resolve(this.gradleUserHome, x))
for (const p of resolvedPaths) {
this.debug(`Deleting excluded path: ${p}`)
@@ -97,10 +107,12 @@ export class GradleUserHomeCache extends AbstractCache {
}
}
private async saveArtifactBundles(): Promise<void> {
private async saveArtifactBundles(listener: CacheListener): Promise<void> {
const processes: Promise<void>[] = []
for (const [bundle, pattern] of this.getArtifactBundles()) {
const p = this.saveArtifactBundle(bundle, pattern)
const entryListener = listener.entry(bundle)
const p = this.saveArtifactBundle(bundle, pattern, entryListener)
// Run sequentially when debugging enabled
if (this.cacheDebuggingEnabled) {
await p
@@ -113,7 +125,8 @@ export class GradleUserHomeCache extends AbstractCache {
private async saveArtifactBundle(
bundle: string,
artifactPath: string
artifactPath: string,
listener: CacheEntryListener
): Promise<void> {
const bundleMetaFile = this.getBundleMetaFile(bundle)
@@ -138,13 +151,14 @@ export class GradleUserHomeCache extends AbstractCache {
const cacheKey = this.createCacheKey(bundle, bundleFiles)
if (previouslyRestoredKey === cacheKey) {
this.debug(
`No change to previously restored ${bundle}. Not caching.`
)
this.debug(`No change to previously restored ${bundle}. Not caching.`)
} else {
core.info(`Caching ${bundle} with cache key: ${cacheKey}`)
await this.saveCache([artifactPath], cacheKey)
this.writeBundleMetaFile(bundleMetaFile, cacheKey)
const savedEntry = await this.saveCache([artifactPath], cacheKey)
if (savedEntry !== undefined) {
this.writeBundleMetaFile(bundleMetaFile, cacheKey)
listener.markSaved(savedEntry.key, savedEntry.size)
}
}
for (const file of bundleFiles) {
@@ -154,14 +168,10 @@ export class GradleUserHomeCache extends AbstractCache {
protected createCacheKey(bundle: string, files: string[]): string {
const cacheKeyPrefix = getCacheKeyPrefix()
const relativeFiles = files.map(x =>
path.relative(this.gradleUserHome, x)
)
const relativeFiles = files.map(x => path.relative(this.gradleUserHome, x))
const key = hashFileNames(relativeFiles)
this.debug(
`Generating cache key for ${bundle} from files: ${relativeFiles}`
)
this.debug(`Generating cache key for ${bundle} from files: ${relativeFiles}`)
return `${cacheKeyPrefix}${bundle}-${key}`
}
@@ -193,9 +203,7 @@ export class GradleUserHomeCache extends AbstractCache {
}
protected getCachePath(): string[] {
const rawPaths: string[] = JSON.parse(
core.getInput(INCLUDE_PATHS_PARAMETER)
)
const rawPaths: string[] = core.getMultilineInput(INCLUDE_PATHS_PARAMETER)
rawPaths.push(META_FILE_DIR)
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
this.debug(`Using cache paths: ${resolvedPaths}`)
@@ -211,19 +219,10 @@ export class GradleUserHomeCache extends AbstractCache {
}
private getArtifactBundles(): Map<string, string> {
const artifactBundleDefinition = core.getInput(
ARTIFACT_BUNDLES_PARAMETER
)
this.debug(
`Using artifact bundle definition: ${artifactBundleDefinition}`
)
const artifactBundleDefinition = core.getInput(ARTIFACT_BUNDLES_PARAMETER)
this.debug(`Using artifact bundle definition: ${artifactBundleDefinition}`)
const artifactBundles = JSON.parse(artifactBundleDefinition)
return new Map(
Array.from(artifactBundles, ([key, value]) => [
key,
path.resolve(this.gradleUserHome, value)
])
)
return new Map(Array.from(artifactBundles, ([key, value]) => [key, path.resolve(this.gradleUserHome, value)]))
}
private async reportGradleUserHomeSize(label: string): Promise<void> {
@@ -233,15 +232,11 @@ export class GradleUserHomeCache extends AbstractCache {
if (!fs.existsSync(this.gradleUserHome)) {
return
}
const result = await exec.getExecOutput(
'du',
['-h', '-c', '-t', '5M'],
{
cwd: this.gradleUserHome,
silent: true,
ignoreReturnCode: true
}
)
const result = await exec.getExecOutput('du', ['-h', '-c', '-t', '5M'], {
cwd: this.gradleUserHome,
silent: true,
ignoreReturnCode: true
})
core.info(`Gradle User Home (directories >5M): ${label}`)

View File

@@ -1,6 +1,6 @@
import path from 'path'
import fs from 'fs'
import {AbstractCache} from './cache-utils'
import {AbstractCache} from './cache-base'
// TODO: Maybe allow the user to override / tweak this set
const PATHS_TO_CACHE = [
@@ -10,7 +10,7 @@ const PATHS_TO_CACHE = [
export class ProjectDotGradleCache extends AbstractCache {
private rootDir: string
constructor(rootDir: string) {
super('project', 'Project .gradle directory')
super('project', 'Project configuration cache')
this.rootDir = rootDir
}

View File

@@ -1,13 +1,13 @@
import * as core from '@actions/core'
import * as cache from '@actions/cache'
import * as github from '@actions/github'
import * as crypto from 'crypto'
import * as path from 'path'
import * as fs from 'fs'
const CACHE_PROTOCOL_VERSION = 'v4-'
const CACHE_DISABLED_PARAMETER = 'cache-disabled'
const CACHE_READONLY_PARAMETER = 'cache-read-only'
const JOB_CONTEXT_PARAMETER = 'workflow-job-context'
const CACHE_DEBUG_VAR = 'GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED'
const CACHE_PREFIX_VAR = 'GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX'
@@ -25,36 +25,7 @@ export function isCacheDebuggingEnabled(): boolean {
export function getCacheKeyPrefix(): string {
// Prefix can be used to force change all cache keys (defaults to cache protocol version)
return process.env[CACHE_PREFIX_VAR] || 'v3-'
}
function generateCacheKey(cacheName: string): CacheKey {
const cacheKeyPrefix = getCacheKeyPrefix()
// At the most general level, share caches for all executions on the same OS
const runnerOs = process.env['RUNNER_OS'] || ''
const cacheKeyForOs = `${cacheKeyPrefix}${cacheName}|${runnerOs}`
// Prefer caches that run this job
const cacheKeyForJob = `${cacheKeyForOs}|${github.context.job}`
// Prefer (even more) jobs that run this job with the same context (matrix)
const cacheKeyForJobContext = `${cacheKeyForJob}[${determineJobContext()}]`
// Exact match on Git SHA
const cacheKey = `${cacheKeyForJobContext}-${github.context.sha}`
return new CacheKey(cacheKey, [
cacheKeyForJobContext,
cacheKeyForJob,
cacheKeyForOs
])
}
function determineJobContext(): string {
// By default, we hash the full `matrix` data for the run, to uniquely identify this job invocation
const workflowJobContext = core.getInput(JOB_CONTEXT_PARAMETER)
return hashStrings([workflowJobContext])
return process.env[CACHE_PREFIX_VAR] || CACHE_PROTOCOL_VERSION
}
export function hashStrings(values: string[]): string {
@@ -66,9 +37,25 @@ export function hashStrings(values: string[]): string {
}
export function hashFileNames(fileNames: string[]): string {
return hashStrings(
fileNames.map(x => x.replace(new RegExp(`\\${path.sep}`, 'g'), '/'))
)
return hashStrings(fileNames.map(x => x.replace(new RegExp(`\\${path.sep}`, 'g'), '/')))
}
export function handleCacheFailure(error: unknown, message: string): void {
if (error instanceof cache.ValidationError) {
// Fail on cache validation errors
throw error
}
if (error instanceof cache.ReserveCacheError) {
// Reserve cache errors are expected if the artifact has been previously cached
if (isCacheDebuggingEnabled()) {
core.info(message)
} else {
core.debug(message)
}
} else {
// Warn on all other errors
core.warning(`${message}: ${error}`)
}
}
/**
@@ -98,176 +85,3 @@ export async function tryDelete(file: string): Promise<void> {
async function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
class CacheKey {
key: string
restoreKeys: string[]
constructor(key: string, restoreKeys: string[]) {
this.key = key
this.restoreKeys = restoreKeys
}
}
export abstract class AbstractCache {
private cacheName: string
private cacheDescription: string
private cacheKeyStateKey: string
private cacheResultStateKey: string
protected readonly cacheDebuggingEnabled: boolean
constructor(cacheName: string, cacheDescription: string) {
this.cacheName = cacheName
this.cacheDescription = cacheDescription
this.cacheKeyStateKey = `CACHE_KEY_${cacheName}`
this.cacheResultStateKey = `CACHE_RESULT_${cacheName}`
this.cacheDebuggingEnabled = isCacheDebuggingEnabled()
}
async restore(): Promise<void> {
if (this.cacheOutputExists()) {
core.info(
`${this.cacheDescription} already exists. Not restoring from cache.`
)
return
}
const cacheKey = generateCacheKey(this.cacheName)
core.saveState(this.cacheKeyStateKey, cacheKey.key)
this.debug(
`Requesting ${this.cacheDescription} with
key:${cacheKey.key}
restoreKeys:[${cacheKey.restoreKeys}]`
)
const cacheResult = await this.restoreCache(
this.getCachePath(),
cacheKey.key,
cacheKey.restoreKeys
)
if (!cacheResult) {
core.info(
`${this.cacheDescription} cache not found. Will start with empty.`
)
return
}
core.saveState(this.cacheResultStateKey, cacheResult)
core.info(
`Restored ${this.cacheDescription} from cache key: ${cacheResult}`
)
try {
await this.afterRestore()
} catch (error) {
core.warning(
`Restore ${this.cacheDescription} failed in 'afterRestore': ${error}`
)
}
return
}
protected async restoreCache(
cachePath: string[],
cacheKey: string,
cacheRestoreKeys: string[] = []
): Promise<string | undefined> {
try {
return await cache.restoreCache(
cachePath,
cacheKey,
cacheRestoreKeys
)
} catch (error) {
if (error instanceof cache.ValidationError) {
// Validation errors should fail the build action
throw error
}
// Warn about any other error and continue
core.warning(`Failed to restore ${cacheKey}: ${error}`)
return undefined
}
}
protected async afterRestore(): Promise<void> {}
async save(): Promise<void> {
if (!this.cacheOutputExists()) {
this.debug(`No ${this.cacheDescription} to cache.`)
return
}
const cacheKey = core.getState(this.cacheKeyStateKey)
const cacheResult = core.getState(this.cacheResultStateKey)
if (!cacheKey) {
this.debug(
`${this.cacheDescription} existed prior to cache restore. Not saving.`
)
return
}
if (cacheResult && cacheKey === cacheResult) {
core.info(
`Cache hit occurred on the cache key ${cacheKey}, not saving cache.`
)
return
}
try {
await this.beforeSave()
} catch (error) {
core.warning(
`Save ${this.cacheDescription} failed in 'beforeSave': ${error}`
)
return
}
core.info(
`Caching ${this.cacheDescription} with cache key: ${cacheKey}`
)
const cachePath = this.getCachePath()
await this.saveCache(cachePath, cacheKey)
return
}
protected async beforeSave(): Promise<void> {}
protected async saveCache(
cachePath: string[],
cacheKey: string
): Promise<void> {
try {
await cache.saveCache(cachePath, cacheKey)
} catch (error) {
if (error instanceof cache.ValidationError) {
// Validation errors should fail the build action
throw error
} else if (error instanceof cache.ReserveCacheError) {
// Reserve cache errors are expected if the artifact has been previously cached
this.debug(error.message)
} else {
// Warn about any other error and continue
core.warning(String(error))
}
}
}
protected debug(message: string): void {
if (this.cacheDebuggingEnabled) {
core.info(message)
} else {
core.debug(message)
}
}
protected abstract cacheOutputExists(): boolean
protected abstract getCachePath(): string[]
}

View File

@@ -2,39 +2,97 @@ import {GradleUserHomeCache} from './cache-gradle-user-home'
import {ProjectDotGradleCache} from './cache-project-dot-gradle'
import * as core from '@actions/core'
import {isCacheDisabled, isCacheReadOnly} from './cache-utils'
import {CacheEntryListener, CacheListener} from './cache-base'
const BUILD_ROOT_DIR = 'BUILD_ROOT_DIR'
const CACHE_LISTENER = 'CACHE_LISTENER'
export async function restore(buildRootDirectory: string): Promise<void> {
if (isCacheDisabled()) {
core.info(
'Cache is disabled: will not restore state from previous builds.'
)
core.info('Cache is disabled: will not restore state from previous builds.')
return
}
await core.group('Restore Gradle state from cache', async () => {
core.saveState(BUILD_ROOT_DIR, buildRootDirectory)
return Promise.all([
new GradleUserHomeCache(buildRootDirectory).restore(),
new ProjectDotGradleCache(buildRootDirectory).restore()
])
const cacheListener = new CacheListener()
await new GradleUserHomeCache(buildRootDirectory).restore(cacheListener)
const projectDotGradleCache = new ProjectDotGradleCache(buildRootDirectory)
if (cacheListener.fullyRestored) {
// Only restore the configuration-cache if the Gradle Home is fully restored
await projectDotGradleCache.restore(cacheListener)
} else {
// Otherwise, prepare the cache key for later save()
core.info('Gradle Home cache not fully restored: not restoring configuration-cache state')
projectDotGradleCache.prepareCacheKey()
}
core.saveState(CACHE_LISTENER, cacheListener.stringify())
})
}
export async function save(): Promise<void> {
const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER))
if (isCacheReadOnly()) {
core.info(
'Cache is read-only: will not save state for use in subsequent builds.'
)
core.info('Cache is read-only: will not save state for use in subsequent builds.')
logCachingReport(cacheListener)
return
}
await core.group('Caching Gradle state', async () => {
const buildRootDirectory = core.getState(BUILD_ROOT_DIR)
return Promise.all([
new GradleUserHomeCache(buildRootDirectory).save(),
new ProjectDotGradleCache(buildRootDirectory).save()
new GradleUserHomeCache(buildRootDirectory).save(cacheListener),
new ProjectDotGradleCache(buildRootDirectory).save(cacheListener)
])
})
logCachingReport(cacheListener)
}
function logCachingReport(listener: CacheListener): void {
if (listener.cacheEntries.length === 0) {
return
}
core.info(`---------- Caching Summary -------------
Restored Entries Count: ${getCount(listener.cacheEntries, e => e.restoredSize)}
Size: ${getSum(listener.cacheEntries, e => e.restoredSize)}
Saved Entries Count: ${getCount(listener.cacheEntries, e => e.savedSize)}
Size: ${getSum(listener.cacheEntries, e => e.savedSize)}`)
core.startGroup('Cache Entry details')
for (const entry of listener.cacheEntries) {
core.info(`Entry: ${entry.entryName}
Requested Key : ${entry.requestedKey ?? ''}
Restored Key : ${entry.restoredKey ?? ''}
Size: ${formatSize(entry.restoredSize)}
Saved Key : ${entry.savedKey ?? ''}
Size: ${formatSize(entry.savedSize)}`)
}
core.endGroup()
}
function getCount(
cacheEntries: CacheEntryListener[],
predicate: (value: CacheEntryListener) => number | undefined
): number {
return cacheEntries.filter(e => predicate(e) !== undefined).length
}
function getSum(
cacheEntries: CacheEntryListener[],
predicate: (value: CacheEntryListener) => number | undefined
): string {
return formatSize(cacheEntries.map(e => predicate(e) ?? 0).reduce((p, v) => p + v, 0))
}
function formatSize(bytes: number | undefined): string {
if (bytes === undefined || bytes === 0) {
return ''
}
return `${Math.round(bytes / (1024 * 1024))} MB (${bytes} B)`
}

View File

@@ -3,11 +3,7 @@ import fs from 'fs'
import path from 'path'
import {writeInitScript} from './build-scan-capture'
export async function execute(
executable: string,
root: string,
args: string[]
): Promise<BuildResult> {
export async function execute(executable: string, root: string, args: string[]): Promise<BuildResult> {
let buildScanUrl: string | undefined
// TODO: instead of running with no-daemon, run `--stop` in post action.

View File

@@ -17,10 +17,7 @@ export function locateGradleWrapperScript(buildRootDirectory: string): string {
}
function validateGradleWrapper(buildRootDirectory: string): void {
const wrapperProperties = path.resolve(
buildRootDirectory,
'gradle/wrapper/gradle-wrapper.properties'
)
const wrapperProperties = path.resolve(buildRootDirectory, 'gradle/wrapper/gradle-wrapper.properties')
if (!fs.existsSync(wrapperProperties)) {
throw new Error(
`Cannot locate a Gradle wrapper properties file at '${wrapperProperties}'. Specify 'gradle-version' or 'gradle-executable' for projects without Gradle wrapper configured.`

View File

@@ -18,10 +18,7 @@ export async function run(): Promise<void> {
const args: string[] = parseCommandLineArguments()
const result = await execution.execute(
await resolveGradleExecutable(
workspaceDirectory,
buildRootDirectory
),
await resolveGradleExecutable(workspaceDirectory, buildRootDirectory),
buildRootDirectory,
args
)
@@ -34,9 +31,7 @@ export async function run(): Promise<void> {
if (result.buildScanUrl) {
core.setFailed(`Gradle build failed: ${result.buildScanUrl}`)
} else {
core.setFailed(
`Gradle build failed: process exited with status ${result.status}`
)
core.setFailed(`Gradle build failed: process exited with status ${result.status}`)
}
} else {
if (result.buildScanUrl) {
@@ -53,10 +48,7 @@ export async function run(): Promise<void> {
run()
async function resolveGradleExecutable(
workspaceDirectory: string,
buildRootDirectory: string
): Promise<string> {
async function resolveGradleExecutable(workspaceDirectory: string, buildRootDirectory: string): Promise<string> {
const gradleVersion = core.getInput('gradle-version')
if (gradleVersion !== '' && gradleVersion !== 'wrapper') {
return path.resolve(await provision.gradleVersion(gradleVersion))
@@ -73,9 +65,7 @@ async function resolveGradleExecutable(
function resolveBuildRootDirectory(baseDirectory: string): string {
const buildRootDirectory = core.getInput('build-root-directory')
const resolvedBuildRootDirectory =
buildRootDirectory === ''
? path.resolve(baseDirectory)
: path.resolve(baseDirectory, buildRootDirectory)
buildRootDirectory === '' ? path.resolve(baseDirectory) : path.resolve(baseDirectory, buildRootDirectory)
return resolvedBuildRootDirectory
}

View File

@@ -7,7 +7,7 @@ import * as cache from '@actions/cache'
import * as toolCache from '@actions/tool-cache'
import * as gradlew from './gradlew'
import {isCacheDisabled, isCacheReadOnly} from './cache-utils'
import {handleCacheFailure, isCacheDisabled, isCacheReadOnly} from './cache-utils'
const gradleVersionsBaseUrl = 'https://services.gradle.org/versions'
@@ -19,9 +19,7 @@ export async function gradleVersion(version: string): Promise<string> {
case 'current':
return gradleCurrent()
case 'rc':
core.warning(
`Specifying gradle-version 'rc' has been deprecated. Use 'release-candidate' instead.`
)
core.warning(`Specifying gradle-version 'rc' has been deprecated. Use 'release-candidate' instead.`)
return gradleReleaseCandidate()
case 'release-candidate':
return gradleReleaseCandidate()
@@ -35,16 +33,12 @@ export async function gradleVersion(version: string): Promise<string> {
}
async function gradleCurrent(): Promise<string> {
const versionInfo = await gradleVersionDeclaration(
`${gradleVersionsBaseUrl}/current`
)
const versionInfo = await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/current`)
return provisionGradle(versionInfo)
}
async function gradleReleaseCandidate(): Promise<string> {
const versionInfo = await gradleVersionDeclaration(
`${gradleVersionsBaseUrl}/release-candidate`
)
const versionInfo = await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/release-candidate`)
if (versionInfo && versionInfo.version && versionInfo.downloadUrl) {
return provisionGradle(versionInfo)
}
@@ -53,16 +47,12 @@ async function gradleReleaseCandidate(): Promise<string> {
}
async function gradleNightly(): Promise<string> {
const versionInfo = await gradleVersionDeclaration(
`${gradleVersionsBaseUrl}/nightly`
)
const versionInfo = await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/nightly`)
return provisionGradle(versionInfo)
}
async function gradleReleaseNightly(): Promise<string> {
const versionInfo = await gradleVersionDeclaration(
`${gradleVersionsBaseUrl}/release-nightly`
)
const versionInfo = await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/release-nightly`)
return provisionGradle(versionInfo)
}
@@ -74,34 +64,24 @@ async function gradle(version: string): Promise<string> {
return provisionGradle(versionInfo)
}
async function gradleVersionDeclaration(
url: string
): Promise<GradleVersionInfo> {
async function gradleVersionDeclaration(url: string): Promise<GradleVersionInfo> {
return await httpGetGradleVersion(url)
}
async function findGradleVersionDeclaration(
version: string
): Promise<GradleVersionInfo | undefined> {
const gradleVersions = await httpGetGradleVersions(
`${gradleVersionsBaseUrl}/all`
)
async function findGradleVersionDeclaration(version: string): Promise<GradleVersionInfo | undefined> {
const gradleVersions = await httpGetGradleVersions(`${gradleVersionsBaseUrl}/all`)
return gradleVersions.find((entry: GradleVersionInfo) => {
return entry.version === version
})
}
async function provisionGradle(
versionInfo: GradleVersionInfo
): Promise<string> {
async function provisionGradle(versionInfo: GradleVersionInfo): Promise<string> {
return core.group(`Provision Gradle ${versionInfo.version}`, async () => {
return locateGradleAndDownloadIfRequired(versionInfo)
})
}
async function locateGradleAndDownloadIfRequired(
versionInfo: GradleVersionInfo
): Promise<string> {
async function locateGradleAndDownloadIfRequired(versionInfo: GradleVersionInfo): Promise<string> {
const installsDir = path.join(os.homedir(), 'gradle-installations/installs')
const installDir = path.join(installsDir, `gradle-${versionInfo.version}`)
if (fs.existsSync(installDir)) {
@@ -120,13 +100,8 @@ async function locateGradleAndDownloadIfRequired(
return executable
}
async function downloadAndCacheGradleDistribution(
versionInfo: GradleVersionInfo
): Promise<string> {
const downloadPath = path.join(
os.homedir(),
`gradle-installations/downloads/gradle-${versionInfo.version}-bin.zip`
)
async function downloadAndCacheGradleDistribution(versionInfo: GradleVersionInfo): Promise<string> {
const downloadPath = path.join(os.homedir(), `gradle-installations/downloads/gradle-${versionInfo.version}-bin.zip`)
if (isCacheDisabled()) {
await downloadGradleDistribution(versionInfo, downloadPath)
@@ -134,45 +109,32 @@ async function downloadAndCacheGradleDistribution(
}
const cacheKey = `gradle-${versionInfo.version}`
const restoreKey = await cache.restoreCache([downloadPath], cacheKey)
if (restoreKey) {
core.info(
`Restored Gradle distribution ${cacheKey} from cache to ${downloadPath}`
)
return downloadPath
try {
const restoreKey = await cache.restoreCache([downloadPath], cacheKey)
if (restoreKey) {
core.info(`Restored Gradle distribution ${cacheKey} from cache to ${downloadPath}`)
return downloadPath
}
} catch (error) {
handleCacheFailure(error, `Restore Gradle distribution ${versionInfo.version} failed`)
}
core.info(
`Gradle distribution ${versionInfo.version} not found in cache. Will download.`
)
core.info(`Gradle distribution ${versionInfo.version} not found in cache. Will download.`)
await downloadGradleDistribution(versionInfo, downloadPath)
if (!isCacheReadOnly()) {
try {
await cache.saveCache([downloadPath], cacheKey)
} catch (error) {
// Fail on validation errors or non-errors (the latter to keep Typescript happy)
if (
error instanceof cache.ValidationError ||
!(error instanceof Error)
) {
throw error
}
core.warning(error.message)
handleCacheFailure(error, `Save Gradle distribution ${versionInfo.version} failed`)
}
}
return downloadPath
}
async function downloadGradleDistribution(
versionInfo: GradleVersionInfo,
downloadPath: string
): Promise<void> {
async function downloadGradleDistribution(versionInfo: GradleVersionInfo, downloadPath: string): Promise<void> {
await toolCache.downloadTool(versionInfo.downloadUrl, downloadPath)
core.info(
`Downloaded ${versionInfo.downloadUrl} to ${downloadPath} (size ${
fs.statSync(downloadPath).size
})`
)
core.info(`Downloaded ${versionInfo.downloadUrl} to ${downloadPath} (size ${fs.statSync(downloadPath).size})`)
}
function executableFrom(installDir: string): string {
@@ -183,9 +145,7 @@ async function httpGetGradleVersion(url: string): Promise<GradleVersionInfo> {
return JSON.parse(await httpGetString(url))
}
async function httpGetGradleVersions(
url: string
): Promise<GradleVersionInfo[]> {
async function httpGetGradleVersions(url: string): Promise<GradleVersionInfo[]> {
return JSON.parse(await httpGetString(url))
}