Compare commits

...

73 Commits

Author SHA1 Message Date
bigdaz
833b05f3e6 [bot] Update dist directory 2024-08-01 17:05:22 +00:00
Daz DeBoer
06905c7a0f Enable wrapper-validation by default in setup-gradle (#318) 2024-08-01 11:04:20 -06:00
daz
73f1290de7 Improve docs linked for wrapper-validation failure 2024-08-01 10:59:51 -06:00
daz
b6395da67c Cache validated checksums for later executions
The most common case for validation will be that the wrapper jars are unchanged
from a previous workflow run. In this case, we cache the validated wrapper
checksums to minimise the work required on a subsequent run.

Fixes #172
2024-08-01 10:52:36 -06:00
daz
ce4c3a6c5e Move wrapper-validation into common setup code 2024-08-01 10:52:36 -06:00
daz
b644be617f Enable wrapper validation by default
- Add 'allow-snapshot-wrappers' input parameter
- Default 'validate-wrappers' to 'true'

Fixes #12
2024-08-01 10:51:02 -06:00
daz
7179909719 Verify wrappers for distribution-snapshots
By slurping the checksum URLs from https://services.gradle.org/distributions-snapshots/
we can include these unpublished wrapper checksums in validation.

Fixes #281
2024-07-31 21:26:55 -06:00
daz
c01aea0cb4 Introduce cheerio for fast HTML querying 2024-07-31 21:26:55 -06:00
bigdaz
479297d73e [bot] Update dist directory 2024-08-01 03:26:19 +00:00
daz
fe594a580d Group cache-cleanup log messages for clarity 2024-07-31 21:25:22 -06:00
daz
53f2a5657b Add newline to args message 2024-07-31 13:22:03 -06:00
bigdaz
fd87365911 [bot] Update dist directory 2024-07-23 02:56:59 +00:00
bot-githubaction
c3f989640d Bump references to Develocity Gradle plugin from 3.17.5 to 3.17.6 2024-07-22 20:56:04 -06:00
bigdaz
6c9e547314 [bot] Update dist directory 2024-07-22 09:30:39 -06:00
daz
1a11891cfe Add error to job summary 2024-07-22 09:30:39 -06:00
daz
fae6382622 Fail the build when arguments parameter is used 2024-07-22 09:30:39 -06:00
Daz DeBoer
81b4ac7741 Fix typo 2024-07-22 07:21:54 -06:00
daz
c9872874b0 Segregate cache key for quick and full integtest 2024-07-21 14:38:05 -06:00
bigdaz
561dcd8516 [bot] Update dist directory 2024-07-21 20:20:49 +00:00
daz
7bb45b1dbd Finish enabling cache-cleanup by default
- Add deprecation warning for `gradle-home-cache-cleanup`
- Change default for `dependency-submission` to `cache-cleanup: on-success`
- Update documentation for changed default
2024-07-21 14:19:54 -06:00
bigdaz
dc97151e55 [bot] Update dist directory 2024-07-20 01:41:36 +00:00
Daz DeBoer
2289da045c Introduce dependency graph params (#304)
Fixes #193
2024-07-19 19:40:42 -06:00
bigdaz
40a3605597 [bot] Update dist directory 2024-07-20 01:25:51 +00:00
daz
e6688f31b8 Include RUNNER_ARCH in cache key
Previously, including RUNNER_OS was enough to prevent leaking incompatible
content between Gradle User Homes. With the introduction of macos-14,
we now need to differentiate between different runner architectures as well.

Fixes #138
2024-07-19 19:24:50 -06:00
Daz DeBoer
46308b920a Document dependency verification requirements
Fixes #256
2024-07-19 19:24:12 -06:00
daz
7387edbbb3 Add more input parameters for dependency-graph generation
- dependency-graph-include-projects
- dependency-graph-exclude-projects
- dependency-graph include-configurations
- dependency-graph-exclude-configurations
2024-07-19 17:22:07 -06:00
daz
9e459adb11 Add 'dependency-graph-report-dir' input parameter 2024-07-19 17:21:10 -06:00
bigdaz
1371d49f1d [bot] Update dist directory 2024-07-19 23:14:37 +00:00
Daz DeBoer
8e4868ab4a Prepare for v4 release (#303)
Resolves #297
Resolves #26
2024-07-19 17:13:42 -06:00
daz
5acdee61f0 Make cache-cleanup compatible with cache-write-only 2024-07-19 16:16:59 -06:00
daz
32f9239e2c Remove the undocumented 'dependency-graph: clear' option 2024-07-19 16:16:13 -06:00
daz
12cb23c359 Enable cache-cleanup 'on-success' by default
Fixes #26
2024-07-19 15:28:39 -06:00
daz
238a3da6f4 Remove tests for deprecated features 2024-07-19 15:15:03 -06:00
daz
2041ce6ab1 Fail on delegation from legacy actions 2024-07-19 15:12:40 -06:00
daz
ded8009fcf Remove deprecated input parameters 2024-07-19 15:12:40 -06:00
bigdaz
db8e69bc03 [bot] Update dist directory 2024-07-19 20:14:55 +00:00
Daz DeBoer
4b56f19bda Use pre-installed Gradle when available (#301)
Fixes #270
2024-07-19 14:13:59 -06:00
daz
4576973cce Test provision with more Gradle versions
Include Gradle 8.8 since it is the pre-installed version on Windows runners.
2024-07-19 14:05:33 -06:00
daz
5d7c18409c Use pre-installed Gradle when version matches
By executing Gradle on the PATH, we can avoid downloading and installing
a Gradle version that is already available on the runner.

Fixes #270
2024-07-19 13:46:56 -06:00
daz
7da993afd5 Add 'which' NPM library 2024-07-19 13:46:55 -06:00
daz
de6862d826 Use bash to ensure env vars are expanded 2024-07-19 12:59:38 -06:00
bigdaz
4c453aec59 [bot] Update dist directory 2024-07-19 17:56:33 +00:00
daz
917439d87d Always set the GRADLE_USER_HOME env var 2024-07-19 11:55:34 -06:00
bigdaz
77266ec345 [bot] Update dist directory 2024-07-19 17:08:28 +00:00
Daz DeBoer
514ac344fc Use faster D: drive on windows for Gradle User Home and Gradle downloads (#299)
Fixes #290
2024-07-19 11:07:34 -06:00
daz
f8aa3ed8b4 Use faster D: drive for Gradle User Home on Windows 2024-07-19 10:04:41 -06:00
daz
b175189376 Download and install Gradle distributions into tmpdir 2024-07-19 09:14:37 -06:00
bigdaz
20e923b8cb [bot] Update dist directory 2024-07-19 15:06:31 +00:00
daz
e7f9f25d86 Delete excluded paths on restore Gradle Home
Fixes #296
2024-07-19 09:05:31 -06:00
bigdaz
f8f7d3e704 [bot] Update dist directory 2024-07-19 04:33:23 +00:00
Daz DeBoer
7a630bc41b Disable cache-cleanup on config-cache hit (#295)
Fixes #19
2024-07-18 22:32:30 -06:00
daz
a77cb2b0f8 Add test for no cache-cleanup with config-cache hit 2024-07-18 22:25:59 -06:00
daz
54f7dc55a5 Adapt for new structure of build results 2024-07-18 22:25:58 -06:00
daz
579a013225 Capture config-cache hit in build results 2024-07-18 22:25:58 -06:00
daz
94355bbb2f Split build-results and build-scan capture into separate files 2024-07-18 22:25:58 -06:00
daz
6d20c16462 Use settingsEvaluated in preference to projectsEvaluated in init-script 2024-07-18 22:25:53 -06:00
bigdaz
b7e399239c [bot] Update dist directory 2024-07-18 17:34:32 +00:00
Daz DeBoer
723ca4de01 Caching improvements (#294)
- Better reporting of cache status in Job Summary (including cache
cleanup)
- Allow cache cleanup to be skipped when Gradle builds fail
- Refactoring
2024-07-18 11:33:36 -06:00
daz
72dde7ef1e Move action entry-point sources 2024-07-18 11:09:54 -06:00
daz
3083f01451 Use new cache-cleanup param in integtest 2024-07-17 22:47:46 -06:00
daz
27dea2df09 Allow better control over cache-cleanup
Adds new 'cache-cleanup' parameter with 3 settings: 'never', 'on-success' and 'always'.
This gives users more control over whether cache cleanup should occur.

Fixes #71
2024-07-17 20:36:33 -06:00
daz
91a526b647 Refactor BuildResults 2024-07-17 20:35:14 -06:00
daz
d92de28b80 Improve cache reporting
- More succinct messages for cache-read-only and cache-disabled
- Report on cache-cleanup enabled/disabled status
2024-07-17 20:29:57 -06:00
daz
8d318190ad Add better explanation for cache status
- Clarify default settings for cache-read-only
- Explain why cache was disabled or read-only
- Provide links to documentation in Job Summary

Fixes #255
2024-07-17 15:17:09 -06:00
daz
a025cbe7ec Don't run test on windows 2024-07-16 22:43:22 -06:00
bigdaz
23dad2b1c7 [bot] Update dist directory 2024-07-17 04:13:39 +00:00
Daz DeBoer
4a315dceb2 Isolate 'dependency-submission' action from 'setup-gradle' (#293) 2024-07-16 22:12:40 -06:00
daz
bb7a843511 Reset dependency-graph after submission
Fixes #204
2024-07-16 21:15:51 -06:00
daz
9a57bcca96 Add test for combining setup-gradle and dependency-submission 2024-07-16 21:15:30 -06:00
daz
22818445b3 Restrict download-and-submit to selected file 2024-07-16 21:15:29 -06:00
bigdaz
36c24e793d [bot] Update dist directory 2024-07-16 19:05:54 +00:00
Daz DeBoer
dff3ef9b8d Only fetch checksums for unknown wrapper versions (#292)
The checksum values for most wrapper versions are hard-coded into the
action. These known checksum values are first used for validation: only
if none of the known values work do we download checksums.

Previously, we blindly downloaded all of the checksum values in this
case: we now only download the checksums for versions that are not in
our "known" set.

Fixes #171
2024-07-16 13:04:57 -06:00
Daz DeBoer
01254b3eaa Add additional references to dependency-submission docs
Fixes #276
2024-07-15 16:21:32 -06:00
69 changed files with 104162 additions and 3559 deletions

View File

@@ -9,6 +9,11 @@ runs:
distribution: 'temurin'
java-version: 11
- name: Configure environment
shell: bash
run: |
echo "ALLOWED_GRADLE_WRAPPER_CHECKSUMS=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" >> "$GITHUB_ENV"
# Downloads a 'dist' directory artifact that was uploaded in an earlier 'build-dist' step
- name: Download dist
if: ${{ env.SKIP_DIST != 'true' && !env.ACT }}

View File

@@ -1,5 +1,5 @@
plugins {
id "com.gradle.develocity" version "3.17.5"
id "com.gradle.develocity" version "3.17.6"
id "com.gradle.common-custom-user-data-gradle-plugin" version "2.0.1"
}

View File

@@ -1,5 +1,5 @@
plugins {
id("com.gradle.develocity") version "3.17.5"
id("com.gradle.develocity") version "3.17.6"
id("com.gradle.common-custom-user-data-gradle-plugin") version "2.0.1"
}

View File

@@ -1,5 +1,5 @@
plugins {
id "com.gradle.develocity" version "3.17.5"
id "com.gradle.develocity" version "3.17.6"
}
develocity {

View File

@@ -1,5 +1,5 @@
plugins {
id "com.gradle.develocity" version "3.17.5"
id "com.gradle.develocity" version "3.17.6"
}
develocity {

View File

@@ -1,5 +1,5 @@
plugins {
id "com.gradle.develocity" version "3.17.5"
id "com.gradle.develocity" version "3.17.6"
}
develocity {

View File

@@ -53,14 +53,6 @@ jobs:
if: ${{ needs.determine-suite.outputs.suite != 'full' }}
uses: ./.github/actions/build-dist
action-inputs:
needs: [determine-suite, build-distribution]
uses: ./.github/workflows/integ-test-action-inputs.yml
with:
runner-os: '${{ needs.determine-suite.outputs.runner-os }}'
cache-key-prefix: '${{ needs.determine-suite.outputs.cache-key-prefix }}-'
skip-dist: ${{ needs.determine-suite.outputs.suite == 'full' }}
build-scan-publish:
needs: [determine-suite, build-distribution]
uses: ./.github/workflows/integ-test-build-scan-publish.yml
@@ -74,7 +66,7 @@ jobs:
uses: ./.github/workflows/integ-test-cache-cleanup.yml
with:
runner-os: '${{ needs.determine-suite.outputs.runner-os }}'
cache-key-prefix: '${{github.run_number}}-' # Requires a fresh cache entry each run
cache-key-prefix: '${{ needs.determine-suite.outputs.suite}}-${{github.run_number}}-' # Requires a fresh cache entry each run
skip-dist: ${{ needs.determine-suite.outputs.suite == 'full' }}
caching-config:
@@ -118,22 +110,6 @@ jobs:
cache-key-prefix: '${{ needs.determine-suite.outputs.cache-key-prefix }}-'
skip-dist: ${{ needs.determine-suite.outputs.suite == 'full' }}
execution-with-caching:
needs: [determine-suite, build-distribution]
uses: ./.github/workflows/integ-test-execution-with-caching.yml
with:
runner-os: '${{ needs.determine-suite.outputs.runner-os }}'
cache-key-prefix: '${{ needs.determine-suite.outputs.cache-key-prefix }}-'
skip-dist: ${{ needs.determine-suite.outputs.suite == 'full' }}
execution:
needs: [determine-suite, build-distribution]
uses: ./.github/workflows/integ-test-execution.yml
with:
runner-os: '${{ needs.determine-suite.outputs.runner-os }}'
cache-key-prefix: '${{ needs.determine-suite.outputs.cache-key-prefix }}-'
skip-dist: ${{ needs.determine-suite.outputs.suite == 'full' }}
develocity-injection:
if: ${{ ! github.event.pull_request.head.repo.fork }}
needs: [determine-suite, build-distribution]

View File

@@ -1,62 +0,0 @@
name: demo-failure-cases
on:
workflow_dispatch:
jobs:
build-distribution:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Build and upload distribution
uses: ./.github/actions/build-dist
failing-build:
needs: build-distribution
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Test build failure
uses: ./setup-gradle
continue-on-error: true
with:
build-root-directory: .github/workflow-samples/kotlin-dsl
arguments: not-a-valid-task
wrapper-missing:
needs: build-distribution
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Test wrapper missing
uses: ./setup-gradle
continue-on-error: true
with:
build-root-directory: .github/workflow-samples/no-wrapper
arguments: help
bad-configuration:
needs: build-distribution
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Test bad config value
uses: ./setup-gradle
continue-on-error: true
with:
build-root-directory: .github/workflow-samples/no-wrapper
arguments: help
cache-disabled: yes

View File

@@ -23,6 +23,9 @@ jobs:
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-read-only: false
cache-cleanup: 'on-success'
- name: Build kotlin-dsl project
working-directory: .github/workflow-samples/kotlin-dsl
run: ./gradlew assemble

View File

@@ -1,42 +0,0 @@
name: Test action inputs
on:
workflow_call:
inputs:
cache-key-prefix:
type: string
runner-os:
type: string
default: '["ubuntu-latest", "windows-latest", "macos-latest"]'
skip-dist:
type: boolean
default: false
env:
SKIP_DIST: ${{ inputs.skip-dist }}
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: action-inputs-${{ inputs.cache-key-prefix }}
jobs:
action-inputs:
strategy:
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Invoke with multi-line arguments
uses: ./setup-gradle
with:
build-root-directory: .github/workflow-samples/groovy-dsl
arguments: |
--configuration-cache
--build-cache
-DsystemProperty=FOO
-PgradleProperty=BAR
test
jar

View File

@@ -55,7 +55,7 @@ jobs:
uses: ./setup-gradle
with:
cache-read-only: false
gradle-home-cache-cleanup: true
cache-cleanup: 'on-success'
- name: Build with 3.1.1
working-directory: sources/test/jest/resources/cache-cleanup
run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1.1" build
@@ -78,21 +78,22 @@ jobs:
with:
cache-read-only: true
- name: Report Gradle User Home
shell: bash
run: |
du -hc ~/.gradle/caches/modules-2
du -hc ~/.gradle/wrapper/dists
du -hc $GRADLE_USER_HOME/caches/modules-2
du -hc $GRADLE_USER_HOME/wrapper/dists
- name: Verify cleaned cache
shell: bash
run: |
if [ ! -e ~/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-math3/3.1.1 ]; then
if [ ! -e $GRADLE_USER_HOME/caches/modules-2/files-2.1/org.apache.commons/commons-math3/3.1.1 ]; then
echo "::error ::Should find commons-math3 3.1.1 in cache"
exit 1
fi
if [ -e ~/.gradle/caches/modules-2/files-2.1/org.apache.commons/commons-math3/3.1 ]; then
if [ -e $GRADLE_USER_HOME/caches/modules-2/files-2.1/org.apache.commons/commons-math3/3.1 ]; then
echo "::error ::Should NOT find commons-math3 3.1 in cache"
exit 1
fi
if [ ! -e ~/.gradle/wrapper/dists/gradle-8.0.2-bin ]; then
if [ ! -e $GRADLE_USER_HOME/wrapper/dists/gradle-8.0.2-bin ]; then
echo "::error ::Should find gradle-8.0.2 in wrapper/dists"
exit 1
fi

View File

@@ -21,7 +21,7 @@ env:
GITHUB_DEPENDENCY_GRAPH_REF: 'refs/tags/v0.0.1' # Use a different ref to avoid updating the real dependency graph for the repository
jobs:
groovy-generate:
groovy-upload:
strategy:
fail-fast: false
matrix:
@@ -42,7 +42,7 @@ jobs:
working-directory: .github/workflow-samples/groovy-dsl
groovy-submit:
needs: [groovy-generate]
needs: [groovy-upload]
runs-on: "ubuntu-latest"
steps:
- name: Checkout sources
@@ -54,6 +54,8 @@ jobs:
uses: ./setup-gradle
with:
dependency-graph: download-and-submit
env:
DEPENDENCY_GRAPH_DOWNLOAD_ARTIFACT_NAME: groovy-upload
kotlin-generate-and-submit:
strategy:

View File

@@ -80,6 +80,8 @@ jobs:
uses: ./dependency-submission
with:
dependency-graph: download-and-submit
env:
DEPENDENCY_GRAPH_DOWNLOAD_ARTIFACT_NAME: groovy-generate-and-upload-${{ matrix.os }}
kotlin-generate-and-submit:
strategy:
@@ -225,12 +227,8 @@ jobs:
gradle-version: ${{ matrix.gradle }}
build-root-directory: .github/workflow-samples/no-wrapper${{ matrix.build-root-suffix }}
after-setup-gradle:
strategy:
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
with-setup-gradle:
runs-on: ubuntu-latest # Test is not compatible with Windows
steps:
- name: Checkout sources
uses: actions/checkout@v4
@@ -244,6 +242,59 @@ jobs:
uses: ./dependency-submission
with:
build-root-directory: .github/workflow-samples/groovy-dsl
- name: Check and delete generated dependency graph
shell: bash
run: |
if [ ! -e "${{ steps.dependency-submission.outputs.dependency-graph-file }}" ]; then
echo "Did not find generated dependency graph files"
exit 1
fi
rm ${{ steps.dependency-submission.outputs.dependency-graph-file }}*
- name: Run Gradle build
run: ./gradlew build
working-directory: .github/workflow-samples/groovy-dsl
- name: Check no dependency graph is generated
shell: bash
run: |
if [ ! -z "$(ls -A dependency-graph-reports)" ]; then
echo "Expected no dependency graph files to be generated"
ls -l dependency-graph-reports
exit 1
fi
with-includes-and-excludes:
runs-on: ubuntu-latest # Test is not compatible with Windows
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Generate and submit dependencies
id: dependency-submission
uses: ./dependency-submission
with:
build-root-directory: .github/workflow-samples/groovy-dsl
dependency-graph-exclude-projects: excluded-project
dependency-graph-include-projects: included-project
dependency-graph-exclude-configurations: excluded-configuration
dependency-graph-include-configurations: included-configuration
- name: Check generated dependency graph and env vars
shell: bash
run: |
if [ ! -e "${{ steps.dependency-submission.outputs.dependency-graph-file }}" ]; then
echo "Did not find generated dependency graph file"
exit 1
fi
if [ "$DEPENDENCY_GRAPH_EXCLUDE_PROJECTS" != "excluded-project" ] ||
[ "$DEPENDENCY_GRAPH_INCLUDE_PROJECTS" != "included-project" ] ||
[ "$DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS" != "excluded-configuration" ] ||
[ "$DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS" != "included-configuration" ]; then
echo "Did not set expected environment variables"
exit 1
fi
custom-report-dir-submit:
strategy:
@@ -262,9 +313,8 @@ jobs:
uses: ./dependency-submission
with:
dependency-graph: generate-and-submit
dependency-graph-report-dir: '${{ github.workspace }}/custom/report-dir'
build-root-directory: .github/workflow-samples/groovy-dsl
env:
DEPENDENCY_GRAPH_REPORT_DIR: '${{ github.workspace }}/custom/report-dir'
- name: Check generated dependency graphs
shell: bash
run: |
@@ -293,9 +343,8 @@ jobs:
uses: ./dependency-submission
with:
dependency-graph: generate-and-upload
dependency-graph-report-dir: '${{ github.workspace }}/custom/report-dir'
build-root-directory: .github/workflow-samples/groovy-dsl
env:
DEPENDENCY_GRAPH_REPORT_DIR: '${{ github.workspace }}/custom/report-dir'
custom-report-dir-download-and-submit:
needs: custom-report-dir-upload
@@ -310,9 +359,11 @@ jobs:
uses: ./dependency-submission
with:
dependency-graph: download-and-submit
dependency-graph-report-dir: '${{ github.workspace }}/custom/report-dir'
build-root-directory: .github/workflow-samples/groovy-dsl
env:
DEPENDENCY_GRAPH_REPORT_DIR: '${{ github.workspace }}/custom/report-dir'
DEPENDENCY_GRAPH_DOWNLOAD_ARTIFACT_NAME: custom-report-dir-upload # For testing, to avoid downloading artifacts from other worklfows
- name: Check downloaded dependency graph
shell: bash
run: |

View File

@@ -1,60 +0,0 @@
name: Test execution with caching
on:
workflow_call:
inputs:
cache-key-prefix:
type: string
runner-os:
type: string
default: '["ubuntu-latest", "windows-latest", "macos-latest"]'
skip-dist:
type: boolean
default: false
env:
SKIP_DIST: ${{ inputs.skip-dist }}
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: execution-with-caching-${{ inputs.cache-key-prefix }}
GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true
jobs:
seed-build:
strategy:
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Execute Gradle build
uses: ./setup-gradle
with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
build-root-directory: .github/workflow-samples/groovy-dsl
arguments: test
# Test that the gradle-user-home is restored
verify-build:
needs: seed-build
strategy:
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Execute Gradle build
uses: ./setup-gradle
with:
cache-read-only: true
build-root-directory: .github/workflow-samples/groovy-dsl
arguments: test --offline -DverifyCachedBuild=true

View File

@@ -1,102 +0,0 @@
name: Test execution
on:
workflow_call:
inputs:
cache-key-prefix:
type: string
runner-os:
type: string
default: '["ubuntu-latest", "windows-latest", "macos-latest"]'
skip-dist:
type: boolean
default: false
env:
SKIP_DIST: ${{ inputs.skip-dist }}
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: execution-${{ inputs.cache-key-prefix }}
jobs:
# Tests for executing with different Gradle versions.
# Each build verifies that it is executed with the expected Gradle version.
gradle-execution:
strategy:
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
include:
- os: windows-latest
script-suffix: '.bat'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Test use defined Gradle version
uses: ./setup-gradle
with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
gradle-version: 6.9
build-root-directory: .github/workflow-samples/no-wrapper
arguments: help -DgradleVersionCheck=6.9
- name: Test use Gradle version alias
uses: ./setup-gradle
with:
gradle-version: release-candidate
build-root-directory: .github/workflow-samples/no-wrapper
arguments: help
- name: Test with non-executable wrapper
uses: ./setup-gradle
with:
gradle-version: wrapper
build-root-directory: .github/workflow-samples/non-executable-wrapper
arguments: help
gradle-versions:
strategy:
fail-fast: false
matrix:
gradle: [7.5.1, 6.9.2, 5.6.4, 4.10.3, 3.5.1]
os: ${{fromJSON(inputs.runner-os)}}
include:
- java-version: 11
- gradle: 5.6.4
build-root-suffix: -gradle-5
- gradle: 4.10.3
build-root-suffix: -gradle-4
- gradle: 3.5.1
build-root-suffix: -gradle-4
java-version: 8
exclude:
- os: macos-latest # Java 8 is not supported on macos-latest, so we cannot test Gradle 3.5.1
gradle: 3.5.1
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java-version }}
- name: Run Gradle build
uses: ./setup-gradle
id: gradle
with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
gradle-version: ${{matrix.gradle}}
build-root-directory: .github/workflow-samples/no-wrapper${{ matrix.build-root-suffix }}
arguments: help -DgradleVersionCheck=${{matrix.gradle}}
- name: Check Build Scan url
if: ${{ !steps.gradle.outputs.build-scan-url }}
uses: actions/github-script@v7
with:
script: |
core.setFailed('No Build Scan detected')

View File

@@ -32,11 +32,11 @@ jobs:
matrix:
gradle: [current, 7.6.2, 6.9.4, 5.6.4]
os: ${{fromJSON(inputs.runner-os)}}
plugin-version: [3.16.2, 3.17.5]
plugin-version: [3.16.2, 3.17.6]
include:
- plugin-version: 3.16.2
accessKeyEnv: GRADLE_ENTERPRISE_ACCESS_KEY
- plugin-version: 3.17.5
- plugin-version: 3.17.6
accessKeyEnv: DEVELOCITY_ACCESS_KEY
runs-on: ${{ matrix.os }}
@@ -83,7 +83,7 @@ jobs:
matrix:
gradle: [current, 7.6.2, 6.9.4, 5.6.4]
os: ${{fromJSON(inputs.runner-os)}}
plugin-version: [3.16.2, 3.17.5]
plugin-version: [3.16.2, 3.17.6]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
@@ -132,7 +132,7 @@ jobs:
matrix:
gradle: [ current, 7.6.2, 6.9.4, 5.6.4 ]
os: ${{fromJSON(inputs.runner-os)}}
plugin-version: [ 3.16.2, 3.17.5 ]
plugin-version: [ 3.16.2, 3.17.6 ]
runs-on: ubuntu-latest
steps:
- name: Checkout sources
@@ -168,7 +168,7 @@ jobs:
matrix:
gradle: [ current, 7.6.2, 6.9.4, 5.6.4 ]
os: ${{fromJSON(inputs.runner-os)}}
plugin-version: [ 3.16.2, 3.17.5 ]
plugin-version: [ 3.16.2, 3.17.6 ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources

View File

@@ -25,9 +25,6 @@ jobs:
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
include:
- os: windows-latest
script-suffix: '.bat'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
@@ -62,6 +59,9 @@ jobs:
uses: ./setup-gradle
with:
gradle-version: current
- name: Test use current
working-directory: .github/workflow-samples/no-wrapper
run: gradle help
- name: Check current version output parameter
if: ${{ !startsWith(steps.gradle-current.outputs.gradle-version , '8.') }}
uses: actions/github-script@v7
@@ -73,7 +73,7 @@ jobs:
strategy:
fail-fast: false
matrix:
gradle: [7.3, 6.9, 5.6.4, 4.10.3, 3.5.1]
gradle: [8.9, 8.8, 7.6.4, 6.9.4, 5.6.4, 4.10.3, 3.5.1] # 8.8 is the latest installed on windows runners
os: ${{fromJSON(inputs.runner-os)}}
include:
- java-version: 11

View File

@@ -43,6 +43,7 @@ jobs:
uses: ./setup-gradle
with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
cache-write-only: true # Ensure we start with a clean cache entry
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
gradle-version: 8.6
- name: Groovy build with configuration-cache enabled
@@ -52,6 +53,7 @@ jobs:
verify-build-groovy:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-groovy
GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION: ${{github.sha}}_1
needs: seed-build-groovy
strategy:
fail-fast: false
@@ -64,6 +66,47 @@ jobs:
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Java to ensure consistency
uses: actions/setup-java@v4
with:
distribution: 'liberica'
java-version: 17
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-read-only: false
cache-cleanup: on-success
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
gradle-version: 8.6
- name: Groovy build with configuration-cache enabled
id: execute
working-directory: .github/workflow-samples/groovy-dsl
run: gradle test --configuration-cache
- name: Verify configuration-cache hit
shell: bash
run: |
if [ -e ".github/workflow-samples/groovy-dsl/task-configured.txt" ]; then
echo "Configuration cache was not used - task was configured unexpectedly"
exit 1
fi
# Ensure that cache-cleanup doesn't remove all necessary files
verify-no-cache-cleanup-groovy:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-groovy
GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION: ${{github.sha}}_2
needs: verify-build-groovy
strategy:
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Java to ensure consistency
uses: actions/setup-java@v4
with:
@@ -79,19 +122,19 @@ jobs:
id: execute
working-directory: .github/workflow-samples/groovy-dsl
run: gradle test --configuration-cache
- name: Check that configuration-cache was used
uses: actions/github-script@v7
with:
script: |
const fs = require('fs')
if (fs.existsSync('.github/workflow-samples/groovy-dsl/task-configured.txt')) {
core.setFailed('Configuration cache was not used - task was configured unexpectedly')
}
- name: Verify configuration-cache hit
shell: bash
run: |
if [ -e ".github/workflow-samples/groovy-dsl/task-configured.txt" ]; then
echo "Configuration cache was not used - task was configured unexpectedly"
exit 1
fi
# Check that the build can run when no extracted cache entries are restored
gradle-user-home-not-fully-restored:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-groovy
GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION: ${{github.sha}}_x
needs: seed-build-groovy
strategy:
fail-fast: false
@@ -144,6 +187,7 @@ jobs:
uses: ./setup-gradle
with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
cache-write-only: true # Ensure we start with a clean cache entry
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
gradle-version: 8.6
- name: Execute 'help' with configuration-cache enabled
@@ -152,7 +196,8 @@ jobs:
modify-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-kotlin-modified
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-kotlin
GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION: ${{github.sha}}_1
needs: seed-build-kotlin
strategy:
fail-fast: false
@@ -183,7 +228,8 @@ jobs:
# Test restore configuration-cache from the third build invocation
verify-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-kotlin-modified
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-kotlin
GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION: ${{github.sha}}_2
needs: modify-build-kotlin
strategy:
fail-fast: false
@@ -211,12 +257,10 @@ jobs:
id: execute
working-directory: .github/workflow-samples/kotlin-dsl
run: gradle test --configuration-cache
- name: Check that configuration-cache was used
uses: actions/github-script@v7
with:
script: |
const fs = require('fs')
if (fs.existsSync('.github/workflow-samples/kotlin-dsl/task-configured.txt')) {
core.setFailed('Configuration cache was not used - task was configured unexpectedly')
}
- name: Verify configuration-cache hit
shell: bash
run: |
if [ -e ".github/workflow-samples/kotlin-dsl/task-configured.txt" ]; then
echo "Configuration cache was not used - task was configured unexpectedly"
exit 1
fi

View File

@@ -29,8 +29,8 @@ jobs:
- name: Run wrapper-validation-action
id: setup-gradle
uses: ./setup-gradle
with:
validate-wrappers: true
env:
ALLOWED_GRADLE_WRAPPER_CHECKSUMS: ''
continue-on-error: true
- name: Check failure

View File

@@ -57,6 +57,19 @@ inputs:
Configuration-cache data will not be saved/restored without an encryption key being provided.
required: false
cache-cleanup:
description: |
Specifies if the action should attempt to remove any stale/unused entries from the Gradle User Home prior to saving to the GitHub Actions cache.
By default, no cleanup is performed. It can be configured to run every time, or only when all Gradle builds succeed for the Job.
Valid values are 'never', 'on-success' and 'always'.
required: false
default: 'on-success'
gradle-home-cache-cleanup:
description: When 'true', the action will attempt to remove any stale/unused entries from the Gradle User Home prior to saving to the GitHub Actions cache.
required: false
deprecation-message: This input has been superceded by the 'cache-cleanup' input parameter.
gradle-home-cache-includes:
description: Paths within Gradle User Home to cache.
required: false
@@ -68,11 +81,6 @@ inputs:
description: Paths within Gradle User Home to exclude from cache.
required: false
gradle-home-cache-cleanup:
description: When 'true', the action will attempt to remove any stale/unused entries from the Gradle User Home prior to saving to the GitHub Actions cache.
required: false
default: false
# Job summary configuration
add-job-summary:
description: Specifies when a Job Summary should be inluded in the action results. Valid values are 'never', 'always' (default), and 'on-failure'.
@@ -99,11 +107,42 @@ inputs:
required: false
default: 'generate-and-submit'
dependency-graph-report-dir:
description: |
Specifies where the dependency graph report will be generated.
Paths can relative or absolute. Relative paths are resolved relative to the workspace directory.
required: false
default: 'dependency-graph-reports'
dependency-graph-continue-on-failure:
description: When 'false' a failure to generate or submit a dependency graph will fail the Step or Job. When 'true' a warning will be emitted but no failure will result.
required: false
default: false
dependency-graph-exclude-projects:
description: |
Gradle projects that should be excluded from dependency graph (regular expression).
When set, any matching project will be excluded.
required: false
dependency-graph-include-projects:
description: |
Gradle projects that should be included in dependency graph (regular expression).
When set, only matching projects will be included.
required: false
dependency-graph-exclude-configurations:
description: |
Gradle configurations that should be included in dependency graph (regular expression).
When set, anymatching configurations will be excluded.
required: false
dependency-graph-include-configurations:
description: |
Gradle configurations that should be included in dependency graph (regular expression).
When set, only matching configurations will be included.
required: false
artifact-retention-days:
description: Specifies the number of days to retain any artifacts generated by the action. If not set, the default retention settings for the repository will apply.
required: false
@@ -133,22 +172,22 @@ inputs:
description: The Develocity short-lived access tokens expiry in hours. Default is 2 hours.
required: false
# Wrapper validation configuration
validate-wrappers:
description: |
When 'true' the action will automatically validate all wrapper jars found in the repository.
If the wrapper checksums are not valid, the action will fail.
required: false
default: false
allow-snapshot-wrappers:
description: |
When 'true', wrapper validation will include the checksums of snapshot wrapper jars.
Use this if you are running with nightly or snapshot versions of the Gradle wrapper.
required: false
default: false
# DEPRECATED ACTION INPUTS
build-scan-terms-of-service-url:
description: The URL to the Build Scan® terms of use. This input must be set to 'https://gradle.com/terms-of-service'.
required: false
deprecation-message: The input has been renamed to align with the Develocity API. Use 'build-scan-terms-of-use-url' instead.
build-scan-terms-of-service-agree:
description: Indicate that you agree to the Build Scan® terms of use. This input value must be "yes".
required: false
deprecation-message: The input has been renamed to align with the Develocity API. Use 'build-scan-terms-of-use-agree' instead.
generate-job-summary:
description: When 'false', no Job Summary will be generated for the Job.
required: false
default: true
deprecation-message: Superceded by the new 'add-job-summary' and 'add-job-summary-as-pr-comment' parameters.
# EXPERIMENTAL ACTION INPUTS
# The following action properties allow fine-grained tweaking of the action caching behaviour.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -102,6 +102,13 @@ In some cases, the default action configuration will not be sufficient, and addi
# Do not attempt to submit the dependency-graph. Save it as a workflow artifact.
dependency-graph: generate-and-upload
# Specify the location where dependency graph files will be generated.
dependency-graph-report-dir: custom-report-dir
# By default, failure to generate a dependency graph will cause the workflow to fail
dependency-graph-continue-on-failure: true
```
See the [Action Metadata file](../dependency-submission/action.yml) for a more detailed description of each input parameter.
@@ -235,26 +242,26 @@ contribute to the dependency graph.
> These dependencies would be assigned to different scopes (eg development, runtime, testing) and the GitHub UI would make it easy to opt-in to security alerts for different dependency scopes.
> However, this functionality does not yet exist.
### Excluding certain Gradle projects from the dependency graph
### Selecting Gradle projects that will contribute to the dependency graph
If you do not want the dependency graph to include dependencies from every project in your build,
you can easily exclude certain projects from the dependency extraction process.
you can easily exclude or include certain projects from the dependency extraction process.
To restrict which Gradle subprojects contribute to the report, specify which projects to exclude via a regular expression.
You can provide this value via the `DEPENDENCY_GRAPH_EXCLUDE_PROJECTS` environment variable or system property.
To restrict which Gradle subprojects contribute to the report, specify which projects to exclude or include via a regular expression.
You can use the `dependency-graph-exclude-projects` and `dependency-graph-include-projects` input parameters for this purpose.
Note that excluding a project in this way only removes dependencies that are _resolved_ as part of that project, and may
not necessarily remove all dependencies _declared_ in that project. If another project depends on the excluded project
then it may transitively resolve dependencies declared in the excluded project: these dependencies will still be included
in the generated dependency graph.
### Excluding certain Gradle configurations from the dependency graph
### Selecting Gradle configurations that will contribute to the dependency graph
Similarly to Gradle projects, it is possible to exclude a set of configuration instances from dependency graph generation,
so that dependencies resolved by those configurations are not included.
Similarly to Gradle projects, it is possible to exclude or include a set of dependency configurations from dependency graph generation,
so that only dependencies resolved by the included configurations are reported.
To restrict which Gradle configurations contribute to the report, specify which configurations to exclude via a regular expression.
You can provide this value via the `DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS` environment variable or system property.
To restrict which Gradle configurations contribute to the report, specify which configurations to exclude or include via a regular expression.
You can use the `dependency-graph-exclude-configurations` and `dependency-graph-include-configurations` input parameters for this purpose.
Note that configuration exclusion applies to the configuration in which the dependency is _resolved_ which is not necessarily
the configuration where the dependency is _declared_. For example if you decare a dependency as `implementation` in
@@ -262,24 +269,18 @@ a Java project, that dependency will be resolved in `compileClasspath`, `runtime
### Example of project and configuration filtering
For example, if you want to exclude dependencies in the `buildSrc` project, and exclude dependencies from the `testCompileClasspath` and `testRuntimeClasspath` configurations, you would use the following configuration:
For example, if you want to exclude dependencies resolved by the `buildSrc` project, and exclude dependencies from the `testCompileClasspath` and `testRuntimeClasspath` configurations, you would use the following configuration:
```yaml
- name: Generate and submit dependency graph
uses: gradle/actions/dependency-submission@v3
env:
with:
# Exclude all dependencies that originate solely in the 'buildSrc' project
DEPENDENCY_GRAPH_EXCLUDE_PROJECTS: ':buildSrc'
dependency-graph-exclude-projets: ':buildSrc'
# Exclude dependencies that are only resolved in test classpaths
DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS: '.*[Tt]est(Compile|Runtime)Classpath'
dependency-graph-exclude-configurations: '.*[Tt]est(Compile|Runtime)Classpath'
```
### Other filtering options
The [GitHub Dependency Graph Gradle Plugin](https://plugins.gradle.org/plugin/org.gradle.github-dependency-graph-gradle-plugin)
has other filtering options that may be useful.
See [the docs](https://github.com/gradle/github-dependency-graph-gradle-plugin?tab=readme-ov-file#filtering-which-gradle-configurations-contribute-to-the-dependency-graph) for details.
# Advance usage scenarios
## Using a custom plugin repository
@@ -418,3 +419,10 @@ Gradle versions `5.2.1`, `5.6.4`, `6.0.1`, `6.9.4`, `7.1.1` and `7.6.3`, as well
A known exception to this is that Gradle `7.0`, `7.0.1` and `7.0.2` are not supported.
See [here](https://github.com/gradle/github-dependency-graph-gradle-plugin?tab=readme-ov-file#gradle-compatibility) for complete compatibility information.
# Additional references
- Dependency Submission Demo repository: https://github.com/gradle/github-dependency-submission-demo
- GitHub Dependency Graph Gradle Plugin: https://github.com/gradle/github-dependency-graph-gradle-plugin
- Webinar - Gradle at Scale with GitHub and GitHub Actions at Allegro: https://www.youtube.com/watch?v=gV94I28FPos

View File

@@ -148,3 +148,15 @@ These deprecated build-scan parameters are scheduled to be removed in `setup-gra
Gradle Enterprise has been renamed to Develocity starting from Gradle plugin 3.17 and Develocity server 2024.1.
In v4 release of the action, it will require setting the access key with the `develocity-access-key` input and Develocity 2024.1 at least to generate short-lived tokens.
If those requirements are not met, the `GRADLE_ENTERPRISE_ACCESS_KEY` env var will be cleared out and build scan publication or other authenticated Develocity operations won't be possible.
## The `gradle-home-cache-cleanup` input parameter has been replaced by `cache-cleanup`
In versions of the action prior to `v4`, the boolean `gradle-home-cache-cleanup` parameter allows users to opt-in
to cache cleanup, removing unused files in Gradle User Home prior to saving to the cache.
With `v4`, cache-cleanup is enabled by default, and controlled by the `cache-cleanup` input parameter.
To remove this deprecation:
- If you are using `gradle-home-cache-cleanup: true` in your workflow, you can remove this option as this is now enabled by default.
- If you want cache-cleanup to run even when a Gradle build fails, then add the `cache-cleanup: always` input.
- If cache-cleanup is causing problems with your workflow, you can disable it with `cache-cleanup: never`.

View File

@@ -153,9 +153,32 @@ In certain circumstances it may be desirable to start with a clean Gradle User H
cache-write-only: true
```
### Configuring cache cleanup
The Gradle User Home directory tends to grow over time. When you switch to a new Gradle wrapper version
or upgrade a dependency version the old files are not automatically and immediately removed.
While this can make sense in a local environment, in a GitHub Actions environment
it can lead to ever-larger Gradle User Home cache entries being saved and restored.
To avoid this situation, the `setup-gradle` and `dependency-submission` actions will perform "cache-cleanup",
purging any unused files from the Gradle User Home before saving it to the GitHub Actions cache.
Cache cleanup will attempt to remove any files that are initially restored to the Gradle User Home directory
but that are not used used by Gradle during the GitHub Actions Workflow.
If a Gradle build fails when running the Job, then it is possible that some required files and dependencies
will not be touched during the Job. To prevent these files from being purged, the default behavior is for
cache cleanup to run only when all Gradle builds in the Job are successful.
Gradle Home cache cleanup is enabled by default, and can be controlled by the `cache-cleanup` parameter as follows:
- `cache-cleanup: always`: Always run cache cleanup, even when a Gradle build fails in the Job.
- `cache-cleanup: on-success` (default): Run cache cleanup when the Job contains no failing Gradle builds.
- `cache-cleanup: never`: Disable cache cleanup for the Job.
Cache cleanup will never run when the cache is configured as read-only or disabled.
### Overwriting an existing Gradle User Home
When the action detects that the Gradle User Home caches directory already exists (`~/.gradle/caches`), then by default it will not overwrite the existing content of this directory.
When the action detects that the Gradle User Home caches directory already exists (`$GRADLE_USER_HOME/caches`), then by default it will not overwrite the existing content of this directory.
This can occur when a prior action initializes this directory, or when using a self-hosted runner that retains this directory between uses.
In this case, the Job Summary will display a message like:
@@ -356,20 +379,6 @@ gradle-home-cache-excludes: |
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.
### Remove unused files from Gradle User Home before saving to the cache
The Gradle User Home directory tends to grow over time. When you switch to a new Gradle wrapper version or upgrade a dependency version
the old files are not automatically and immediately removed. While this can make sense in a local environment, in a GitHub Actions environment
it can lead to ever-larger Gradle User Home cache entries being saved and restored.
To avoid this situation, The `setup-gradle` action supports the `gradle-home-cache-cleanup` parameter.
When enabled, this feature will attempt to delete any files in the Gradle User Home that were not used by Gradle during the GitHub Actions Workflow, before saving the Gradle User Home to the GitHub Actions cache.
Gradle Home cache cleanup is considered experimental and is disabled by default. You can enable this feature for the action as follows:
```yaml
gradle-home-cache-cleanup: true
```
### Disable local build-cache when remote build-cache is available
If you have a remote build-cache available for your build, then it is recommended to do the following:
@@ -506,14 +515,30 @@ Since Gradle applies init scripts in alphabetical order, one way to ensure this
## Gradle Wrapper validation
Instead of using the [wrapper-validation action](./wrapper-validation.md) separately, you can enable
wrapper validation directly in your Setup Gradle step.
By default, this action will perform the same wrapper validation as is performed by the dedicated
[wrapper-validation action](./wrapper-validation.md).
This means that invalid wrapper jars will be automatically detected when using `setup-gradle`.
If you do not want wrapper-validation to occur automatically, you can disable it:
```yaml
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
validate-wrappers: false
```
If your repository uses snapshot versions of the Gradle wrapper, such as nightly builds, then you'll need to
explicitly allow snapshot wrappers in wrapper validation.
These are not allowed by default.
```yaml
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
validate-wrappers: true
allow-snapshot-wrappers: true
```
If you need more advanced configuration, then you're advised to continue using a separate workflow step
@@ -724,7 +749,7 @@ Here's a minimal example:
run: ./gradlew build
```
This configuration will automatically apply `v3.17.5` of the [Develocity Gradle plugin](https://docs.gradle.com/develocity/gradle-plugin/), and publish build scans to https://develocity.your-server.com.
This configuration will automatically apply `v3.17.6` of the [Develocity Gradle plugin](https://docs.gradle.com/develocity/gradle-plugin/), and publish build scans to https://develocity.your-server.com.
This example assumes that the `develocity.your-server.com` server allows anonymous publishing of build scans.
In the likely scenario that your Develocity server requires authentication, you will also need to pass a valid [Develocity access key](https://docs.gradle.com/develocity/gradle-plugin/#via_environment_variable) taken from a secret:
@@ -796,7 +821,7 @@ Here's an example using the env vars:
env:
DEVELOCITY_INJECTION_ENABLED: true
DEVELOCITY_URL: https://develocity.your-server.com
DEVELOCITY_PLUGIN_VERSION: 3.17.5
DEVELOCITY_PLUGIN_VERSION: 3.17.6
```
## Publishing to scans.gradle.com
@@ -817,3 +842,21 @@ To publish to https://scans.gradle.com, you must specify in your workflow that y
- name: Run a Gradle build - a build scan will be published automatically
run: ./gradlew build
```
# Dependency verification
Develocity injection, Build Scan publishing and Dependency Graph generation all work by applying external plugins to your build.
If you project has [dependency verification enabled](https://docs.gradle.org/current/userguide/dependency_verification.html#sec:signature-verification),
then you'll need to update your verification metadata to trust these plugins.
Each of the plugins is signed by Gradle, and you can simply add the following snippet to your `dependency-verificaton.xml` file:
```xml
<trusted-keys>
<trusted-key id="7B79ADD11F8A779FE90FD3D0893A028475557671">
<trusting group="com.gradle"/>
<trusting group="org.gradle"/>
</trusted-key>
</trusted-keys>
```

View File

@@ -4,6 +4,9 @@ This action validates the checksums of _all_ [Gradle Wrapper](https://docs.gradl
The action should be run in the root of the repository, as it will recursively search for any files named `gradle-wrapper.jar`.
The `setup-gradle` action will perform wrapper validation on each execution. If you are using `setup-gradle` in your
workflows, it is unlikely that you will need to use this action.
## The Gradle Wrapper Problem in Open Source
The `gradle-wrapper.jar` is a binary blob of executable code that is checked into nearly
@@ -90,18 +93,22 @@ We recommend the message commit contents of:
From there, you can easily follow the rest of the prompts to create a Pull Request against the project.
## Reporting Failures
## Validation Failures
If this GitHub action fails because a `gradle-wrapper.jar` doesn't match one of our published SHA-256 checksums,
A wrapper jar can fail validation for a few reasons:
1. The wrapper is from a snapshot build of Gradle (nightly or release nightly) and you have not set `allow-snapshots`
or `allow-snapshot-wrappers` to `true`.
2. The wrapper jar is from a version of Gradle with an unverifiable wrapper jar (see below).
3. The wrapper jar was not published by Gradle, and could be compromised.
If this GitHub action fails because a `gradle-wrapper.jar` was not published by Gradle,
we highly recommend that you reach out to us at [security@gradle.com](mailto:security@gradle.com).
**Note:** `gradle-wrapper.jar` generated by Gradle 3.3 to 4.0 are not verifiable because those files were dynamically generated by Gradle in a non-reproducible way. It's not possible to verify the `gradle-wrapper.jar` for those versions are legitimate using a hash comparison. You should try to determine if the `gradle-wrapper.jar` was generated by one of these versions before running the build.
#### Unverifiable Wrapper Jars
Wrapper Jars generated by Gradle versions `3.3` to `4.0` are not verifiable because those files were dynamically generated by Gradle in a non-reproducible way. It's not possible to verify the `gradle-wrapper.jar` for those versions are legitimate using a hash comparison. If you have a validation failure, you should try to determine if the `gradle-wrapper.jar` was generated by one of these versions before running the build.
If the Gradle version in `gradle-wrapper.properties` is out of this range, you may need to regenerate the `gradle-wrapper.jar` by running `./gradlew wrapper`. If you need to use a version of Gradle between 3.3 and 4.0, you can use a newer version of Gradle to generate the `gradle-wrapper.jar`.
If you're curious and want to explore what the differences are between the `gradle-wrapper.jar` in your possession
and one of our valid release, you can compare them using this online utility: [diffoscope](https://try.diffoscope.org/).
Regardless of what you find, we still kindly request that you reach out to us and let us know.
- If the Gradle version in `gradle-wrapper.properties` is outside of this range, you can regenerate the `gradle-wrapper.jar` by running `./gradlew wrapper`. This will generate a new, verifiable wrapper jar.
- If you need to run your build with a version of Gradle between 3.3 and 4.0, you can use a newer version of Gradle to generate the `gradle-wrapper.jar`.
## Resources

View File

@@ -40,6 +40,19 @@ inputs:
Configuration-cache data will not be saved/restored without an encryption key being provided.
required: false
cache-cleanup:
description: |
Specifies if the action should attempt to remove any stale/unused entries from the Gradle User Home prior to saving to the GitHub Actions cache.
By default, no cleanup is performed. It can be configured to run every time, or only when all Gradle builds succeed for the Job.
Valid values are 'never', 'on-success' and 'always'.
required: false
default: 'on-success'
gradle-home-cache-cleanup:
description: When 'true', the action will attempt to remove any stale/unused entries from the Gradle User Home prior to saving to the GitHub Actions cache.
required: false
deprecation-message: This input has been superceded by the 'cache-cleanup' input parameter.
gradle-home-cache-includes:
description: Paths within Gradle User Home to cache.
required: false
@@ -51,11 +64,6 @@ inputs:
description: Paths within Gradle User Home to exclude from cache.
required: false
gradle-home-cache-cleanup:
description: When 'true', the action will attempt to remove any stale/unused entries from the Gradle User Home prior to saving to the GitHub Actions cache.
required: false
default: false
# Job summary configuration
add-job-summary:
description: Specifies when a Job Summary should be inluded in the action results. Valid values are 'never', 'always' (default), and 'on-failure'.
@@ -71,15 +79,46 @@ inputs:
dependency-graph:
description: |
Specifies if a GitHub dependency snapshot should be generated for each Gradle build, and if so, how.
Valid values are 'disabled' (default), 'generate', 'generate-and-submit', 'generate-and-upload', 'download-and-submit' and 'clear'.
Valid values are 'disabled' (default), 'generate', 'generate-and-submit', 'generate-and-upload', and 'download-and-submit'.
required: false
default: 'disabled'
dependency-graph-report-dir:
description: |
Specifies where the dependency graph report will be generated.
Paths can relative or absolute. Relative paths are resolved relative to the workspace directory.
required: false
default: 'dependency-graph-reports'
dependency-graph-continue-on-failure:
description: When 'false' a failure to generate or submit a dependency graph will fail the Step or Job. When 'true' a warning will be emitted but no failure will result.
required: false
default: true
dependency-graph-exclude-projects:
description: |
Gradle projects that should be excluded from dependency graph (regular expression).
When set, any matching project will be excluded.
required: false
dependency-graph-include-projects:
description: |
Gradle projects that should be included in dependency graph (regular expression).
When set, only matching projects will be included.
required: false
dependency-graph-exclude-configurations:
description: |
Gradle configurations that should be included in dependency graph (regular expression).
When set, anymatching configurations will be excluded.
required: false
dependency-graph-include-configurations:
description: |
Gradle configurations that should be included in dependency graph (regular expression).
When set, only matching configurations will be included.
required: false
artifact-retention-days:
description: Specifies the number of days to retain any artifacts generated by the action. If not set, the default retention settings for the repository will apply.
required: false
@@ -151,37 +190,23 @@ inputs:
# Wrapper validation configuration
validate-wrappers:
description: |
When 'true', the action will perform the 'wrapper-validation' action automatically.
When 'true' (the default) the action will automatically validate all wrapper jars found in the repository.
If the wrapper checksums are not valid, the action will fail.
required: false
default: true
allow-snapshot-wrappers:
description: |
When 'true', wrapper validation will include the checksums of snapshot wrapper jars.
Use this if you are running with nightly or snapshot versions of the Gradle wrapper.
required: false
default: false
# DEPRECATED ACTION INPUTS
build-scan-terms-of-service-url:
description: The URL to the Build Scan® terms of use. This input must be set to 'https://gradle.com/terms-of-service'.
required: false
deprecation-message: The input has been renamed to align with the Develocity API. Use 'build-scan-terms-of-use-url' instead.
build-scan-terms-of-service-agree:
description: Indicate that you agree to the Build Scan® terms of use. This input value must be "yes".
required: false
deprecation-message: The input has been renamed to align with the Develocity API. Use 'build-scan-terms-of-use-agree' instead.
generate-job-summary:
description: When 'false', no Job Summary will be generated for the Job.
required: false
default: true
deprecation-message: Superceded by the new 'add-job-summary' and 'add-job-summary-as-pr-comment' parameters.
arguments:
description: Gradle command line arguments (supports multi-line input)
required: false
deprecation-message: Using the action to execute Gradle directly is deprecated in favor of using the action to setup Gradle, and executing Gradle in a subsequent Step.
build-root-directory:
description: Path to the root directory of the build. Default is the root of the GitHub workspace.
required: false
deprecation-message: Using the action to execute Gradle directly is deprecated in favor of using the action to setup Gradle, and executing Gradle in a subsequent Step.
deprecation-message: This parameter has been deprecated and removed. It is only left here to allow for better reporting to assist users to migrate.
# EXPERIMENTAL ACTION INPUTS
# The following action properties allow fine-grained tweaking of the action caching behaviour.

View File

@@ -20,15 +20,18 @@
"@actions/tool-cache": "2.0.1",
"@octokit/rest": "20.1.0",
"@octokit/webhooks-types": "7.5.0",
"cheerio": "^1.0.0-rc.12",
"semver": "7.6.0",
"string-argv": "0.3.2",
"typed-rest-client": "1.8.11",
"unhomoglyph": "1.0.6"
"unhomoglyph": "1.0.6",
"which": "4.0.0"
},
"devDependencies": {
"@types/jest": "29.5.12",
"@types/node": "20.12.4",
"@types/unzipper": "0.10.9",
"@types/which": "3.0.4",
"@typescript-eslint/parser": "7.5.0",
"@vercel/ncc": "0.38.1",
"eslint": "8.57.0",
@@ -2215,6 +2218,12 @@
"@types/node": "*"
}
},
"node_modules/@types/which": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.4.tgz",
"integrity": "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==",
"dev": true
},
"node_modules/@types/yargs": {
"version": "17.0.23",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.23.tgz",
@@ -3301,6 +3310,11 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"node_modules/bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
@@ -3518,6 +3532,42 @@
"node": ">=10"
}
},
"node_modules/cheerio": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
"integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"htmlparser2": "^8.0.1",
"parse5": "^7.0.0",
"parse5-htmlparser2-tree-adapter": "^7.0.0"
},
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/ci-info": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
@@ -3694,12 +3744,53 @@
"node": ">= 8"
}
},
"node_modules/cross-spawn/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -3877,6 +3968,57 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
]
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dot-object": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz",
@@ -3921,6 +4063,17 @@
"once": "^1.4.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -5164,6 +5317,24 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"node_modules/htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -7037,6 +7208,17 @@
"node": ">=8"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -7270,6 +7452,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
"dependencies": {
"entities": "^4.4.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz",
"integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==",
"dependencies": {
"domhandler": "^5.0.2",
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
@@ -8885,18 +9090,17 @@
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"dependencies": {
"isexe": "^2.0.0"
"isexe": "^3.1.1"
},
"bin": {
"node-which": "bin/node-which"
"node-which": "bin/which.js"
},
"engines": {
"node": ">= 8"
"node": "^16.13.0 || >=18.0.0"
}
},
"node_modules/which-boxed-primitive": {
@@ -8949,6 +9153,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which/node_modules/isexe": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -10875,6 +11087,12 @@
"@types/node": "*"
}
},
"@types/which": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.4.tgz",
"integrity": "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==",
"dev": true
},
"@types/yargs": {
"version": "17.0.23",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.23.tgz",
@@ -11613,6 +11831,11 @@
"readable-stream": "^3.4.0"
}
},
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
@@ -11754,6 +11977,33 @@
"integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
"dev": true
},
"cheerio": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
"integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==",
"requires": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"htmlparser2": "^8.0.1",
"parse5": "^7.0.0",
"parse5-htmlparser2-tree-adapter": "^7.0.0"
}
},
"cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"requires": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
}
},
"ci-info": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
@@ -11882,6 +12132,17 @@
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"dependencies": {
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"crypto": {
@@ -11889,6 +12150,23 @@
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
},
"css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"requires": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
}
},
"css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
},
"damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -12015,6 +12293,39 @@
"esutils": "^2.0.2"
}
},
"dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"requires": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
}
},
"domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
},
"domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"requires": {
"domelementtype": "^2.3.0"
}
},
"domutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"requires": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
}
},
"dot-object": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.4.tgz",
@@ -12050,6 +12361,11 @@
"once": "^1.4.0"
}
},
"entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
},
"error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -12960,6 +13276,17 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"htmlparser2": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
"requires": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1",
"entities": "^4.4.0"
}
},
"human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -14379,6 +14706,14 @@
"path-key": "^3.0.0"
}
},
"nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"requires": {
"boolbase": "^1.0.0"
}
},
"object-inspect": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
@@ -14537,6 +14872,23 @@
"lines-and-columns": "^1.1.6"
}
},
"parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
"requires": {
"entities": "^4.4.0"
}
},
"parse5-htmlparser2-tree-adapter": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz",
"integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==",
"requires": {
"domhandler": "^5.0.2",
"parse5": "^7.0.0"
}
},
"pascal-case": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
@@ -15700,12 +16052,18 @@
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"requires": {
"isexe": "^2.0.0"
"isexe": "^3.1.1"
},
"dependencies": {
"isexe": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="
}
}
},
"which-boxed-primitive": {

View File

@@ -8,11 +8,11 @@
"prettier-write": "prettier --write 'src/**/*.ts'",
"prettier-check": "prettier --check 'src/**/*.ts'",
"lint": "eslint 'src/**/*.ts'",
"compile-dependency-submission-main": "ncc build src/dependency-submission/main.ts --out dist/dependency-submission/main --source-map --no-source-map-register",
"compile-dependency-submission-post": "ncc build src/dependency-submission/post.ts --out dist/dependency-submission/post --source-map --no-source-map-register",
"compile-setup-gradle-main": "ncc build src/setup-gradle/main.ts --out dist/setup-gradle/main --source-map --no-source-map-register",
"compile-setup-gradle-post": "ncc build src/setup-gradle/post.ts --out dist/setup-gradle/post --source-map --no-source-map-register",
"compile-wrapper-validation-main": "ncc build src/wrapper-validation/main.ts --out dist/wrapper-validation/main --source-map --no-source-map-register",
"compile-dependency-submission-main": "ncc build src/actions/dependency-submission/main.ts --out dist/dependency-submission/main --source-map --no-source-map-register",
"compile-dependency-submission-post": "ncc build src/actions/dependency-submission/post.ts --out dist/dependency-submission/post --source-map --no-source-map-register",
"compile-setup-gradle-main": "ncc build src/actions/setup-gradle/main.ts --out dist/setup-gradle/main --source-map --no-source-map-register",
"compile-setup-gradle-post": "ncc build src/actions/setup-gradle/post.ts --out dist/setup-gradle/post --source-map --no-source-map-register",
"compile-wrapper-validation-main": "ncc build src/actions/wrapper-validation/main.ts --out dist/wrapper-validation/main --source-map --no-source-map-register",
"compile": "npm-run-all --parallel compile-*",
"check": "npm-run-all --parallel prettier-check lint",
"format": "npm-run-all --parallel prettier-write lint",
@@ -42,15 +42,18 @@
"@actions/tool-cache": "2.0.1",
"@octokit/rest": "20.1.0",
"@octokit/webhooks-types": "7.5.0",
"cheerio": "^1.0.0-rc.12",
"semver": "7.6.0",
"string-argv": "0.3.2",
"typed-rest-client": "1.8.11",
"unhomoglyph": "1.0.6"
},
"unhomoglyph": "1.0.6",
"which": "4.0.0"
},
"devDependencies": {
"@types/jest": "29.5.12",
"@types/node": "20.12.4",
"@types/unzipper": "0.10.9",
"@types/which": "3.0.4",
"@typescript-eslint/parser": "7.5.0",
"@vercel/ncc": "0.38.1",
"eslint": "8.57.0",
@@ -59,11 +62,11 @@
"eslint-plugin-prettier": "5.1.3",
"jest": "29.7.0",
"js-yaml": "4.1.0",
"nock": "13.5.4",
"npm-run-all": "4.1.5",
"patch-package": "8.0.0",
"prettier": "3.2.5",
"ts-jest": "29.1.2",
"typescript": "5.4.3",
"nock": "13.5.4"
"typescript": "5.4.3"
}
}

View File

@@ -1,6 +1,7 @@
import * as setupGradle from '../setup-gradle'
import * as gradle from '../execution/gradle'
import * as dependencyGraph from '../dependency-graph'
import * as core from '@actions/core'
import * as setupGradle from '../../setup-gradle'
import * as gradle from '../../execution/gradle'
import * as dependencyGraph from '../../dependency-graph'
import {parseArgsStringToArgv} from 'string-argv'
import {
@@ -9,10 +10,11 @@ import {
DependencyGraphConfig,
DependencyGraphOption,
GradleExecutionConfig,
setActionId
} from '../configuration'
import {saveDeprecationState} from '../deprecation-collector'
import {handleMainActionError} from '../errors'
setActionId,
WrapperValidationConfig
} from '../../configuration'
import {saveDeprecationState} from '../../deprecation-collector'
import {handleMainActionError} from '../../errors'
/**
* The main entry point for the action, called by Github Actions for the step.
@@ -22,7 +24,10 @@ export async function run(): Promise<void> {
setActionId('gradle/actions/dependency-submission')
// Configure Gradle environment (Gradle User Home)
await setupGradle.setup(new CacheConfig(), new BuildScanConfig())
await setupGradle.setup(new CacheConfig(), new BuildScanConfig(), new WrapperValidationConfig())
// Capture the enabled state of dependency-graph
const originallyEnabled = process.env['GITHUB_DEPENDENCY_GRAPH_ENABLED']
// Configure the dependency graph submission
const config = new DependencyGraphConfig()
@@ -53,6 +58,9 @@ export async function run(): Promise<void> {
await dependencyGraph.complete(config)
// Reset the enabled state of dependency graph
core.exportVariable('GITHUB_DEPENDENCY_GRAPH_ENABLED', originallyEnabled)
saveDeprecationState()
} catch (error) {
handleMainActionError(error)

View File

@@ -1,7 +1,7 @@
import * as setupGradle from '../setup-gradle'
import * as setupGradle from '../../setup-gradle'
import {CacheConfig, SummaryConfig} from '../configuration'
import {handlePostActionError} from '../errors'
import {CacheConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to

View File

@@ -1,17 +1,17 @@
import * as setupGradle from '../setup-gradle'
import * as gradle from '../execution/gradle'
import * as dependencyGraph from '../dependency-graph'
import * as setupGradle from '../../setup-gradle'
import * as provisioner from '../../execution/provision'
import * as dependencyGraph from '../../dependency-graph'
import {
BuildScanConfig,
CacheConfig,
DependencyGraphConfig,
GradleExecutionConfig,
doValidateWrappers,
WrapperValidationConfig,
getActionId,
setActionId
} from '../configuration'
import {recordDeprecation, saveDeprecationState} from '../deprecation-collector'
import {handleMainActionError} from '../errors'
} from '../../configuration'
import {failOnUseOfRemovedFeature, saveDeprecationState} from '../../deprecation-collector'
import {handleMainActionError} from '../../errors'
/**
* The main entry point for the action, called by Github Actions for the step.
@@ -19,30 +19,22 @@ import {handleMainActionError} from '../errors'
export async function run(): Promise<void> {
try {
if (getActionId() === 'gradle/gradle-build-action') {
recordDeprecation(
failOnUseOfRemovedFeature(
'The action `gradle/gradle-build-action` has been replaced by `gradle/actions/setup-gradle`'
)
} else {
setActionId('gradle/actions/setup-gradle')
}
// Check for invalid wrapper JARs if requested
if (doValidateWrappers()) {
await setupGradle.checkNoInvalidWrapperJars()
}
setActionId('gradle/actions/setup-gradle')
// Configure Gradle environment (Gradle User Home)
await setupGradle.setup(new CacheConfig(), new BuildScanConfig())
await setupGradle.setup(new CacheConfig(), new BuildScanConfig(), new WrapperValidationConfig())
// Configure the dependency graph submission
await dependencyGraph.setup(new DependencyGraphConfig())
const config = new GradleExecutionConfig()
await gradle.provisionAndMaybeExecute(
config.getGradleVersion(),
config.getBuildRootDirectory(),
config.getArguments()
)
config.verifyNoArguments()
await provisioner.provisionGradle(config.getGradleVersion())
saveDeprecationState()
} catch (error) {

View File

@@ -1,9 +1,9 @@
import * as setupGradle from '../setup-gradle'
import * as dependencyGraph from '../dependency-graph'
import * as setupGradle from '../../setup-gradle'
import * as dependencyGraph from '../../dependency-graph'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../configuration'
import {handlePostActionError} from '../errors'
import {emitDeprecationWarnings, restoreDeprecationState} from '../deprecation-collector'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors'
import {emitDeprecationWarnings, restoreDeprecationState} from '../../deprecation-collector'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to

View File

@@ -1,21 +1,21 @@
import * as path from 'path'
import * as core from '@actions/core'
import * as validate from './validate'
import {getActionId, setActionId} from '../configuration'
import {recordDeprecation, emitDeprecationWarnings} from '../deprecation-collector'
import {handleMainActionError} from '../errors'
import * as validate from '../../wrapper-validation/validate'
import {getActionId, setActionId} from '../../configuration'
import {failOnUseOfRemovedFeature, emitDeprecationWarnings} from '../../deprecation-collector'
import {handleMainActionError} from '../../errors'
export async function run(): Promise<void> {
try {
if (getActionId() === 'gradle/wrapper-validation-action') {
recordDeprecation(
failOnUseOfRemovedFeature(
'The action `gradle/wrapper-validation-action` has been replaced by `gradle/actions/wrapper-validation`'
)
} else {
setActionId('gradle/actions/wrapper-validation')
}
setActionId('gradle/actions/wrapper-validation')
const result = await validate.findInvalidWrapperJars(
path.resolve('.'),
+core.getInput('min-wrapper-count'),

View File

@@ -8,15 +8,40 @@ export interface BuildResult {
get gradleVersion(): string
get gradleHomeDir(): string
get buildFailed(): boolean
get configCacheHit(): boolean
get buildScanUri(): string
get buildScanFailed(): boolean
}
export function loadBuildResults(): BuildResult[] {
return getUnprocessedResults().map(filePath => {
export class BuildResults {
results: BuildResult[]
constructor(results: BuildResult[]) {
this.results = results
}
anyFailed(): boolean {
return this.results.some(result => result.buildFailed)
}
anyConfigCacheHit(): boolean {
return this.results.some(result => result.configCacheHit)
}
uniqueGradleHomes(): string[] {
const allHomes = this.results.map(buildResult => buildResult.gradleHomeDir)
return Array.from(new Set(allHomes))
}
}
export function loadBuildResults(): BuildResults {
const results = getUnprocessedResults().map(filePath => {
const content = fs.readFileSync(filePath, 'utf8')
return JSON.parse(content) as BuildResult
const buildResult = JSON.parse(content) as BuildResult
addScanResults(filePath, buildResult)
return buildResult
})
return new BuildResults(results)
}
export function markBuildResultsProcessed(): void {
@@ -24,7 +49,7 @@ export function markBuildResultsProcessed(): void {
}
function getUnprocessedResults(): string[] {
const buildResultsDir = path.resolve(process.env['RUNNER_TEMP']!, '.build-results')
const buildResultsDir = path.resolve(process.env['RUNNER_TEMP']!, '.gradle-actions', 'build-results')
if (!fs.existsSync(buildResultsDir)) {
return []
}
@@ -39,6 +64,22 @@ function getUnprocessedResults(): string[] {
})
}
function addScanResults(buildResultsFile: string, buildResult: BuildResult): void {
const buildScansDir = path.resolve(process.env['RUNNER_TEMP']!, '.gradle-actions', 'build-scans')
if (!fs.existsSync(buildScansDir)) {
return
}
const buildScanResults = path.resolve(buildScansDir, path.basename(buildResultsFile))
if (fs.existsSync(buildScanResults)) {
const content = fs.readFileSync(buildScanResults, 'utf8')
const scanResults = JSON.parse(content)
Object.assign(buildResult, scanResults)
}
return
}
function isProcessed(resultFile: string): boolean {
const markerFile = `${resultFile}.processed`
return fs.existsSync(markerFile)

View File

@@ -1,7 +1,9 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import fs from 'fs'
import path from 'path'
import {provisionAndMaybeExecute} from '../execution/gradle'
import * as provisioner from '../execution/provision'
export class CacheCleaner {
private readonly gradleUserHome: string
@@ -24,9 +26,8 @@ export class CacheCleaner {
await this.forceCleanupFilesOlderThan(cleanTimestamp)
}
// Visible for testing
async forceCleanupFilesOlderThan(cleanTimestamp: string): Promise<void> {
core.info(`Cleaning up caches before ${cleanTimestamp}`)
// Run a dummy Gradle build to trigger cache cleanup
const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project')
fs.mkdirSync(cleanupProjectDir, {recursive: true})
@@ -54,7 +55,16 @@ export class CacheCleaner {
)
fs.writeFileSync(path.resolve(cleanupProjectDir, 'build.gradle'), 'task("noop") {}')
await provisionAndMaybeExecute('current', cleanupProjectDir, [
const executable = await provisioner.provisionGradle('current')
await core.group('Executing Gradle to clean up caches', async () => {
core.info(`Cleaning up caches last used before ${cleanTimestamp}`)
await this.executeCleanupBuild(executable!, cleanupProjectDir)
})
}
private async executeCleanupBuild(executable: string, cleanupProjectDir: string): Promise<void> {
const args = [
'-g',
this.gradleUserHome,
'-I',
@@ -65,6 +75,13 @@ export class CacheCleaner {
'--build-cache',
'-DGITHUB_DEPENDENCY_GRAPH_ENABLED=false',
'noop'
])
]
const result = await exec.getExecOutput(executable, args, {
cwd: cleanupProjectDir,
silent: true
})
core.info(result.stdout)
}
}

View File

@@ -73,7 +73,8 @@ export function getCacheKeyBase(cacheName: string, cacheProtocolVersion: string)
function getCacheKeyEnvironment(): string {
const runnerOs = process.env['RUNNER_OS'] || ''
return process.env[CACHE_KEY_OS_VAR] || runnerOs
const runnerArch = process.env['RUNNER_ARCH'] || ''
return process.env[CACHE_KEY_OS_VAR] || `${runnerOs}-${runnerArch}`
}
function getCacheKeyJob(): string {

View File

@@ -1,5 +1,27 @@
import * as cache from '@actions/cache'
export const DEFAULT_CACHE_ENABLED_REASON = `[Cache was enabled](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#caching-build-state-between-jobs). Action attempted to both restore and save the Gradle User Home.`
export const DEFAULT_READONLY_REASON = `[Cache was read-only](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#using-the-cache-read-only). By default, the action will only write to the cache for Jobs running on the default branch.`
export const DEFAULT_DISABLED_REASON = `[Cache was disabled](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#disabling-caching) via action confiugration. Gradle User Home was not restored from or saved to the cache.`
export const DEFAULT_WRITEONLY_REASON = `[Cache was set to write-only](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#using-the-cache-write-only) via action configuration. Gradle User Home was not restored from cache.`
export const EXISTING_GRADLE_HOME = `[Cache was disabled to avoid overwriting a pre-existing Gradle User Home](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#overwriting-an-existing-gradle-user-home). Gradle User Home was not restored from or saved to the cache.`
export const CLEANUP_DISABLED_READONLY = `[Cache cleanup](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#configuring-cache-cleanup) is always disabled when cache is read-only or disabled.`
export const DEFAULT_CLEANUP_ENABLED_REASON = `[Cache cleanup](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#configuring-cache-cleanup) was enabled. Stale files in Gradle User Home were purged before saving to the cache.`
export const DEFAULT_CLEANUP_DISABLED_REASON = `[Cache cleanup](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#configuring-cache-cleanup) was disabled via action parameter. No cleanup of Gradle User Home was performed.`
export const CLEANUP_DISABLED_DUE_TO_FAILURE =
'[Cache cleanup was disabled due to build failure](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#configuring-cache-cleanup). Use `cache-cleanup: always` to override this behavior.'
export const CLEANUP_DISABLED_DUE_TO_CONFIG_CACHE_HIT =
'[Cache cleanup was disabled due to configuration-cache reuse](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#configuring-cache-cleanup). This is expected.'
/**
* Collects information on what entries were saved and restored during the action.
* This information is used to generate a summary of the cache usage.
@@ -9,7 +31,8 @@ export class CacheListener {
cacheReadOnly = false
cacheWriteOnly = false
cacheDisabled = false
cacheDisabledReason = 'disabled'
cacheStatusReason: string = DEFAULT_CACHE_ENABLED_REASON
cacheCleanupMessage: string = DEFAULT_CLEANUP_DISABLED_REASON
get fullyRestored(): boolean {
return this.cacheEntries.every(x => !x.wasRequestedButNotRestored())
@@ -17,12 +40,37 @@ export class CacheListener {
get cacheStatus(): string {
if (!cache.isFeatureAvailable()) return 'not available'
if (this.cacheDisabled) return this.cacheDisabledReason
if (this.cacheDisabled) return 'disabled'
if (this.cacheWriteOnly) return 'write-only'
if (this.cacheReadOnly) return 'read-only'
return 'enabled'
}
setReadOnly(reason: string = DEFAULT_READONLY_REASON): void {
this.cacheReadOnly = true
this.cacheStatusReason = reason
this.cacheCleanupMessage = CLEANUP_DISABLED_READONLY
}
setDisabled(reason: string = DEFAULT_DISABLED_REASON): void {
this.cacheDisabled = true
this.cacheStatusReason = reason
this.cacheCleanupMessage = CLEANUP_DISABLED_READONLY
}
setWriteOnly(reason: string = DEFAULT_WRITEONLY_REASON): void {
this.cacheWriteOnly = true
this.cacheStatusReason = reason
}
setCacheCleanupEnabled(): void {
this.cacheCleanupMessage = DEFAULT_CLEANUP_ENABLED_REASON
}
setCacheCleanupDisabled(reason: string = DEFAULT_CLEANUP_DISABLED_REASON): void {
this.cacheCleanupMessage = reason
}
entry(name: string): CacheEntryListener {
for (const entry of this.cacheEntries) {
if (entry.entryName === name) {
@@ -117,6 +165,10 @@ export function generateCachingReport(listener: CacheListener): string {
return `
<details>
<summary><h4>Caching for Gradle actions was ${listener.cacheStatus} - expand for details</h4></summary>
- ${listener.cacheStatusReason}
- ${listener.cacheCleanupMessage}
${renderEntryTable(entries)}
<h5>Cache Entry Details</h5>

View File

@@ -1,9 +1,15 @@
import * as core from '@actions/core'
import {CacheListener} from './cache-reporting'
import {
CacheListener,
EXISTING_GRADLE_HOME,
CLEANUP_DISABLED_DUE_TO_FAILURE,
CLEANUP_DISABLED_DUE_TO_CONFIG_CACHE_HIT
} from './cache-reporting'
import {GradleUserHomeCache} from './gradle-user-home-cache'
import {CacheCleaner} from './cache-cleaner'
import {DaemonController} from '../daemon-controller'
import {CacheConfig} from '../configuration'
import {BuildResults} from '../build-results'
const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED'
@@ -26,7 +32,7 @@ export async function restore(
core.info('Cache is disabled: will not restore state from previous builds.')
// Initialize the Gradle User Home even when caching is disabled.
gradleStateCache.init()
cacheListener.cacheDisabled = true
cacheListener.setDisabled()
return
}
@@ -35,8 +41,7 @@ export async function restore(
core.info('Gradle User Home already exists: will not restore from cache.')
// Initialize pre-existing Gradle User Home.
gradleStateCache.init()
cacheListener.cacheDisabled = true
cacheListener.cacheDisabledReason = 'disabled due to pre-existing Gradle User Home'
cacheListener.setDisabled(EXISTING_GRADLE_HOME)
return
}
core.info('Gradle User Home already exists: will overwrite with cached contents.')
@@ -46,21 +51,21 @@ export async function restore(
// Mark the state as restored so that post-action will perform save.
core.saveState(CACHE_RESTORED_VAR, true)
if (cacheConfig.isCacheCleanupEnabled()) {
core.info('Preparing cache for cleanup.')
const cacheCleaner = new CacheCleaner(gradleUserHome, process.env['RUNNER_TEMP']!)
await cacheCleaner.prepare()
}
if (cacheConfig.isCacheWriteOnly()) {
core.info('Cache is write-only: will not restore from cache.')
cacheListener.cacheWriteOnly = true
cacheListener.setWriteOnly()
return
}
await core.group('Restore Gradle state from cache', async () => {
await gradleStateCache.restore(cacheListener)
})
if (cacheConfig.isCacheCleanupEnabled()) {
core.info('Preparing cache for cleanup.')
const cacheCleaner = new CacheCleaner(gradleUserHome, process.env['RUNNER_TEMP']!)
await cacheCleaner.prepare()
}
}
export async function save(
@@ -68,6 +73,7 @@ export async function save(
gradleUserHome: string,
cacheListener: CacheListener,
daemonController: DaemonController,
buildResults: BuildResults,
cacheConfig: CacheConfig
): Promise<void> {
if (cacheConfig.isCacheDisabled()) {
@@ -82,19 +88,24 @@ export async function save(
if (cacheConfig.isCacheReadOnly()) {
core.info('Cache is read-only: will not save state for use in subsequent builds.')
cacheListener.cacheReadOnly = true
cacheListener.setReadOnly()
return
}
await daemonController.stopAllDaemons()
await core.group('Stopping Gradle daemons', async () => {
await daemonController.stopAllDaemons()
})
if (cacheConfig.isCacheCleanupEnabled()) {
core.info('Forcing cache cleanup.')
const cacheCleaner = new CacheCleaner(gradleUserHome, process.env['RUNNER_TEMP']!)
try {
await cacheCleaner.forceCleanup()
} catch (e) {
core.warning(`Cache cleanup failed. Will continue. ${String(e)}`)
if (buildResults.anyConfigCacheHit()) {
core.info('Not performing cache-cleanup due to config-cache reuse')
cacheListener.setCacheCleanupDisabled(CLEANUP_DISABLED_DUE_TO_CONFIG_CACHE_HIT)
} else if (cacheConfig.shouldPerformCacheCleanup(buildResults.anyFailed())) {
cacheListener.setCacheCleanupEnabled()
await performCacheCleanup(gradleUserHome)
} else {
core.info('Not performing cache-cleanup due to build failure')
cacheListener.setCacheCleanupDisabled(CLEANUP_DISABLED_DUE_TO_FAILURE)
}
}
@@ -102,3 +113,12 @@ export async function save(
return new GradleUserHomeCache(userHome, gradleUserHome, cacheConfig).save(cacheListener)
})
}
async function performCacheCleanup(gradleUserHome: string): Promise<void> {
const cacheCleaner = new CacheCleaner(gradleUserHome, process.env['RUNNER_TEMP']!)
try {
await cacheCleaner.forceCleanup()
} catch (e) {
core.warning(`Cache cleanup failed. Will continue. ${String(e)}`)
}
}

View File

@@ -4,12 +4,11 @@ import * as core from '@actions/core'
import * as glob from '@actions/glob'
import * as semver from 'semver'
import {META_FILE_DIR} from './gradle-user-home-cache'
import {CacheEntryListener, CacheListener} from './cache-reporting'
import {cacheDebug, hashFileNames, isCacheDebuggingEnabled, restoreCache, saveCache, tryDelete} from './cache-utils'
import {BuildResult, loadBuildResults} from '../build-results'
import {CacheConfig} from '../configuration'
import {CacheConfig, ACTION_METADATA_DIR} from '../configuration'
import {getCacheKeyBase} from './cache-key'
const SKIP_RESTORE_VAR = 'GRADLE_BUILD_ACTION_SKIP_RESTORE'
@@ -298,7 +297,7 @@ abstract class AbstractEntryExtractor {
}
private getCacheMetadataFile(): string {
const actionMetadataDirectory = path.resolve(this.gradleUserHome, META_FILE_DIR)
const actionMetadataDirectory = path.resolve(this.gradleUserHome, ACTION_METADATA_DIR)
fs.mkdirSync(actionMetadataDirectory, {recursive: true})
return path.resolve(actionMetadataDirectory, `${this.extractorName}-entry-metadata.json`)
@@ -449,7 +448,7 @@ export class ConfigurationCacheEntryExtractor extends AbstractEntryExtractor {
}
private getConfigCacheDirectoriesWithAssociatedBuildResults(): Record<string, BuildResult[]> {
return loadBuildResults().reduce(
return loadBuildResults().results.reduce(
(acc, buildResult) => {
// For each build result, find the config-cache dir
const configCachePath = path.resolve(buildResult.rootProjectDir, '.gradle/configuration-cache')

View File

@@ -7,14 +7,12 @@ import fs from 'fs'
import {generateCacheKey} from './cache-key'
import {CacheListener} from './cache-reporting'
import {saveCache, restoreCache, cacheDebug, isCacheDebuggingEnabled, tryDelete} from './cache-utils'
import {CacheConfig} from '../configuration'
import {CacheConfig, ACTION_METADATA_DIR} from '../configuration'
import {GradleHomeEntryExtractor, ConfigurationCacheEntryExtractor} from './gradle-home-extry-extractor'
import {getPredefinedToolchains, mergeToolchainContent, readResourceFileAsString} from './gradle-user-home-utils'
const RESTORED_CACHE_KEY_KEY = 'restored-cache-key'
export const META_FILE_DIR = '.setup-gradle'
export class GradleUserHomeCache {
private readonly cacheName = 'home'
private readonly cacheDescription = 'Gradle User Home'
@@ -86,6 +84,7 @@ export class GradleUserHomeCache {
await this.debugReportGradleUserHomeSize('as restored from cache')
await new GradleHomeEntryExtractor(this.gradleUserHome, this.cacheConfig).restore(listener)
await new ConfigurationCacheEntryExtractor(this.gradleUserHome, this.cacheConfig).restore(listener)
await this.deleteExcludedPaths()
await this.debugReportGradleUserHomeSize('after restoring common artifacts')
}
@@ -171,7 +170,7 @@ export class GradleUserHomeCache {
*/
protected getCachePath(): string[] {
const rawPaths: string[] = this.cacheConfig.getCacheIncludes()
rawPaths.push(META_FILE_DIR)
rawPaths.push(ACTION_METADATA_DIR)
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
cacheDebug(`Using cache paths: ${resolvedPaths}`)
return resolvedPaths
@@ -187,7 +186,7 @@ export class GradleUserHomeCache {
private initializeGradleUserHome(): void {
// Create a directory for storing action metadata
const actionCacheDir = path.resolve(this.gradleUserHome, META_FILE_DIR)
const actionCacheDir = path.resolve(this.gradleUserHome, ACTION_METADATA_DIR)
fs.mkdirSync(actionCacheDir, {recursive: true})
this.copyInitScripts()

View File

@@ -4,11 +4,12 @@ import * as cache from '@actions/cache'
import * as deprecator from './deprecation-collector'
import {SUMMARY_ENV_VAR} from '@actions/core/lib/summary'
import {parseArgsStringToArgv} from 'string-argv'
import path from 'path'
const ACTION_ID_VAR = 'GRADLE_ACTION_ID'
export const ACTION_METADATA_DIR = '.setup-gradle'
export class DependencyGraphConfig {
getDependencyGraphOption(): DependencyGraphOption {
const val = core.getInput('dependency-graph')
@@ -23,11 +24,9 @@ export class DependencyGraphConfig {
return DependencyGraphOption.GenerateAndUpload
case 'download-and-submit':
return DependencyGraphOption.DownloadAndSubmit
case 'clear':
return DependencyGraphOption.Clear
}
throw TypeError(
`The value '${val}' is not valid for 'dependency-graph'. Valid values are: [disabled, generate, generate-and-submit, generate-and-upload, download-and-submit, clear]. The default value is 'disabled'.`
`The value '${val}' is not valid for 'dependency-graph'. Valid values are: [disabled, generate, generate-and-submit, generate-and-upload, download-and-submit]. The default value is 'disabled'.`
)
}
@@ -46,7 +45,28 @@ export class DependencyGraphConfig {
}
getReportDirectory(): string {
return path.resolve(getWorkspaceDirectory(), 'dependency-graph-reports')
const param = core.getInput('dependency-graph-report-dir')
return path.resolve(getWorkspaceDirectory(), param)
}
getDownloadArtifactName(): string | undefined {
return process.env['DEPENDENCY_GRAPH_DOWNLOAD_ARTIFACT_NAME']
}
getExcludeProjects(): string | undefined {
return getOptionalInput('dependency-graph-exclude-projects')
}
getIncludeProjects(): string | undefined {
return getOptionalInput('dependency-graph-include-projects')
}
getExcludeConfigurations(): string | undefined {
return getOptionalInput('dependency-graph-exclude-configurations')
}
getIncludeConfigurations(): string | undefined {
return getOptionalInput('dependency-graph-include-configurations')
}
static constructJobCorrelator(workflow: string, jobId: string, matrixJson: string): string {
@@ -77,8 +97,7 @@ export enum DependencyGraphOption {
Generate = 'generate',
GenerateAndSubmit = 'generate-and-submit',
GenerateAndUpload = 'generate-and-upload',
DownloadAndSubmit = 'download-and-submit',
Clear = 'clear'
DownloadAndSubmit = 'download-and-submit'
}
export class CacheConfig {
@@ -107,7 +126,45 @@ export class CacheConfig {
}
isCacheCleanupEnabled(): boolean {
return getBooleanInput('gradle-home-cache-cleanup') && !this.isCacheReadOnly()
if (this.isCacheReadOnly()) {
return false
}
const cleanupOption = this.getCacheCleanupOption()
return cleanupOption === CacheCleanupOption.Always || cleanupOption === CacheCleanupOption.OnSuccess
}
shouldPerformCacheCleanup(hasFailure: boolean): boolean {
const cleanupOption = this.getCacheCleanupOption()
if (cleanupOption === CacheCleanupOption.Always) {
return true
}
if (cleanupOption === CacheCleanupOption.OnSuccess) {
return !hasFailure
}
return false
}
private getCacheCleanupOption(): CacheCleanupOption {
const legacyVal = getOptionalBooleanInput('gradle-home-cache-cleanup')
if (legacyVal !== undefined) {
deprecator.recordDeprecation(
'The `gradle-home-cache-cleanup` input parameter has been replaced by `cache-cleanup`'
)
return legacyVal ? CacheCleanupOption.Always : CacheCleanupOption.Never
}
const val = core.getInput('cache-cleanup')
switch (val.toLowerCase().trim()) {
case 'always':
return CacheCleanupOption.Always
case 'on-success':
return CacheCleanupOption.OnSuccess
case 'never':
return CacheCleanupOption.Never
}
throw TypeError(
`The value '${val}' is not valid for cache-cleanup. Valid values are: [never, always, on-success].`
)
}
getCacheEncryptionKey(): string {
@@ -123,6 +180,12 @@ export class CacheConfig {
}
}
export enum CacheCleanupOption {
Never = 'never',
OnSuccess = 'on-success',
Always = 'always'
}
export class SummaryConfig {
shouldGenerateJobSummary(hasFailure: boolean): boolean {
// Check if Job Summary is supported on this platform
@@ -130,11 +193,6 @@ export class SummaryConfig {
return false
}
// Check if Job Summary is disabled using the deprecated input
if (!this.isJobSummaryEnabled()) {
return false
}
return this.shouldAddJobSummary(this.getJobSummaryOption(), hasFailure)
}
@@ -153,10 +211,6 @@ export class SummaryConfig {
}
}
private isJobSummaryEnabled(): boolean {
return getBooleanInput('generate-job-summary', true)
}
private getJobSummaryOption(): JobSummaryOption {
return this.parseJobSummaryOption('add-job-summary')
}
@@ -196,11 +250,11 @@ export class BuildScanConfig {
}
getBuildScanTermsOfUseUrl(): string {
return this.getTermsOfUseProp('build-scan-terms-of-use-url', 'build-scan-terms-of-service-url')
return core.getInput('build-scan-terms-of-use-url')
}
getBuildScanTermsOfUseAgree(): string {
return this.getTermsOfUseProp('build-scan-terms-of-use-agree', 'build-scan-terms-of-service-agree')
return core.getInput('build-scan-terms-of-use-agree')
}
getDevelocityAccessKey(): string {
@@ -269,22 +323,6 @@ export class BuildScanConfig {
}
return true
}
/**
* TODO @bigdaz: remove support for the deprecated input property in the next major release of the action
*/
private getTermsOfUseProp(newPropName: string, oldPropName: string): string {
const newProp = core.getInput(newPropName)
if (newProp !== '') {
return newProp
}
const oldProp = core.getInput(oldPropName)
if (oldProp !== '') {
deprecator.recordDeprecation('The `build-scan-terms-of-service` input parameters have been renamed')
return oldProp
}
return core.getInput(oldPropName)
}
}
export class GradleExecutionConfig {
@@ -302,16 +340,6 @@ export class GradleExecutionConfig {
return resolvedBuildRootDirectory
}
getArguments(): string[] {
const input = core.getInput('arguments')
if (input.length !== 0) {
deprecator.recordDeprecation(
'Using the action to execute Gradle via the `arguments` parameter is deprecated'
)
}
return parseArgsStringToArgv(input)
}
getDependencyResolutionTask(): string {
return core.getInput('dependency-resolution-task') || ':ForceDependencyResolutionPlugin_resolveAllDependencies'
}
@@ -319,10 +347,26 @@ export class GradleExecutionConfig {
getAdditionalArguments(): string {
return core.getInput('additional-arguments')
}
verifyNoArguments(): void {
const input = core.getInput('arguments')
if (input.length !== 0) {
deprecator.failOnUseOfRemovedFeature(
`The 'arguments' parameter is no longer supported for ${getActionId()}`,
'Using the action to execute Gradle via the `arguments` parameter is deprecated'
)
}
}
}
export function doValidateWrappers(): boolean {
return getBooleanInput('validate-wrappers')
export class WrapperValidationConfig {
doValidateWrappers(): boolean {
return getBooleanInput('validate-wrappers')
}
allowSnapshotWrappers(): boolean {
return getBooleanInput('allow-snapshot-wrappers')
}
}
// Internal parameters
@@ -357,6 +401,14 @@ export function parseNumericInput(paramName: string, paramValue: string, paramDe
return numericValue
}
function getOptionalInput(paramName: string): string | undefined {
const paramValue = core.getInput(paramName)
if (paramValue.length > 0) {
return paramValue
}
return undefined
}
function getBooleanInput(paramName: string, paramDefault = false): boolean {
const paramValue = core.getInput(paramName)
switch (paramValue.toLowerCase().trim()) {

View File

@@ -2,19 +2,16 @@ import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as path from 'path'
import {BuildResult} from './build-results'
import {BuildResults} from './build-results'
export class DaemonController {
private readonly gradleHomes
constructor(buildResults: BuildResult[]) {
const allHomes = buildResults.map(buildResult => buildResult.gradleHomeDir)
this.gradleHomes = Array.from(new Set(allHomes))
constructor(buildResults: BuildResults) {
this.gradleHomes = buildResults.uniqueGradleHomes()
}
async stopAllDaemons(): Promise<void> {
core.info('Stopping all Gradle daemons before saving Gradle User Home state')
const executions: Promise<number>[] = []
const args = ['--stop']

View File

@@ -31,22 +31,23 @@ export async function setup(config: DependencyGraphConfig): Promise<void> {
core.exportVariable('GITHUB_DEPENDENCY_GRAPH_ENABLED', 'true')
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_CONTINUE_ON_FAILURE', config.getDependencyGraphContinueOnFailure())
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR', config.getJobCorrelator())
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_ID', github.context.runId)
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_ID', github.context.runId.toString())
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_REF', github.context.ref)
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_SHA', getShaFromContext())
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_WORKSPACE', getWorkspaceDirectory())
maybeExportVariable('DEPENDENCY_GRAPH_REPORT_DIR', config.getReportDirectory())
// To clear the dependency graph, we generate an empty graph by excluding all projects and configurations
if (option === DependencyGraphOption.Clear) {
core.exportVariable('DEPENDENCY_GRAPH_INCLUDE_PROJECTS', '')
core.exportVariable('DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS', '')
}
maybeExportVariable('DEPENDENCY_GRAPH_EXCLUDE_PROJECTS', config.getExcludeProjects())
maybeExportVariable('DEPENDENCY_GRAPH_INCLUDE_PROJECTS', config.getIncludeProjects())
maybeExportVariable('DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS', config.getExcludeConfigurations())
maybeExportVariable('DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS', config.getIncludeConfigurations())
}
function maybeExportVariable(variableName: string, value: unknown): void {
function maybeExportVariable(variableName: string, value: string | boolean | undefined): void {
if (!process.env[variableName]) {
core.exportVariable(variableName, value)
if (value !== undefined) {
core.exportVariable(variableName, value)
}
}
}
@@ -59,7 +60,6 @@ export async function complete(config: DependencyGraphConfig): Promise<void> {
case DependencyGraphOption.DownloadAndSubmit: // Performed in setup
return
case DependencyGraphOption.GenerateAndSubmit:
case DependencyGraphOption.Clear: // Submit the empty dependency graph
await findAndSubmitDependencyGraphs(config)
return
case DependencyGraphOption.GenerateAndUpload:
@@ -77,7 +77,7 @@ async function downloadAndSubmitDependencyGraphs(config: DependencyGraphConfig):
}
try {
await submitDependencyGraphs(await downloadDependencyGraphs())
await submitDependencyGraphs(await downloadDependencyGraphs(config))
} catch (e) {
warnOrFail(config, DependencyGraphOption.DownloadAndSubmit, e)
}
@@ -111,7 +111,7 @@ async function findAndUploadDependencyGraphs(config: DependencyGraphConfig): Pro
await uploadDependencyGraphs(await findDependencyGraphFiles(), config)
}
async function downloadDependencyGraphs(): Promise<string[]> {
async function downloadDependencyGraphs(config: DependencyGraphConfig): Promise<string[]> {
const findBy = github.context.payload.workflow_run
? {
token: getGithubToken(),
@@ -123,13 +123,19 @@ async function downloadDependencyGraphs(): Promise<string[]> {
const artifactClient = new DefaultArtifactClient()
const dependencyGraphArtifacts = (
let dependencyGraphArtifacts = (
await artifactClient.listArtifacts({
latest: true,
findBy
})
).artifacts.filter(artifact => artifact.name.startsWith(DEPENDENCY_GRAPH_PREFIX))
const artifactName = config.getDownloadArtifactName()
if (artifactName) {
core.info(`Filtering for artifacts ending with ${artifactName}`)
dependencyGraphArtifacts = dependencyGraphArtifacts.filter(artifact => artifact.name.includes(artifactName))
}
for (const artifact of dependencyGraphArtifacts) {
const downloadedArtifact = await artifactClient.downloadArtifact(artifact.id, {
findBy

View File

@@ -3,6 +3,7 @@ import {getActionId} from './configuration'
const DEPRECATION_UPGRADE_PAGE = 'https://github.com/gradle/actions/blob/main/docs/deprecation-upgrade-guide.md'
const recordedDeprecations: Deprecation[] = []
const recordedErrors: string[] = []
export class Deprecation {
constructor(readonly message: string) {}
@@ -22,10 +23,21 @@ export function recordDeprecation(message: string): void {
}
}
export function failOnUseOfRemovedFeature(removalMessage: string, deprecationMessage: string = removalMessage): void {
const deprecation = new Deprecation(deprecationMessage)
const errorMessage = `${removalMessage}.\nSee ${deprecation.getDocumentationLink()}`
recordedErrors.push(errorMessage)
core.setFailed(errorMessage)
}
export function getDeprecations(): Deprecation[] {
return recordedDeprecations
}
export function getErrors(): string[] {
return recordedErrors
}
export function emitDeprecationWarnings(hasJobSummary = true): void {
if (recordedDeprecations.length > 0) {
core.warning(
@@ -38,17 +50,21 @@ export function emitDeprecationWarnings(hasJobSummary = true): void {
}
export function saveDeprecationState(): void {
core.saveState('deprecations', JSON.stringify(recordedDeprecations))
core.saveState('deprecation-collector_deprecations', JSON.stringify(recordedDeprecations))
core.saveState('deprecation-collector_errors', JSON.stringify(recordedErrors))
}
export function restoreDeprecationState(): void {
const stringRep = core.getState('deprecations')
if (stringRep === '') {
return
const savedDeprecations = core.getState('deprecation-collector_deprecations')
if (savedDeprecations) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
JSON.parse(savedDeprecations).forEach((obj: any) => {
recordedDeprecations.push(new Deprecation(obj.message))
})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
JSON.parse(stringRep).forEach((obj: any) => {
recordedDeprecations.push(new Deprecation(obj.message))
})
const savedErrors = core.getState('deprecation-collector_errors')
if (savedErrors) {
recordedErrors.push(...JSON.parse(savedErrors))
}
}

View File

@@ -7,7 +7,7 @@ export async function setup(config: BuildScanConfig): Promise<void> {
maybeExportVariable('DEVELOCITY_AUTO_INJECTION_CUSTOM_VALUE', 'gradle-actions')
if (config.getBuildScanPublishEnabled()) {
maybeExportVariable('DEVELOCITY_INJECTION_ENABLED', 'true')
maybeExportVariable('DEVELOCITY_PLUGIN_VERSION', '3.17.5')
maybeExportVariable('DEVELOCITY_PLUGIN_VERSION', '3.17.6')
maybeExportVariable('DEVELOCITY_CCUD_PLUGIN_VERSION', '2.0')
maybeExportVariable('DEVELOCITY_TERMS_OF_USE_URL', config.getBuildScanTermsOfUseUrl())
maybeExportVariable('DEVELOCITY_TERMS_OF_USE_AGREE', config.getBuildScanTermsOfUseAgree())

View File

@@ -1,9 +1,11 @@
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import which from 'which'
import * as httpm from '@actions/http-client'
import * as core from '@actions/core'
import * as cache from '@actions/cache'
import * as exec from '@actions/exec'
import * as toolCache from '@actions/tool-cache'
import * as gradlew from './gradlew'
@@ -95,12 +97,18 @@ async function findGradleVersionDeclaration(version: string): Promise<GradleVers
async function installGradleVersion(versionInfo: GradleVersionInfo): Promise<string> {
return core.group(`Provision Gradle ${versionInfo.version}`, async () => {
const preInstalledGradle = await findGradleVersionOnPath(versionInfo)
if (preInstalledGradle !== undefined) {
core.info(`Gradle version ${versionInfo.version} is already available on PATH. Not installing.`)
return preInstalledGradle
}
return locateGradleAndDownloadIfRequired(versionInfo)
})
}
async function locateGradleAndDownloadIfRequired(versionInfo: GradleVersionInfo): Promise<string> {
const installsDir = path.join(os.homedir(), 'gradle-installations/installs')
const installsDir = path.join(getProvisionDir(), 'installs')
const installDir = path.join(installsDir, `gradle-${versionInfo.version}`)
if (fs.existsSync(installDir)) {
core.info(`Gradle installation already exists at ${installDir}`)
@@ -119,7 +127,7 @@ async function locateGradleAndDownloadIfRequired(versionInfo: GradleVersionInfo)
}
async function downloadAndCacheGradleDistribution(versionInfo: GradleVersionInfo): Promise<string> {
const downloadPath = path.join(os.homedir(), `gradle-installations/downloads/gradle-${versionInfo.version}-bin.zip`)
const downloadPath = path.join(getProvisionDir(), `downloads/gradle-${versionInfo.version}-bin.zip`)
// TODO: Convert this to a class and inject config
const cacheConfig = new CacheConfig()
@@ -152,6 +160,11 @@ async function downloadAndCacheGradleDistribution(versionInfo: GradleVersionInfo
return downloadPath
}
function getProvisionDir(): string {
const tmpDir = process.env['RUNNER_TEMP'] ?? os.tmpdir()
return path.join(tmpDir, `.gradle-actions/gradle-installations`)
}
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})`)
@@ -179,3 +192,15 @@ interface GradleVersionInfo {
version: string
downloadUrl: string
}
async function findGradleVersionOnPath(versionInfo: GradleVersionInfo): Promise<string | undefined> {
const gradleExecutable = await which('gradle', {nothrow: true})
if (gradleExecutable) {
const output = await exec.getExecOutput(gradleExecutable, ['-v'], {silent: true})
if (output.stdout.includes(`Gradle ${versionInfo.version}`)) {
return gradleExecutable
}
}
return undefined
}

View File

@@ -2,18 +2,25 @@ import * as core from '@actions/core'
import * as github from '@actions/github'
import {RequestError} from '@octokit/request-error'
import {BuildResult} from './build-results'
import {BuildResults, BuildResult} from './build-results'
import {SummaryConfig, getActionId, getGithubToken} from './configuration'
import {Deprecation, getDeprecations} from './deprecation-collector'
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
export async function generateJobSummary(
buildResults: BuildResult[],
buildResults: BuildResults,
cachingReport: string,
config: SummaryConfig
): Promise<void> {
const summaryTable = renderSummaryTable(buildResults)
const errors = renderErrors()
if (errors) {
core.summary.addRaw(errors)
await core.summary.write()
return
}
const hasFailure = buildResults.some(result => result.buildFailed)
const summaryTable = renderSummaryTable(buildResults.results)
const hasFailure = buildResults.anyFailed()
if (config.shouldGenerateJobSummary(hasFailure)) {
core.info('Generating Job Summary')
@@ -82,6 +89,14 @@ export function renderSummaryTable(results: BuildResult[]): string {
return `${renderDeprecations()}\n${renderBuildResults(results)}`
}
function renderErrors(): string | undefined {
const errors = getErrors()
if (errors.length === 0) {
return undefined
}
return errors.map(error => `<b>:x: ${error}</b>`).join('\n')
}
function renderDeprecations(): string {
const deprecations = getDeprecations()
if (deprecations.length === 0) {

View File

@@ -1,23 +1,27 @@
import org.gradle.tooling.events.*
import org.gradle.tooling.events.task.*
import org.gradle.internal.operations.*
import org.gradle.initialization.*
import org.gradle.api.internal.tasks.execution.*
import org.gradle.execution.*
import org.gradle.internal.build.event.BuildEventListenerRegistryInternal
import org.gradle.util.GradleVersion
// Can't use settingsEvaluated since this script is applied inside a settingsEvaluated handler
// But projectsEvaluated is good enough, since the build service won't catch configuration failures anyway
projectsEvaluated {
settingsEvaluated { settings ->
def projectTracker = gradle.sharedServices.registerIfAbsent("gradle-action-buildResultsRecorder", BuildResultsRecorder, { spec ->
spec.getParameters().getRootProjectName().set(gradle.rootProject.name)
spec.getParameters().getRootProjectDir().set(gradle.rootProject.rootDir.absolutePath)
spec.getParameters().getRootProjectName().set(settings.rootProject.name)
spec.getParameters().getRootProjectDir().set(settings.rootDir.absolutePath)
spec.getParameters().getRequestedTasks().set(gradle.startParameter.taskNames.join(" "))
spec.getParameters().getGradleHomeDir().set(gradle.gradleHomeDir.absolutePath)
spec.getParameters().getInvocationId().set(gradle.ext.invocationId)
})
gradle.services.get(BuildEventsListenerRegistry).onTaskCompletion(projectTracker)
gradle.services.get(BuildEventListenerRegistryInternal).onOperationCompletion(projectTracker)
}
abstract class BuildResultsRecorder implements BuildService<BuildResultsRecorder.Params>, OperationCompletionListener, AutoCloseable {
abstract class BuildResultsRecorder implements BuildService<BuildResultsRecorder.Params>, BuildOperationListener, AutoCloseable {
private boolean buildFailed = false
private boolean configCacheHit = true
interface Params extends BuildServiceParameters {
Property<String> getRootProjectName()
Property<String> getRootProjectDir()
@@ -26,9 +30,19 @@ abstract class BuildResultsRecorder implements BuildService<BuildResultsRecorder
Property<String> getInvocationId()
}
public void onFinish(FinishEvent finishEvent) {
if (finishEvent instanceof TaskFinishEvent && finishEvent.result instanceof TaskFailureResult) {
buildFailed = true
void started(BuildOperationDescriptor buildOperation, OperationStartEvent startEvent) {}
void progress(OperationIdentifier operationIdentifier, OperationProgressEvent progressEvent) {}
void finished(BuildOperationDescriptor buildOperation, OperationFinishEvent finishEvent) {
if (buildOperation.details in EvaluateSettingsBuildOperationType.Details) {
// Got EVALUATE SETTINGS event: not a config-cache hit"
configCacheHit = false
}
if (buildOperation.details in RunRootBuildWorkBuildOperationType.Details) {
if (finishEvent.failure != null) {
buildFailed = true
}
}
}
@@ -41,8 +55,7 @@ abstract class BuildResultsRecorder implements BuildService<BuildResultsRecorder
gradleVersion: GradleVersion.current().version,
gradleHomeDir: getParameters().getGradleHomeDir().get(),
buildFailed: buildFailed,
buildScanUri: null,
buildScanFailed: false
configCacheHit: configCacheHit
]
def runnerTempDir = System.getProperty("RUNNER_TEMP") ?: System.getenv("RUNNER_TEMP")
@@ -52,7 +65,7 @@ abstract class BuildResultsRecorder implements BuildService<BuildResultsRecorder
}
try {
def buildResultsDir = new File(runnerTempDir, ".build-results")
def buildResultsDir = new File(runnerTempDir, ".gradle-actions/build-results")
buildResultsDir.mkdirs()
def buildResultsFile = new File(buildResultsDir, githubActionStep + getParameters().getInvocationId().get() + ".json")
if (!buildResultsFile.exists()) {

View File

@@ -13,6 +13,7 @@ def GE_EXTENSION = "gradleEnterprise"
// Only run against root build. Do not run against included builds.
def isTopLevelBuild = gradle.getParent() == null
if (isTopLevelBuild) {
def resultsWriter = new ResultsWriter()
def version = GradleVersion.current().baseVersion
def atLeastGradle3 = version >= GradleVersion.version("3.0")
@@ -21,59 +22,77 @@ if (isTopLevelBuild) {
def invocationId = "-${System.currentTimeMillis()}"
if (atLeastGradle6) {
// By default, use standard mechanisms to capture build results
def useBuildService = version >= GradleVersion.version("6.6")
if (useBuildService) {
captureUsingBuildService(invocationId)
} else {
captureUsingBuildFinished(gradle, invocationId, resultsWriter)
}
// Use the Develocity plugin to also capture build scan links, when available
settingsEvaluated { settings ->
// By default, use standard mechanisms to capture build results
if (useBuildService) {
captureUsingBuildService(settings, invocationId)
} else {
captureUsingBuildFinished(gradle, invocationId)
}
settings.pluginManager.withPlugin(GE_PLUGIN_ID) {
// Only execute if develocity plugin isn't applied.
if (!settings.extensions.findByName(DEVELOCITY_EXTENSION)) {
captureUsingBuildScanPublished(settings.extensions[GE_EXTENSION].buildScan, settings.rootProject, invocationId)
captureUsingBuildScanPublished(settings.extensions[GE_EXTENSION].buildScan, invocationId, resultsWriter)
}
}
settings.pluginManager.withPlugin(DEVELOCITY_PLUGIN_ID) {
captureUsingBuildScanPublished(settings.extensions[DEVELOCITY_EXTENSION].buildScan, settings.rootProject, invocationId)
captureUsingBuildScanPublished(settings.extensions[DEVELOCITY_EXTENSION].buildScan, invocationId, resultsWriter)
}
}
} else if (atLeastGradle3) {
projectsEvaluated { gradle ->
// By default, use 'buildFinished' to capture build results
captureUsingBuildFinished(gradle, invocationId)
captureUsingBuildFinished(gradle, invocationId, resultsWriter)
gradle.rootProject.pluginManager.withPlugin(BUILD_SCAN_PLUGIN_ID) {
// Only execute if develocity plugin isn't applied.
if (!gradle.rootProject.extensions.findByName(DEVELOCITY_EXTENSION)) {
captureUsingBuildScanPublished(gradle.rootProject.extensions[BUILD_SCAN_EXTENSION], gradle.rootProject, invocationId)
captureUsingBuildScanPublished(gradle.rootProject.extensions[BUILD_SCAN_EXTENSION], invocationId, resultsWriter)
}
}
gradle.rootProject.pluginManager.withPlugin(DEVELOCITY_PLUGIN_ID) {
captureUsingBuildScanPublished(gradle.rootProject.extensions[DEVELOCITY_EXTENSION].buildScan, gradle.rootProject, invocationId)
captureUsingBuildScanPublished(gradle.rootProject.extensions[DEVELOCITY_EXTENSION].buildScan, invocationId, resultsWriter)
}
}
}
}
def captureUsingBuildService(invocationId) {
gradle.ext.invocationId = invocationId
apply from: 'gradle-actions.build-result-capture-service.plugin.groovy'
}
void captureUsingBuildFinished(gradle, String invocationId, ResultsWriter resultsWriter) {
gradle.buildFinished { result ->
println "Got buildFinished: ${result}"
def buildResults = [
rootProjectName: rootProject.name,
rootProjectDir: rootProject.projectDir.absolutePath,
requestedTasks: gradle.startParameter.taskNames.join(" "),
gradleVersion: GradleVersion.current().version,
gradleHomeDir: gradle.gradleHomeDir.absolutePath,
buildFailed: result.failure != null,
configCacheHit: false
]
resultsWriter.writeToResultsFile("build-results", invocationId, buildResults)
}
}
// The `buildScanPublished` hook allows the capture of the Build Scan URI.
// Results captured this way will overwrite any results from 'buildFinished'.
def captureUsingBuildScanPublished(buildScanExtension, rootProject, invocationId) {
void captureUsingBuildScanPublished(buildScanExtension, String invocationId, ResultsWriter resultsWriter) {
buildScanExtension.with {
def buildResults = new BuildResults(invocationId, gradle, rootProject)
buildFinished { result ->
buildResults.setBuildResult(result)
}
buildScanPublished { buildScan ->
buildResults.setBuildScanUri(buildScan.buildScanUri.toASCIIString())
buildResults.writeToResultsFile(true)
def scanResults = [
buildScanUri: buildScan.buildScanUri.toASCIIString(),
buildScanFailed: false
]
resultsWriter.writeToResultsFile("build-scans", invocationId, scanResults)
def githubOutput = System.getenv("GITHUB_OUTPUT")
if (githubOutput) {
@@ -85,63 +104,17 @@ def captureUsingBuildScanPublished(buildScanExtension, rootProject, invocationId
}
onError { error ->
buildResults.setBuildScanFailed()
buildResults.writeToResultsFile(true)
def scanResults = [
buildScanUri: null,
buildScanFailed: true
]
resultsWriter.writeToResultsFile("build-scans", invocationId, scanResults)
}
}
}
def captureUsingBuildFinished(gradle, invocationId) {
gradle.buildFinished { result ->
println "Got buildFinished: ${result}"
def buildResults = new BuildResults(invocationId, gradle, gradle.rootProject)
buildResults.setBuildResult(result)
buildResults.writeToResultsFile(false)
}
}
def captureUsingBuildService(settings, invocationId) {
gradle.ext.invocationId = invocationId
apply from: 'gradle-actions.build-result-capture-service.plugin.groovy'
}
class BuildResults {
def invocationId
def buildResults
BuildResults(String invocationId, def gradle, def rootProject) {
this.invocationId = invocationId
buildResults = [
rootProjectName: rootProject.name,
rootProjectDir: rootProject.projectDir.absolutePath,
requestedTasks: gradle.startParameter.taskNames.join(" "),
gradleVersion: GradleVersion.current().version,
gradleHomeDir: gradle.gradleHomeDir.absolutePath,
buildFailed: false,
buildScanUri: null,
buildScanFailed: false
]
}
def setBuildResult(def result) {
try {
// Gradle and old Build Scan/Gradle Enterprise plugins report a single optional failure in the build result
buildResults['buildFailed'] = result.failure != null
} catch (Exception e) {
// Develocity plugin unwraps all build failures and reports them as a mandatory array
buildResults['buildFailed'] = !result.failures.empty
}
}
def setBuildScanUri(def buildScanUrl) {
buildResults['buildScanUri'] = buildScanUrl
}
def setBuildScanFailed() {
buildResults['buildScanFailed'] = true
}
def writeToResultsFile(boolean overwrite) {
class ResultsWriter {
void writeToResultsFile(String subDir, String invocationId, def content) {
def runnerTempDir = System.getProperty("RUNNER_TEMP") ?: System.getenv("RUNNER_TEMP")
def githubActionStep = System.getProperty("GITHUB_ACTION") ?: System.getenv("GITHUB_ACTION")
if (!runnerTempDir || !githubActionStep) {
@@ -149,19 +122,12 @@ class BuildResults {
}
try {
def buildResultsDir = new File(runnerTempDir, ".build-results")
def buildResultsDir = new File(runnerTempDir, ".gradle-actions/${subDir}")
buildResultsDir.mkdirs()
def buildResultsFile = new File(buildResultsDir, githubActionStep + invocationId + ".json")
// Overwrite any contents written by buildFinished or build service, since this result is a superset.
if (buildResultsFile.exists()) {
if (overwrite) {
buildResultsFile.text = groovy.json.JsonOutput.toJson(buildResults)
}
} else {
buildResultsFile << groovy.json.JsonOutput.toJson(buildResults)
if (!buildResultsFile.exists()) {
buildResultsFile << groovy.json.JsonOutput.toJson(content)
}
} catch (Exception e) {
println "\ngradle action failed to write build-results file. Will continue.\n> ${e.getLocalizedMessage()}"
}

View File

@@ -1,5 +1,6 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as path from 'path'
import * as os from 'os'
import * as caches from './caching/caches'
@@ -9,16 +10,25 @@ import * as buildScan from './develocity/build-scan'
import {loadBuildResults, markBuildResultsProcessed} from './build-results'
import {CacheListener, generateCachingReport} from './caching/cache-reporting'
import {DaemonController} from './daemon-controller'
import {BuildScanConfig, CacheConfig, SummaryConfig, getWorkspaceDirectory} from './configuration'
import {findInvalidWrapperJars} from './wrapper-validation/validate'
import {JobFailure} from './errors'
import {
BuildScanConfig,
CacheConfig,
SummaryConfig,
WrapperValidationConfig,
getWorkspaceDirectory
} from './configuration'
import * as wrapperValidator from './wrapper-validation/wrapper-validator'
const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED'
const USER_HOME = 'USER_HOME'
const GRADLE_USER_HOME = 'GRADLE_USER_HOME'
const CACHE_LISTENER = 'CACHE_LISTENER'
export async function setup(cacheConfig: CacheConfig, buildScanConfig: BuildScanConfig): Promise<boolean> {
export async function setup(
cacheConfig: CacheConfig,
buildScanConfig: BuildScanConfig,
wrapperValidationConfig: WrapperValidationConfig
): Promise<boolean> {
const userHome = await determineUserHome()
const gradleUserHome = await determineGradleUserHome()
@@ -41,6 +51,8 @@ export async function setup(cacheConfig: CacheConfig, buildScanConfig: BuildScan
core.saveState(CACHE_LISTENER, cacheListener.stringify())
await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome)
await buildScan.setup(buildScanConfig)
return true
@@ -60,7 +72,7 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER))
const daemonController = new DaemonController(buildResults)
await caches.save(userHome, gradleUserHome, cacheListener, daemonController, cacheConfig)
await caches.save(userHome, gradleUserHome, cacheListener, daemonController, buildResults, cacheConfig)
const cachingReport = generateCachingReport(cacheListener)
await jobSummary.generateJobSummary(buildResults, cachingReport, summaryConfig)
@@ -79,7 +91,24 @@ async function determineGradleUserHome(): Promise<string> {
return path.resolve(rootDir, customGradleUserHome)
}
return path.resolve(await determineUserHome(), '.gradle')
const defaultGradleUserHome = path.resolve(await determineUserHome(), '.gradle')
// Use the default Gradle User Home if it already exists
if (fs.existsSync(defaultGradleUserHome)) {
core.info(`Gradle User Home already exists at ${defaultGradleUserHome}`)
core.exportVariable('GRADLE_USER_HOME', defaultGradleUserHome)
return defaultGradleUserHome
}
// Switch Gradle User Home to faster 'D:' drive if possible
if (os.platform() === 'win32' && defaultGradleUserHome.startsWith('C:\\') && fs.existsSync('D:\\a\\')) {
const fasterGradleUserHome = 'D:\\a\\.gradle'
core.info(`Setting GRADLE_USER_HOME to ${fasterGradleUserHome} to leverage (potentially) faster drive.`)
core.exportVariable('GRADLE_USER_HOME', fasterGradleUserHome)
return fasterGradleUserHome
}
core.exportVariable('GRADLE_USER_HOME', defaultGradleUserHome)
return defaultGradleUserHome
}
/**
@@ -98,16 +127,3 @@ async function determineUserHome(): Promise<string> {
core.debug(`Determined user.home from java -version output: '${userHome}'`)
return userHome
}
export async function checkNoInvalidWrapperJars(rootDir = getWorkspaceDirectory()): Promise<void> {
const allowedChecksums = process.env['ALLOWED_GRADLE_WRAPPER_CHECKSUMS']?.split(',') || []
const result = await findInvalidWrapperJars(rootDir, 1, false, allowedChecksums)
if (result.isValid()) {
core.info(result.toDisplayString())
} else {
core.info(result.toDisplayString())
throw new JobFailure(
`Gradle Wrapper Validation Failed!\n See https://github.com/gradle/actions/blob/main/docs/wrapper-validation.md#reporting-failures\n${result.toDisplayString()}`
)
}
}

View File

@@ -0,0 +1,26 @@
import fs from 'fs'
import path from 'path'
import {ACTION_METADATA_DIR} from '../configuration'
export class ChecksumCache {
private readonly cacheFile: string
constructor(gradleUserHome: string) {
this.cacheFile = path.resolve(gradleUserHome, ACTION_METADATA_DIR, 'valid-wrappers.json')
}
load(): string[] {
// Load previously validated checksums saved in Gradle User Home
if (fs.existsSync(this.cacheFile)) {
return JSON.parse(fs.readFileSync(this.cacheFile, 'utf-8'))
}
return []
}
save(checksums: string[]): void {
const uniqueChecksums = [...new Set(checksums)]
// Save validated checksums to Gradle User Home
fs.mkdirSync(path.dirname(this.cacheFile), {recursive: true})
fs.writeFileSync(this.cacheFile, JSON.stringify(uniqueChecksums))
}
}

View File

@@ -1,24 +1,31 @@
import * as httpm from 'typed-rest-client/HttpClient'
import * as cheerio from 'cheerio'
import fileWrapperChecksums from './wrapper-checksums.json'
const httpc = new httpm.HttpClient('gradle/wrapper-validation-action', undefined, {allowRetries: true, maxRetries: 3})
function getKnownValidChecksums(): Map<string, Set<string>> {
const versionsMap = new Map<string, Set<string>>()
for (const entry of fileWrapperChecksums) {
const checksum = entry.checksum
export class WrapperChecksums {
checksums = new Map<string, Set<string>>()
versions = new Set<string>()
let versionNames = versionsMap.get(checksum)
if (versionNames === undefined) {
versionNames = new Set()
versionsMap.set(checksum, versionNames)
add(version: string, checksum: string): void {
if (this.checksums.has(checksum)) {
this.checksums.get(checksum)!.add(version)
} else {
this.checksums.set(checksum, new Set([version]))
}
versionNames.add(entry.version)
this.versions.add(version)
}
}
return versionsMap
function loadKnownChecksums(): WrapperChecksums {
const checksums = new WrapperChecksums()
for (const entry of fileWrapperChecksums) {
checksums.add(entry.version, entry.checksum)
}
return checksums
}
/**
@@ -26,9 +33,12 @@ function getKnownValidChecksums(): Map<string, Set<string>> {
*
* Maps from the checksum to the names of the Gradle versions whose wrapper has this checksum.
*/
export const KNOWN_VALID_CHECKSUMS = getKnownValidChecksums()
export const KNOWN_CHECKSUMS = loadKnownChecksums()
export async function fetchValidChecksums(allowSnapshots: boolean): Promise<Set<string>> {
export async function fetchUnknownChecksums(
allowSnapshots: boolean,
knownChecksums: WrapperChecksums
): Promise<Set<string>> {
const all = await httpGetJsonArray('https://services.gradle.org/versions/all')
const withChecksum = all.filter(
entry => typeof entry === 'object' && entry != null && entry.hasOwnProperty('wrapperChecksumUrl')
@@ -37,11 +47,23 @@ export async function fetchValidChecksums(allowSnapshots: boolean): Promise<Set<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(entry: any) => allowSnapshots || !entry.snapshot
)
const checksumUrls = allowed.map(
const notKnown = allowed.filter(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(entry: any) => !knownChecksums.versions.has(entry.version)
)
const checksumUrls = notKnown.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(entry: any) => entry.wrapperChecksumUrl as string
)
const checksums = await Promise.all(checksumUrls.map(async (url: string) => httpGetText(url)))
if (allowSnapshots) {
await addDistributionSnapshotChecksums(checksumUrls)
}
const checksums = await Promise.all(
checksumUrls.map(async (url: string) => {
// console.log(`Fetching checksum from ${url}`)
return httpGetText(url)
})
)
return new Set(checksums)
}
@@ -53,3 +75,22 @@ async function httpGetText(url: string): Promise<string> {
const response = await httpc.get(url)
return await response.readBody()
}
// Public for testing
export async function addDistributionSnapshotChecksums(checksumUrls: string[]): Promise<void> {
// Load the index page of the distribution snapshot repository
const indexPage = await httpGetText('https://services.gradle.org/distributions-snapshots/')
// // Extract all wrapper checksum from the index page. These end in -wrapper.jar.sha256
// // Load the HTML into cheerio
const $ = cheerio.load(indexPage)
// // Find all links ending with '-wrapper.jar.sha256'
const wrapperChecksumLinks = $('a[href$="-wrapper.jar.sha256"]')
// build the absolute URL for each wrapper checksum
wrapperChecksumLinks.each((index, element) => {
const url = $(element).attr('href')
checksumUrls.push(`https://services.gradle.org${url}`)
})
}

View File

@@ -8,7 +8,8 @@ export async function findInvalidWrapperJars(
minWrapperCount: number,
allowSnapshots: boolean,
allowedChecksums: string[],
knownValidChecksums: Map<string, Set<string>> = checksums.KNOWN_VALID_CHECKSUMS
previouslyValidatedChecksums: string[] = [],
knownValidChecksums: checksums.WrapperChecksums = checksums.KNOWN_CHECKSUMS
): Promise<ValidationResult> {
const wrapperJars = await find.findWrapperJars(gitRepoRoot)
const result = new ValidationResult([], [])
@@ -21,7 +22,11 @@ export async function findInvalidWrapperJars(
const notYetValidatedWrappers = []
for (const wrapperJar of wrapperJars) {
const sha = await hash.sha256File(resolve(gitRepoRoot, wrapperJar))
if (allowedChecksums.includes(sha) || knownValidChecksums.has(sha)) {
if (
allowedChecksums.includes(sha) ||
previouslyValidatedChecksums.includes(sha) ||
knownValidChecksums.checksums.has(sha)
) {
result.valid.push(new WrapperJar(wrapperJar, sha))
} else {
notYetValidatedWrappers.push(new WrapperJar(wrapperJar, sha))
@@ -31,7 +36,7 @@ export async function findInvalidWrapperJars(
// Otherwise fall back to fetching checksums from Gradle API and compare against them
if (notYetValidatedWrappers.length > 0) {
result.fetchedChecksums = true
const fetchedValidChecksums = await checksums.fetchValidChecksums(allowSnapshots)
const fetchedValidChecksums = await checksums.fetchUnknownChecksums(allowSnapshots, knownValidChecksums)
for (const wrapperJar of notYetValidatedWrappers) {
if (!fetchedValidChecksums.has(wrapperJar.checksum)) {

View File

@@ -0,0 +1,41 @@
import * as core from '@actions/core'
import {WrapperValidationConfig} from '../configuration'
import {ChecksumCache} from './cache'
import {findInvalidWrapperJars} from './validate'
import {JobFailure} from '../errors'
export async function validateWrappers(
config: WrapperValidationConfig,
workspaceRoot: string,
gradleUserHome: string
): Promise<void> {
if (!config.doValidateWrappers()) {
return // Wrapper validation is disabled
}
const checksumCache = new ChecksumCache(gradleUserHome)
const allowedChecksums = process.env['ALLOWED_GRADLE_WRAPPER_CHECKSUMS']?.split(',') || []
const previouslyValidatedChecksums = checksumCache.load()
const result = await findInvalidWrapperJars(
workspaceRoot,
0,
config.allowSnapshotWrappers(),
allowedChecksums,
previouslyValidatedChecksums
)
if (result.isValid()) {
await core.group('All Gradle Wrapper jars are valid', async () => {
core.info(`Loaded previously validated checksums from cache: ${previouslyValidatedChecksums.join(', ')}`)
core.info(result.toDisplayString())
})
} else {
core.info(result.toDisplayString())
throw new JobFailure(
`Gradle Wrapper Validation Failed!\n See https://github.com/gradle/actions/blob/main/docs/wrapper-validation.md#validation-failures\n${result.toDisplayString()}`
)
}
checksumCache.save(result.valid.map(wrapper => wrapper.checksum))
}

View File

@@ -1,5 +1,5 @@
plugins {
id "com.gradle.develocity" version "3.17.5"
id "com.gradle.develocity" version "3.17.6"
id "com.gradle.common-custom-user-data-gradle-plugin" version "2.0.1"
}

View File

@@ -16,7 +16,7 @@ import java.nio.file.Files
import java.util.zip.GZIPOutputStream
class BaseInitScriptTest extends Specification {
static final String DEVELOCITY_PLUGIN_VERSION = '3.17.5'
static final String DEVELOCITY_PLUGIN_VERSION = '3.17.6'
static final String CCUD_PLUGIN_VERSION = '2.0.1'
static final TestGradleVersion GRADLE_3_X = new TestGradleVersion(GradleVersion.version('3.5.1'), 7, 9)

View File

@@ -1,7 +1,6 @@
package com.gradle.gradlebuildaction
import groovy.json.JsonSlurper
import org.gradle.util.GradleVersion
import static org.junit.Assume.assumeTrue
@@ -15,7 +14,7 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
run(testGradleVersion.gradleVersion)
then:
assertResults('help', testGradleVersion, false, false)
assertResults('help', testGradleVersion, false)
where:
testGradleVersion << ALL_VERSIONS
@@ -29,7 +28,7 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
runAndFail(testGradleVersion.gradleVersion)
then:
assertResults('expectFailure', testGradleVersion, true, false)
assertResults('expectFailure', testGradleVersion, true)
where:
testGradleVersion << ALL_VERSIONS
@@ -49,7 +48,7 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
run(['help', '--configuration-cache'], testGradleVersion.gradleVersion)
then:
assertResults('help', testGradleVersion, false, false)
assertResults('help', testGradleVersion, false, true)
where:
testGradleVersion << CONFIGURATION_CACHE_VERSIONS
@@ -63,7 +62,8 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
run(testGradleVersion.gradleVersion)
then:
assertResults('help', testGradleVersion, false, true)
assertResults('help', testGradleVersion, false)
assertScanResults()
where:
testGradleVersion << ALL_VERSIONS
@@ -77,7 +77,8 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
run(testGradleVersion.gradleVersion)
then:
assertResults('help', testGradleVersion, false, true)
assertResults('help', testGradleVersion, false)
assertScanResults()
where:
testGradleVersion << ALL_VERSIONS
@@ -91,7 +92,7 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
run(['help', '--no-scan'], testGradleVersion.gradleVersion)
then:
assertResults('help', testGradleVersion, false, false)
assertResults('help', testGradleVersion, false)
where:
testGradleVersion << ALL_VERSIONS
@@ -106,7 +107,8 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
runAndFail(testGradleVersion.gradleVersion)
then:
assertResults('expectFailure', testGradleVersion, true, true)
assertResults('expectFailure', testGradleVersion, true)
assertScanResults()
where:
testGradleVersion << ALL_VERSIONS
@@ -120,14 +122,17 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
run(['help', '--configuration-cache'], testGradleVersion.gradleVersion)
then:
assertResults('help', testGradleVersion, false, true)
assertResults('help', testGradleVersion, false, false)
assertScanResults()
assert buildResultFile.delete()
assert scanResultFile.delete()
when:
run(['help', '--configuration-cache'], testGradleVersion.gradleVersion)
then:
assertResults('help', testGradleVersion, false, true)
assertScanResults()
where:
testGradleVersion << CONFIGURATION_CACHE_VERSIONS
@@ -143,7 +148,8 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
runAndFail(testGradleVersion.gradleVersion)
then:
assertResults('expectFailure', testGradleVersion, true, false, true)
assertResults('expectFailure', testGradleVersion, true)
assertScanResults(true)
where:
testGradleVersion << ALL_VERSIONS
@@ -190,7 +196,7 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
when:
settingsFile.text = """
plugins {
id 'com.gradle.develocity' version '3.17.5' apply(false)
id 'com.gradle.develocity' version '3.17.6' apply(false)
}
gradle.settingsEvaluated {
apply plugin: 'com.gradle.develocity'
@@ -204,7 +210,8 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
run(testGradleVersion.gradleVersion)
then:
assertResults('help', testGradleVersion, false, true)
assertResults('help', testGradleVersion, false)
assertScanResults()
where:
testGradleVersion << SETTINGS_PLUGIN_VERSIONS
@@ -232,7 +239,7 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
]
}
void assertResults(String task, TestGradleVersion testGradleVersion, boolean hasFailure, boolean hasBuildScan, boolean scanUploadFailed = false) {
void assertResults(String task, TestGradleVersion testGradleVersion, boolean hasFailure, boolean configCacheHit = false) {
def results = new JsonSlurper().parse(buildResultFile)
assert results['rootProjectName'] == ROOT_PROJECT_NAME
assert results['rootProjectDir'] == testProjectDir.canonicalPath
@@ -240,12 +247,17 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
assert results['gradleVersion'] == testGradleVersion.gradleVersion.version
assert results['gradleHomeDir'] != null
assert results['buildFailed'] == hasFailure
assert results['buildScanUri'] == (hasBuildScan ? "${mockScansServer.address}s/${PUBLIC_BUILD_SCAN_ID}" : null)
assert results['buildScanFailed'] == scanUploadFailed
assert results['configCacheHit'] == configCacheHit
}
void assertScanResults(boolean scanUploadFailed = false) {
def scanResults = new JsonSlurper().parse(scanResultFile)
assert scanResults['buildScanUri'] == (scanUploadFailed ? null : "${mockScansServer.address}s/${PUBLIC_BUILD_SCAN_ID}")
assert scanResults['buildScanFailed'] == scanUploadFailed
}
private File getBuildResultFile() {
def buildResultsDir = new File(testProjectDir, '.build-results')
def buildResultsDir = new File(testProjectDir, '.gradle-actions/build-results')
assert buildResultsDir.directory
assert buildResultsDir.listFiles().size() == 1
def resultsFile = buildResultsDir.listFiles()[0]
@@ -253,4 +265,14 @@ class TestBuildResultRecorder extends BaseInitScriptTest {
assert resultsFile.text.count('rootProjectName') == 1
return resultsFile
}
private File getScanResultFile() {
def buildResultsDir = new File(testProjectDir, '.gradle-actions/build-scans')
assert buildResultsDir.directory
assert buildResultsDir.listFiles().size() == 1
def resultsFile = buildResultsDir.listFiles()[0]
assert resultsFile.name.startsWith('github-step-id')
assert resultsFile.text.count('buildScanUri') == 1
return resultsFile
}
}

View File

@@ -10,6 +10,7 @@ const successfulHelpBuild: BuildResult = {
gradleVersion: '8.0',
gradleHomeDir: '/opt/gradle',
buildFailed: false,
configCacheHit: false,
buildScanUri: 'https://scans.gradle.com/s/abc123',
buildScanFailed: false
}

View File

@@ -6,22 +6,22 @@ jest.setTimeout(30000)
test('has loaded hardcoded wrapper jars checksums', async () => {
// Sanity check that generated checksums file is not empty and was properly imported
expect(checksums.KNOWN_VALID_CHECKSUMS.size).toBeGreaterThan(10)
expect(checksums.KNOWN_CHECKSUMS.checksums.size).toBeGreaterThan(10)
// Verify that checksums of arbitrary versions are contained
expect(
checksums.KNOWN_VALID_CHECKSUMS.get(
checksums.KNOWN_CHECKSUMS.checksums.get(
'660ab018b8e319e9ae779fdb1b7ac47d0321bde953bf0eb4545f14952cfdcaa3'
)
).toEqual(new Set(['4.10.3']))
expect(
checksums.KNOWN_VALID_CHECKSUMS.get(
checksums.KNOWN_CHECKSUMS.checksums.get(
'28b330c20a9a73881dfe9702df78d4d78bf72368e8906c70080ab6932462fe9e'
)
).toEqual(new Set(['6.0-rc-1', '6.0-rc-2', '6.0-rc-3', '6.0', '6.0.1']))
})
test('fetches wrapper jars checksums', async () => {
const validChecksums = await checksums.fetchValidChecksums(false)
const validChecksums = await checksums.fetchUnknownChecksums(false, new checksums.WrapperChecksums)
expect(validChecksums.size).toBeGreaterThan(10)
// Verify that checksum of arbitrary version is contained
expect(
@@ -32,6 +32,22 @@ test('fetches wrapper jars checksums', async () => {
).toBe(true)
})
test('fetches wrapper jar checksums for snapshots', async () => {
const nonSnapshotChecksums = await checksums.fetchUnknownChecksums(false, new checksums.WrapperChecksums)
const validChecksums = await checksums.fetchUnknownChecksums(true, new checksums.WrapperChecksums)
// Expect that at least one snapshot checksum is different from the non-snapshot checksums
expect(validChecksums.size).toBeGreaterThan(nonSnapshotChecksums.size)
})
test('fetches all wrapper checksum URLS for snapshots', async () => {
const checksumUrls: string[] = []
await checksums.addDistributionSnapshotChecksums(checksumUrls)
expect(checksumUrls.length).toBeGreaterThan(100) // May only be a few unique checksums
console.log(checksumUrls)
})
describe('retry', () => {
afterEach(() => {
nock.cleanAll()
@@ -47,7 +63,7 @@ describe('retry', () => {
code: 'ECONNREFUSED'
})
const validChecksums = await checksums.fetchValidChecksums(false)
const validChecksums = await checksums.fetchUnknownChecksums(false, new checksums.WrapperChecksums)
expect(validChecksums.size).toBeGreaterThan(10)
nock.isDone()
})

View File

@@ -1,10 +1,15 @@
import * as path from 'path'
import * as fs from 'fs'
import * as validate from '../../../src/wrapper-validation/validate'
import {expect, test, jest} from '@jest/globals'
import { WrapperChecksums } from '../../../src/wrapper-validation/checksums'
import { ChecksumCache } from '../../../src/wrapper-validation/cache'
import exp from 'constants'
jest.setTimeout(30000)
const baseDir = path.resolve('./test/jest/wrapper-validation')
const tmpDir = path.resolve('./test/jest/tmp')
test('succeeds if all found wrapper jars are valid', async () => {
const result = await validate.findInvalidWrapperJars(baseDir, 3, false, [
@@ -23,15 +28,35 @@ test('succeeds if all found wrapper jars are valid', async () => {
)
})
test('succeeds if all found wrapper jars are previously valid', async () => {
const result = await validate.findInvalidWrapperJars(baseDir, 3, false, [], [
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
'3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce'
])
expect(result.isValid()).toBe(true)
// Only hardcoded and explicitly allowed checksums should have been used
expect(result.fetchedChecksums).toBe(false)
expect(result.toDisplayString()).toBe(
'✓ Found known Gradle Wrapper JAR files:\n' +
' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradle-wrapper.jar\n' +
' e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 data/invalid/gradlе-wrapper.jar\n' + // homoglyph
' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar'
)
})
test('succeeds if all found wrapper jars are valid (and checksums are fetched from Gradle API)', async () => {
const knownValidChecksums = new Map<string, Set<string>>()
const knownValidChecksums = new WrapperChecksums()
const result = await validate.findInvalidWrapperJars(
baseDir,
1,
false,
['e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'],
[],
knownValidChecksums
)
console.log(`fetchedChecksums = ${result.fetchedChecksums}`)
expect(result.isValid()).toBe(true)
// Should have fetched checksums because no known checksums were provided
@@ -96,3 +121,17 @@ test('fails if not enough wrapper jars are found', async () => {
' 3888c76faa032ea8394b8a54e04ce2227ab1f4be64f65d450f8509fe112d38ce data/valid/gradle-wrapper.jar'
)
})
test('can save and load checksums', async () => {
const cacheDir = path.join(tmpDir, 'wrapper-validation-cache')
fs.rmSync(cacheDir, {recursive: true, force: true})
const checksumCache = new ChecksumCache(cacheDir)
expect(checksumCache.load()).toEqual([])
checksumCache.save(['123', '456'])
expect(checksumCache.load()).toEqual(['123', '456'])
expect(fs.existsSync(cacheDir)).toBe(true)
})