Compare commits

..

No commits in common. "main" and "v5.0.2" have entirely different histories.
main ... v5.0.2

106 changed files with 5454 additions and 2638 deletions

View File

@ -3,7 +3,7 @@ name: 'Build and upload distribution'
runs: runs:
using: "composite" using: "composite"
steps: steps:
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: 24 node-version: 24
cache: npm cache: npm
@ -23,7 +23,7 @@ runs:
cp -r sources/dist . cp -r sources/dist .
- name: Upload distribution - name: Upload distribution
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: dist name: dist
path: dist/ path: dist/

View File

@ -23,7 +23,7 @@ runs:
# Downloads a 'dist' directory artifact that was uploaded in an earlier 'build-dist' step # Downloads a 'dist' directory artifact that was uploaded in an earlier 'build-dist' step
- name: Download dist - name: Download dist
if: ${{ env.SKIP_DIST != 'true' && !env.ACT }} if: ${{ env.SKIP_DIST != 'true' && !env.ACT }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: dist name: dist
path: dist/ path: dist/

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.

View File

@ -19,14 +19,14 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: 20 node-version: 20
cache: npm cache: npm
cache-dependency-path: sources/package-lock.json cache-dependency-path: sources/package-lock.json
- name: Setup Gradle - name: Setup Gradle
# Use a released version to avoid breakages # Use a released version to avoid breakages
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
env: env:
ALLOWED_GRADLE_WRAPPER_CHECKSUMS: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # Invalid wrapper jar used for testing ALLOWED_GRADLE_WRAPPER_CHECKSUMS: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # Invalid wrapper jar used for testing
with: with:

View File

@ -21,7 +21,7 @@ jobs:
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
with: with:
files: | files: |
dist/** dist/**

View File

@ -35,7 +35,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5 uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
config: | config: |
@ -43,4 +43,4 @@ jobs:
- sources/src - sources/src
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5 uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5

View File

@ -30,7 +30,7 @@ jobs:
java-version: 17 java-version: 17
- name: Setup Gradle - name: Setup Gradle
# Use a released version to avoid breakages # Use a released version to avoid breakages
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
env: env:
ALLOWED_GRADLE_WRAPPER_CHECKSUMS: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # Invalid wrapper jar used for testing ALLOWED_GRADLE_WRAPPER_CHECKSUMS: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # Invalid wrapper jar used for testing
- name: Run integration tests - name: Run integration tests

View File

@ -5,10 +5,12 @@ on:
push: push:
branches: branches:
- 'main' - 'main'
- 'bot/gradle-actions-caching'
paths: paths:
- 'dist/**' - 'dist/**'
- 'sources/vendor/gradle-actions-caching/**'
concurrency:
group: integ-test
cancel-in-progress: false
permissions: permissions:
contents: read contents: read

View File

@ -11,6 +11,10 @@ on:
paths-ignore: paths-ignore:
- 'dist/**' - 'dist/**'
concurrency:
group: integ-test
cancel-in-progress: false
permissions: permissions:
contents: read contents: read
@ -27,9 +31,6 @@ jobs:
caching-integ-tests: caching-integ-tests:
needs: build-distribution needs: build-distribution
uses: ./.github/workflows/suite-integ-test-caching.yml uses: ./.github/workflows/suite-integ-test-caching.yml
concurrency:
group: CI-integ-test
cancel-in-progress: false
with: with:
skip-dist: false skip-dist: false
secrets: inherit secrets: inherit
@ -39,9 +40,6 @@ jobs:
contents: write contents: write
needs: build-distribution needs: build-distribution
uses: ./.github/workflows/suite-integ-test-other.yml uses: ./.github/workflows/suite-integ-test-other.yml
concurrency:
group: CI-integ-test
cancel-in-progress: false
with: with:
skip-dist: false skip-dist: false
secrets: inherit secrets: inherit

View File

@ -44,8 +44,14 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: 'Upload artifact' - name: 'Upload artifact'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif
retention-days: 5 retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: 'Upload to code-scanning'
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5
with:
sarif_file: results.sarif

View File

@ -28,7 +28,7 @@ jobs:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: 20 node-version: 20
cache: npm cache: npm
@ -61,7 +61,7 @@ jobs:
cp -r sources/dist . cp -r sources/dist .
- name: Import GPG key to sign commits - name: Import GPG key to sign commits
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
with: with:
gpg_private_key: ${{ secrets.GH_BOT_PGP_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GH_BOT_PGP_PRIVATE_KEY }}
passphrase: ${{ secrets.GH_BOT_PGP_PASSPHRASE }} passphrase: ${{ secrets.GH_BOT_PGP_PASSPHRASE }}

View File

@ -1,24 +0,0 @@
name: ci-validate-typings.yml
on:
push:
branches:
- 'main'
- 'release/**'
paths-ignore:
- 'dist/**'
pull_request:
permissions:
contents: read
jobs:
validate-typings:
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: typesafegithub/github-actions-typing@9ddf35b71a482be7d8922b28e8d00df16b77e315 # v2.2.2
with:
ignored-action-files: |
.github/actions/build-dist/action.yml
.github/actions/init-integ-test/action.yml
action.yml

View File

@ -12,6 +12,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 - uses: gradle/actions/wrapper-validation@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5.0.1
with: with:
allow-checksums: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 allow-checksums: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

View File

@ -114,55 +114,3 @@ jobs:
- name: Build kotlin-dsl project - name: Build kotlin-dsl project
working-directory: .github/workflow-samples/kotlin-dsl working-directory: .github/workflow-samples/kotlin-dsl
run: ./gradlew assemble run: ./gradlew assemble
cache-disabled:
needs: build-distribution
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-disabled: true
- name: Build kotlin-dsl project
working-directory: .github/workflow-samples/kotlin-dsl
run: ./gradlew assemble
terms-of-use-accepted:
needs: build-distribution
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
build-scan-terms-of-use-url: https://gradle.com/help/legal-terms-of-use
build-scan-terms-of-use-agree: yes
- name: Build kotlin-dsl project
working-directory: .github/workflow-samples/kotlin-dsl
run: ./gradlew assemble
develocity-access-key-set:
needs: build-distribution
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
develocity-access-key: ${{ secrets.DV_SOLUTIONS_ACCESS_KEY }}
- name: Build kotlin-dsl project
working-directory: .github/workflow-samples/kotlin-dsl
run: ./gradlew assemble

View File

@ -41,7 +41,7 @@ jobs:
with: with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches cache-read-only: false # For testing, allow writing cache entries on non-default branches
- name: Build with 3.1 - name: Build with 3.1
working-directory: .github/workflow-samples/cache-cleanup working-directory: sources/test/jest/resources/cache-cleanup
run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1" build run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1" build
# Second build will use the cache from the first build, but cleanup should remove unused artifacts # Second build will use the cache from the first build, but cleanup should remove unused artifacts
@ -67,7 +67,7 @@ jobs:
cache-read-only: false cache-read-only: false
cache-cleanup: 'on-success' cache-cleanup: 'on-success'
- name: Build with 3.1.1 - name: Build with 3.1.1
working-directory: .github/workflow-samples/cache-cleanup working-directory: sources/test/jest/resources/cache-cleanup
run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1.1" build run: ./gradlew --no-daemon --build-cache -Dcommons_math3_version="3.1.1" build
# Third build will restore cache entry from second, and verify stale content removed # Third build will restore cache entry from second, and verify stale content removed

View File

@ -178,7 +178,7 @@ jobs:
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- name: Download dependency-graph artifact - name: Download dependency-graph artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
path: downloaded-dependency-graphs path: downloaded-dependency-graphs
pattern: dependency-graph_*dependency-graph-generate-submit-and-upload.json pattern: dependency-graph_*dependency-graph-generate-submit-and-upload.json

View File

@ -0,0 +1,235 @@
name: Test restore configuration-cache
on:
workflow_call:
inputs:
cache-key-prefix:
type: string
default: '0'
runner-os:
type: string
default: '["ubuntu-latest"]'
skip-dist:
type: boolean
default: false
secrets:
GRADLE_ENCRYPTION_KEY:
required: true
env:
SKIP_DIST: ${{ inputs.skip-dist }}
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: restore-configuration-cache-${{ inputs.cache-key-prefix }}
permissions:
contents: read
jobs:
restore-cc-seed-build-groovy:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-groovy
strategy:
max-parallel: 1
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
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 }}
- name: Groovy build with configuration-cache enabled
working-directory: .github/workflow-samples/groovy-dsl
run: gradle test --configuration-cache
restore-cc-verify-build-groovy:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-groovy
GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION: ${{github.sha}}_1
needs: restore-cc-seed-build-groovy
strategy:
max-parallel: 1
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-read-only: false
cache-cleanup: on-success
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- 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
restore-cc-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: restore-cc-verify-build-groovy
strategy:
max-parallel: 1
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-read-only: true
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- 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
# Check that the build can run when no extracted cache entries are restored
restore-cc-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: restore-cc-seed-build-groovy
strategy:
max-parallel: 1
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle with no extracted cache entries restored
uses: ./setup-gradle
env:
GRADLE_BUILD_ACTION_SKIP_RESTORE: "generated-gradle-jars|wrapper-zips|java-toolchains|instrumented-jars|dependencies|kotlin-dsl"
with:
cache-read-only: true
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Check execute Gradle build with configuration cache enabled (but not restored)
working-directory: .github/workflow-samples/groovy-dsl
run: gradle test --configuration-cache
restore-cc-seed-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-kotlin
strategy:
max-parallel: 1
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
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 }}
- name: Execute 'help' with configuration-cache enabled
working-directory: .github/workflow-samples/kotlin-dsl
run: gradle help --configuration-cache
restore-cc-modify-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-kotlin
GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION: ${{github.sha}}_1
needs: restore-cc-seed-build-kotlin
strategy:
max-parallel: 1
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Execute 'test' with configuration-cache enabled
working-directory: .github/workflow-samples/kotlin-dsl
run: gradle test --configuration-cache
# Test restore configuration-cache from the third build invocation
restore-cc-verify-build-kotlin:
env:
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: restore-cc-kotlin
GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION: ${{github.sha}}_2
needs: restore-cc-modify-build-kotlin
strategy:
max-parallel: 1
fail-fast: false
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Gradle
uses: ./setup-gradle
with:
cache-read-only: true
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Execute 'test' again with configuration-cache enabled
id: execute
working-directory: .github/workflow-samples/kotlin-dsl
run: gradle test --configuration-cache
- 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

@ -111,12 +111,6 @@ jobs:
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github
dist
wrapper-validation
- name: Initialize integ-test - name: Initialize integ-test
uses: ./.github/actions/init-integ-test uses: ./.github/actions/init-integ-test
@ -124,8 +118,10 @@ jobs:
id: action-test id: action-test
uses: ./wrapper-validation uses: ./wrapper-validation
with: with:
min-wrapper-count: 7 # to allow the invalid wrapper jar present in test data
# There are only 6 wrappers in workflow-samples, so expected to fail allow-checksums: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
min-wrapper-count: 11
# Expected to fail; validated below
continue-on-error: true continue-on-error: true
- name: Check outcome - name: Check outcome

View File

@ -26,6 +26,14 @@ jobs:
runner-os: '${{ inputs.runner-os }}' runner-os: '${{ inputs.runner-os }}'
skip-dist: ${{ inputs.skip-dist }} skip-dist: ${{ inputs.skip-dist }}
restore-configuration-cache:
if: ${{ ! github.event.pull_request.head.repo.fork }}
uses: ./.github/workflows/integ-test-restore-configuration-cache.yml
with:
skip-dist: ${{ inputs.skip-dist }}
secrets:
GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
restore-containerized-gradle-home: restore-containerized-gradle-home:
uses: ./.github/workflows/integ-test-restore-containerized-gradle-home.yml uses: ./.github/workflows/integ-test-restore-containerized-gradle-home.yml
with: with:

View File

@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: 20 node-version: 20
cache: npm cache: npm
@ -38,7 +38,7 @@ jobs:
working-directory: sources working-directory: sources
- name: Import GPG key to sign commits - name: Import GPG key to sign commits
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0 uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
with: with:
gpg_private_key: ${{ secrets.GH_BOT_PGP_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GH_BOT_PGP_PRIVATE_KEY }}
passphrase: ${{ secrets.GH_BOT_PGP_PASSPHRASE }} passphrase: ${{ secrets.GH_BOT_PGP_PASSPHRASE }}

View File

@ -1,28 +0,0 @@
# Workspace Instructions
## Vendored Library Flow
This repository uses a proprietary caching library: `gradle-actions-caching`.
- The vendored copy lives at `sources/vendor/gradle-actions-caching`
- The source code is at `../actions-caching` and https://github.com/gradle/actions-caching
When a task involves building, updating, validating, or testing the vendored `gradle-actions-caching` library, use this sequence:
1. Run `npm run build` in `actions-caching`.
2. Copy (overwrite) the contents of `actions-caching/dist/` onto `sources/vendor/gradle-actions-caching/`. (No need to rm the existing contents)
3. Then continue with any build, test, or validation steps in this repository.
Do not treat `actions/sources/vendor/gradle-actions-caching` as the source of truth. The source of truth is `actions-caching`, and the vendor directory must be refreshed from its `dist/` output after rebuilding.
## Building
To build this repository, run the `build` script at the root of that repository with no arguments:
```sh
./build
```
## dist directory
Never make direct changes to the 'dist' directory. Building with npm will populate 'sources/dist' which is enough. There is a CI workflow that will update the 'dist' directory when required.

11
NOTICE
View File

@ -1,11 +0,0 @@
NOTICE
The software in this repository, except for the bundled `gradle-actions-caching` component, is licensed under the MIT License.
The caching functionality in this project has been extracted into `gradle-actions-caching`, a proprietary commercial component that is not covered by the MIT License for this repository.
The bundled `gradle-actions-caching` component is licensed and governed by a separate license, available at https://gradle.com/legal/terms-of-use/.
The `gradle-actions-caching` component is used only when caching is enabled and is not loaded or used when caching is disabled.
Use of the `gradle-actions-caching` component is subject to a separate license, available at https://gradle.com/legal/terms-of-use/.
If you do not agree to these license terms, do not use the gradle-actions-caching component.

View File

@ -1,23 +1,9 @@
# GitHub Actions for Gradle builds # GitHub Actions for Gradle builds
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/gradle/actions/badge)](https://scorecard.dev/viewer/?uri=github.com/gradle/actions)
This repository contains a set of GitHub Actions that are useful for building Gradle projects on GitHub. This repository contains a set of GitHub Actions that are useful for building Gradle projects on GitHub.
> [!IMPORTANT]
> ## Licensing notice
>
> The software in this repository is licensed under the [MIT License](LICENSE).
>
> The caching functionality in this project has been extracted into `gradle-actions-caching`, a proprietary commercial component that is not covered by the MIT License for this repository.
> The bundled `gradle-actions-caching` component is licensed and governed by a separate license, available at https://gradle.com/legal/terms-of-use/.
>
> The `gradle-actions-caching` component is used only when caching is enabled and is not loaded or used when caching is disabled.
>
> Use of the `gradle-actions-caching` component is subject to a separate license, available at https://gradle.com/legal/terms-of-use/.
> If you do not agree to these license terms, do not use the `gradle-actions-caching` component.
This license notice will be displayed in workflow logs and each job summary. To suppress this message,
either [accept the terms of use](docs/setup-gradle.md#publishing-to-scansgradlecom) in your workflow, or [provide a Develocity access key](docs/setup-gradle.md#managing-develocity-access-keys).
## The `setup-gradle` action ## The `setup-gradle` action
The `setup-gradle` action can be used to configure Gradle for optimal execution on any platform supported by GitHub Actions. The `setup-gradle` action can be used to configure Gradle for optimal execution on any platform supported by GitHub Actions.
@ -39,9 +25,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: 17 java-version: 17
@ -77,9 +63,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: 17 java-version: 17
@ -112,7 +98,7 @@ jobs:
name: "Validation" name: "Validation"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: gradle/actions/wrapper-validation@v5 - uses: gradle/actions/wrapper-validation@v5
``` ```

View File

@ -20,9 +20,8 @@
- Include a Full changelog link in the format https://github.com/gradle/actions/compare/v2.12.0...v3.0.0 - Include a Full changelog link in the format https://github.com/gradle/actions/compare/v2.12.0...v3.0.0
- Publish the release. - Publish the release.
- Force push the `v5` tag (or current major version) to point to the new release. It is conventional for users to bind to a major release version using this tag. - Force push the `v5` tag (or current major version) to point to the new release. It is conventional for users to bind to a major release version using this tag.
- From CLI: `git tag -f -s -a -m "v5.0.0" v5 && git push -f --tags` - From CLI: `git tag -f -s -a -m "v5.0.0" v5 v5.0.0 && git push -f --tags`
- Note that we sign the tag and set the commit message for the tag to the newly released version. - Note that we sign the tag and set the commit message for the tag to the newly released version.
- Your HEAD must point at the commit to be tagged.
## Post release steps ## Post release steps

View File

@ -22,9 +22,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: 17 java-version: 17

View File

@ -1,146 +0,0 @@
# Type descriptors based on https://github.com/typesafegithub/github-actions-typing
inputs:
# Gradle execution configuration
gradle-version:
type: string
build-root-directory:
type: string
dependency-resolution-task:
type: string
additional-arguments:
type: string
# Cache configuration
cache-disabled:
type: boolean
cache-read-only:
type: boolean
cache-write-only:
type: boolean
cache-overwrite-existing:
type: boolean
cache-encryption-key:
type: string
cache-cleanup:
type: enum
allowed-values:
- never
- on-success
- always
gradle-home-cache-cleanup:
type: boolean
gradle-home-cache-includes:
type: list
separator: '\n'
list-item:
type: string
gradle-home-cache-excludes:
type: list
separator: '\n'
list-item:
type: string
# Job summary configuration
add-job-summary:
type: enum
allowed-values:
- never
- always
- on-failure
add-job-summary-as-pr-comment:
type: enum
allowed-values:
- never
- always
- on-failure
# Dependency Graph configuration
dependency-graph:
type: enum
allowed-values:
- generate-and-submit
- generate-submit-and-upload
- generate-and-upload
- download-and-submit
dependency-graph-report-dir:
type: string
dependency-graph-continue-on-failure:
type: boolean
dependency-graph-exclude-projects:
type: string
dependency-graph-include-projects:
type: string
dependency-graph-exclude-configurations:
type: string
dependency-graph-include-configurations:
type: string
artifact-retention-days:
type: integer
# Build Scan configuration
build-scan-publish:
type: boolean
build-scan-terms-of-use-url:
type: enum
allowed-values:
- https://gradle.com/terms-of-service
- https://gradle.com/help/legal-terms-of-use
build-scan-terms-of-use-agree:
type: enum
allowed-values:
- 'yes'
develocity-access-key:
type: string
develocity-token-expiry:
type: integer
# Wrapper validation configuration
validate-wrappers:
type: boolean
allow-snapshot-wrappers:
type: boolean
# Experimental action inputs
gradle-home-cache-strict-match:
type: boolean
# Internal action inputs
workflow-job-context:
type: string
github-token:
type: string
outputs:
build-scan-url:
type: string
dependency-graph-file:
type: string
gradle-version:
type: string

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

@ -15,22 +15,6 @@ for vulnerable dependencies, as well as to populate the
If you're confused by the behaviour you're seeing or have specific questions, please check out [the FAQ](dependency-submission-faq.md) before raising an issue. If you're confused by the behaviour you're seeing or have specific questions, please check out [the FAQ](dependency-submission-faq.md) before raising an issue.
> [!IMPORTANT]
> ## Licensing notice
>
> The software in this repository is licensed under the [MIT License](LICENSE).
>
> The caching functionality in this project has been extracted into `gradle-actions-caching`, a proprietary commercial component that is not covered by the MIT License for this repository.
> The bundled `gradle-actions-caching` component is licensed and governed by a separate license, available at https://gradle.com/legal/terms-of-use/.
>
> The `gradle-actions-caching` component is used only when caching is enabled and is not loaded or used when caching is disabled.
>
> Use of the `gradle-actions-caching` component is subject to a separate license, available at https://gradle.com/legal/terms-of-use/.
> If you do not agree to these license terms, do not use the `gradle-actions-caching` component.
This license notice will be displayed in workflow logs and each job summary. To suppress this message,
either [accept the terms of use](setup-gradle.md#publishing-to-scansgradlecom) in your workflow, or [provide a Develocity access key](setup-gradle.md#managing-develocity-access-keys).
## General usage ## General usage
The following workflow will generate a dependency graph for a Gradle project and submit it immediately to the repository via the The following workflow will generate a dependency graph for a Gradle project and submit it immediately to the repository via the
@ -52,8 +36,8 @@ jobs:
dependency-submission: dependency-submission:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -76,13 +60,6 @@ on the command-line will be used.
The action provides the ability to override the Gradle version and task to execute, as well as provide The action provides the ability to override the Gradle version and task to execute, as well as provide
additional arguments that will be passed to Gradle on the command-line. See [Configuration Parameters](#configuration-parameters) below. additional arguments that will be passed to Gradle on the command-line. See [Configuration Parameters](#configuration-parameters) below.
### Disabling caching
Caching is enabled by default. You can disable caching for the action as follows:
```yaml
cache-disabled: true
```
### Publishing a Develocity Build Scan® from your dependency submission workflow ### Publishing a Develocity Build Scan® from your dependency submission workflow
You can automatically publish a free Develocity Build Scan on every run of `gradle/actions/dependency-submission`. You can automatically publish a free Develocity Build Scan on every run of `gradle/actions/dependency-submission`.
@ -366,8 +343,8 @@ jobs:
dependency-submission: dependency-submission:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -428,8 +405,8 @@ jobs:
dependency-submission: dependency-submission:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -466,7 +443,7 @@ jobs:
# Gradle version compatibility # Gradle version compatibility
Dependency-graph generation is compatible with most versions of Gradle >= `5.2`, and is tested regularly against Dependency-graph generation is compatible with most versions of Gradle >= `5.2`, and is tested regularly against
Gradle versions `5.2.1`, `5.6.4`, `6.0.1`, `6.9.4`, `7.1.1`, `7.6.6`, `8.0.2` and `8.14.4`, as well as all patched versions of Gradle 9.x. Gradle versions `5.2.1`, `5.6.4`, `6.0.1`, `6.9.4`, `7.1.1` and `7.6.3`, as well as all patched versions of Gradle 8.x.
A known exception to this is that Gradle `7.0`, `7.0.1` and `7.0.2` are not supported. A known exception to this is that Gradle `7.0`, `7.0.1` and `7.0.2` are not supported.
@ -477,3 +454,4 @@ See [here](https://github.com/gradle/github-dependency-graph-gradle-plugin?tab=r
- Dependency Submission Demo repository: https://github.com/gradle/github-dependency-submission-demo - 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 - 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 - Webinar - Gradle at Scale with GitHub and GitHub Actions at Allegro: https://www.youtube.com/watch?v=gV94I28FPos

View File

@ -2,25 +2,9 @@
This GitHub Action can be used to configure Gradle for optimal execution on any platform supported by GitHub Actions. This GitHub Action can be used to configure Gradle for optimal execution on any platform supported by GitHub Actions.
> [!IMPORTANT]
> ## Licensing notice
>
> The software in this repository is licensed under the [MIT License](LICENSE).
>
> The caching functionality in this project has been extracted into `gradle-actions-caching`, a proprietary commercial component that is not covered by the MIT License for this repository.
> The bundled `gradle-actions-caching` component is licensed and governed by a separate license, available at https://gradle.com/legal/terms-of-use/.
>
> The `gradle-actions-caching` component is used only when caching is enabled and is not loaded or used when caching is disabled.
>
> Use of the `gradle-actions-caching` component is subject to a separate license, available at https://gradle.com/legal/terms-of-use/.
> If you do not agree to these license terms, do not use the `gradle-actions-caching` component.
This license notice will be displayed in workflow logs and each job summary. To suppress this message,
either [accept the terms of use](#publishing-to-scansgradlecom) in your workflow, or [provide a Develocity access key](#managing-develocity-access-keys).
## Why use the `setup-gradle` action? ## Why use the `setup-gradle` action?
It is possible to directly invoke Gradle in your workflow, and the `actions/setup-java@v5` action provides a simple way to cache Gradle dependencies. It is possible to directly invoke Gradle in your workflow, and the `actions/setup-java@v4` action provides a simple way to cache Gradle dependencies.
However, the `setup-gradle` action offers a several advantages over this approach: However, the `setup-gradle` action offers a several advantages over this approach:
@ -54,8 +38,8 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -106,8 +90,8 @@ jobs:
gradle-rc: gradle-rc:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -228,8 +212,8 @@ jobs:
gradle-with-configuration-cache: gradle-with-configuration-cache:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -481,8 +465,8 @@ jobs:
run-gradle-build: run-gradle-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -518,8 +502,8 @@ jobs:
gradle: gradle:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -626,8 +610,8 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -678,8 +662,8 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -708,8 +692,8 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: actions/setup-java@v5 - uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
@ -949,3 +933,4 @@ Each of the plugins is signed by Gradle, and you can simply add the following sn
</trusted-key> </trusted-key>
</trusted-keys> </trusted-keys>
``` ```

View File

@ -72,7 +72,7 @@ jobs:
name: "Validation" name: "Validation"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: gradle/actions/wrapper-validation@v5 - uses: gradle/actions/wrapper-validation@v5
``` ```
@ -120,7 +120,7 @@ restore these Jars on checkout. Without this, only a pointer to the Wrapper Jar
``` ```
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
lfs: true # gradle-wrapper.jar verification will fail without this lfs: true # gradle-wrapper.jar verification will fail without this
``` ```

View File

@ -1,27 +0,0 @@
Software License Agreement Copyright (c) 2026 Gradle, Inc. All rights
reserved.
1. PROPRIETARY AND CONFIDENTIAL. This software and its source code are the
proprietary and confidential property of Gradle, Inc.
2. LICENSE GRANT AND TERMS OF USE. Subject to your compliance with the Terms
of Use referenced below, Gradle, Inc. grants you a non-exclusive,
non-transferable license to use this library solely for internal CI/CD
purposes. Your use, installation, and distribution of this software are
strictly governed by the Gradle, Inc. Terms of Use, which can be found at the
following URL: https://gradle.com/legal/terms-of-use/. By downloading,
installing, or using this software, you agree to be bound by the terms and
conditions set forth in the link above.
3. RESTRICTIONS. Unless expressly permitted in the Terms of Use referenced
above, you may not: (a) Modify, decompile, or reverse engineer this software.
(b) Redistribute the source code or binaries without prior written consent.
(c) Use this software for any purpose not authorized by the Terms of Use.
4. NO WARRANTY. AS SET FORTH IN THE GRADLE, INC. TERMS OF USE, THIS SOFTWARE IS
PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -19,9 +19,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Setup Java - name: Setup Java
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: 17 java-version: 17

View File

@ -1,171 +0,0 @@
# Type descriptors based on https://github.com/typesafegithub/github-actions-typing
inputs:
gradle-version:
type: string
# Cache configuration
cache-disabled:
type: boolean
cache-read-only:
type: boolean
cache-write-only:
type: boolean
cache-overwrite-existing:
type: boolean
cache-encryption-key:
type: string
cache-cleanup:
type: enum
allowed-values:
- never
- on-success
- always
gradle-home-cache-cleanup:
type: boolean
gradle-home-cache-includes:
type: list
separator: '\n'
list-item:
type: string
gradle-home-cache-excludes:
type: list
separator: '\n'
list-item:
type: string
# Job summary configuration
add-job-summary:
type: enum
allowed-values:
- never
- always
- on-failure
add-job-summary-as-pr-comment:
type: enum
allowed-values:
- never
- always
- on-failure
# Dependency Graph configuration
dependency-graph:
type: enum
allowed-values:
- disabled
- generate
- generate-and-submit
- generate-and-upload
- download-and-submit
dependency-graph-report-dir:
type: string
dependency-graph-continue-on-failure:
type: boolean
dependency-graph-exclude-projects:
type: string
dependency-graph-include-projects:
type: string
dependency-graph-exclude-configurations:
type: string
dependency-graph-include-configurations:
type: string
artifact-retention-days:
type: integer
# Build Scan configuration
build-scan-publish:
type: boolean
build-scan-terms-of-use-url:
type: enum
allowed-values:
- https://gradle.com/terms-of-service
- https://gradle.com/help/legal-terms-of-use
build-scan-terms-of-use-agree:
type: enum
allowed-values:
- 'yes'
develocity-access-key:
type: string
develocity-token-expiry:
type: integer
develocity-injection-enabled:
type: boolean
develocity-url:
type: string
develocity-allow-untrusted-server:
type: boolean
develocity-capture-file-fingerprints:
type: boolean
develocity-enforce-url:
type: boolean
develocity-plugin-version:
type: string
develocity-ccud-plugin-version:
type: string
gradle-plugin-repository-url:
type: string
gradle-plugin-repository-username:
type: string
gradle-plugin-repository-password:
type: string
# Wrapper validation configuration
validate-wrappers:
type: boolean
allow-snapshot-wrappers:
type: boolean
# Deprecated action inputs
arguments:
type: string
# Experimental action inputs
gradle-home-cache-strict-match:
type: boolean
# Internal action inputs
workflow-job-context:
type: string
github-token:
type: string
outputs:
build-scan-url:
type: string
dependency-graph-file:
type: string
gradle-version:
type: string

1878
sources/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
"type": "module", "type": "module",
"description": "Execute Gradle Build", "description": "Execute Gradle Build",
"scripts": { "scripts": {
"postinstall": "patch-package",
"prettier-write": "prettier --write 'src/**/*.ts'", "prettier-write": "prettier --write 'src/**/*.ts'",
"prettier-check": "prettier --check 'src/**/*.ts'", "prettier-check": "prettier --check 'src/**/*.ts'",
"lint": "eslint 'src/**/*.ts'", "lint": "eslint 'src/**/*.ts'",
@ -36,7 +37,7 @@
}, },
"dependencies": { "dependencies": {
"@actions/artifact": "6.1.0", "@actions/artifact": "6.1.0",
"@actions/cache": "6.0.0", "@actions/cache": "4.0.5",
"@actions/core": "3.0.0", "@actions/core": "3.0.0",
"@actions/exec": "3.0.0", "@actions/exec": "3.0.0",
"@actions/github": "9.0.0", "@actions/github": "9.0.0",
@ -51,20 +52,21 @@
"which": "6.0.1" "which": "6.0.1"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "30.3.0", "@jest/globals": "30.2.0",
"@types/jest": "30.0.0", "@types/jest": "30.0.0",
"@types/node": "25.5.0", "@types/node": "25.3.0",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@types/unzipper": "0.10.11", "@types/unzipper": "0.10.11",
"@types/which": "3.0.4", "@types/which": "3.0.4",
"@typescript-eslint/eslint-plugin": "8.57.1", "@typescript-eslint/eslint-plugin": "8.56.1",
"dedent": "1.7.2", "dedent": "1.7.1",
"esbuild": "0.27.4", "esbuild": "0.27.3",
"eslint": "10.0.3", "eslint": "10.0.1",
"globals": "17.4.0", "globals": "17.3.0",
"jest": "30.3.0", "jest": "30.2.0",
"nock": "15.0.0", "nock": "15.0.0",
"npm-run-all": "4.1.5", "npm-run-all": "4.1.5",
"patch-package": "8.0.1",
"prettier": "3.8.1", "prettier": "3.8.1",
"ts-jest": "29.4.6", "ts-jest": "29.4.6",
"typescript": "5.9.3" "typescript": "5.9.3"

View File

@ -0,0 +1,248 @@
diff --git a/node_modules/@actions/cache/lib/cache.d.ts b/node_modules/@actions/cache/lib/cache.d.ts
index ef0928b..d06e675 100644
--- a/node_modules/@actions/cache/lib/cache.d.ts
+++ b/node_modules/@actions/cache/lib/cache.d.ts
@@ -21,7 +21,8 @@ export declare function isFeatureAvailable(): boolean;
* @param enableCrossOsArchive an optional boolean enabled to restore on windows any cache created on any platform
* @returns string returns the key for the cache hit, otherwise returns undefined
*/
-export declare function restoreCache(paths: string[], primaryKey: string, restoreKeys?: string[], options?: DownloadOptions, enableCrossOsArchive?: boolean): Promise<string | undefined>;
+export declare function restoreCache(paths: string[], primaryKey: string, restoreKeys?: string[], options?: DownloadOptions, enableCrossOsArchive?: boolean): Promise<CacheEntry | undefined>;
+
/**
* Saves a list of files with the specified key
*
@@ -31,4 +32,12 @@ export declare function restoreCache(paths: string[], primaryKey: string, restor
* @param options cache upload options
* @returns number returns cacheId if the cache was saved successfully and throws an error if save fails
*/
-export declare function saveCache(paths: string[], key: string, options?: UploadOptions, enableCrossOsArchive?: boolean): Promise<number>;
+export declare function saveCache(paths: string[], key: string, options?: UploadOptions, enableCrossOsArchive?: boolean): Promise<CacheEntry>;
+
+// PATCHED: Add `CacheEntry` as return type for save/restore functions
+// This allows us to track and report on cache entry sizes.
+export declare class CacheEntry {
+ key: string;
+ size?: number;
+ constructor(key: string, size?: number);
+}
diff --git a/node_modules/@actions/cache/lib/cache.js b/node_modules/@actions/cache/lib/cache.js
index 41f2a37..2fe1600 100644
--- a/node_modules/@actions/cache/lib/cache.js
+++ b/node_modules/@actions/cache/lib/cache.js
@@ -165,26 +165,29 @@ function restoreCacheV1(paths, primaryKey, restoreKeys, options, enableCrossOsAr
core.info(`Cache Size: ~${Math.round(archiveFileSize / (1024 * 1024))} MB (${archiveFileSize} B)`);
yield (0, tar_1.extractTar)(archivePath, compressionMethod);
core.info('Cache restored successfully');
- return cacheEntry.cacheKey;
- }
- catch (error) {
- const typedError = error;
- if (typedError.name === ValidationError.name) {
- throw error;
- }
- else {
- // warn on cache restore failure and continue build
- // Log server errors (5xx) as errors, all other errors as warnings
- if (typedError instanceof http_client_1.HttpClientError &&
- typeof typedError.statusCode === 'number' &&
- typedError.statusCode >= 500) {
- core.error(`Failed to restore: ${error.message}`);
- }
- else {
- core.warning(`Failed to restore: ${error.message}`);
- }
- }
+
+ // PATCHED - Include size of restored entry
+ return new CacheEntry(cacheEntry.cacheKey, archiveFileSize);
}
+ // PATCHED - propagate errors
+ // catch (error) {
+ // const typedError = error;
+ // if (typedError.name === ValidationError.name) {
+ // throw error;
+ // }
+ // else {
+ // // warn on cache restore failure and continue build
+ // // Log server errors (5xx) as errors, all other errors as warnings
+ // if (typedError instanceof http_client_1.HttpClientError &&
+ // typeof typedError.statusCode === 'number' &&
+ // typedError.statusCode >= 500) {
+ // core.error(`Failed to restore: ${error.message}`);
+ // }
+ // else {
+ // core.warning(`Failed to restore: ${error.message}`);
+ // }
+ // }
+ //}
finally {
// Try to delete the archive to save space
try {
@@ -257,26 +260,29 @@ function restoreCacheV2(paths, primaryKey, restoreKeys, options, enableCrossOsAr
}
yield (0, tar_1.extractTar)(archivePath, compressionMethod);
core.info('Cache restored successfully');
- return response.matchedKey;
- }
- catch (error) {
- const typedError = error;
- if (typedError.name === ValidationError.name) {
- throw error;
- }
- else {
- // Supress all non-validation cache related errors because caching should be optional
- // Log server errors (5xx) as errors, all other errors as warnings
- if (typedError instanceof http_client_1.HttpClientError &&
- typeof typedError.statusCode === 'number' &&
- typedError.statusCode >= 500) {
- core.error(`Failed to restore: ${error.message}`);
- }
- else {
- core.warning(`Failed to restore: ${error.message}`);
- }
- }
+
+ // PATCHED - Include size of restored entry
+ return new CacheEntry(response.matchedKey, archiveFileSize);
}
+ // PATCHED - propagate errors
+ // catch (error) {
+ // const typedError = error;
+ // if (typedError.name === ValidationError.name) {
+ // throw error;
+ // }
+ // else {
+ // // Supress all non-validation cache related errors because caching should be optional
+ // // Log server errors (5xx) as errors, all other errors as warnings
+ // if (typedError instanceof http_client_1.HttpClientError &&
+ // typeof typedError.statusCode === 'number' &&
+ // typedError.statusCode >= 500) {
+ // core.error(`Failed to restore: ${error.message}`);
+ // }
+ // else {
+ // core.warning(`Failed to restore: ${error.message}`);
+ // }
+ // }
+ //}
finally {
try {
if (archivePath) {
@@ -367,27 +373,31 @@ function saveCacheV1(paths, key, options, enableCrossOsArchive = false) {
}
core.debug(`Saving Cache (ID: ${cacheId})`);
yield cacheHttpClient.saveCache(cacheId, archivePath, '', options);
+
+ // PATCHED - Include size of saved entry
+ return new CacheEntry(key, archiveFileSize);
}
- catch (error) {
- const typedError = error;
- if (typedError.name === ValidationError.name) {
- throw error;
- }
- else if (typedError.name === ReserveCacheError.name) {
- core.info(`Failed to save: ${typedError.message}`);
- }
- else {
- // Log server errors (5xx) as errors, all other errors as warnings
- if (typedError instanceof http_client_1.HttpClientError &&
- typeof typedError.statusCode === 'number' &&
- typedError.statusCode >= 500) {
- core.error(`Failed to save: ${typedError.message}`);
- }
- else {
- core.warning(`Failed to save: ${typedError.message}`);
- }
- }
- }
+ // PATCHED - propagate errors
+ //catch (error) {
+ // const typedError = error;
+ // if (typedError.name === ValidationError.name) {
+ // throw error;
+ // }
+ // else if (typedError.name === ReserveCacheError.name) {
+ // core.info(`Failed to save: ${typedError.message}`);
+ // }
+ // else {
+ // // Log server errors (5xx) as errors, all other errors as warnings
+ // if (typedError instanceof http_client_1.HttpClientError &&
+ // typeof typedError.statusCode === 'number' &&
+ // typedError.statusCode >= 500) {
+ // core.error(`Failed to save: ${typedError.message}`);
+ // }
+ // else {
+ // core.warning(`Failed to save: ${typedError.message}`);
+ // }
+ // }
+ //}
finally {
// Try to delete the archive to save space
try {
@@ -471,27 +481,31 @@ function saveCacheV2(paths, key, options, enableCrossOsArchive = false) {
throw new Error(`Unable to finalize cache with key ${key}, another job may be finalizing this cache.`);
}
cacheId = parseInt(finalizeResponse.entryId);
+
+ // PATCHED - Include size of saved entry
+ return new CacheEntry(key, archiveFileSize);
}
- catch (error) {
- const typedError = error;
- if (typedError.name === ValidationError.name) {
- throw error;
- }
- else if (typedError.name === ReserveCacheError.name) {
- core.info(`Failed to save: ${typedError.message}`);
- }
- else {
- // Log server errors (5xx) as errors, all other errors as warnings
- if (typedError instanceof http_client_1.HttpClientError &&
- typeof typedError.statusCode === 'number' &&
- typedError.statusCode >= 500) {
- core.error(`Failed to save: ${typedError.message}`);
- }
- else {
- core.warning(`Failed to save: ${typedError.message}`);
- }
- }
- }
+ // PATCHED - propagate errors
+ //catch (error) {
+ // const typedError = error;
+ // if (typedError.name === ValidationError.name) {
+ // throw error;
+ // }
+ // else if (typedError.name === ReserveCacheError.name) {
+ // core.info(`Failed to save: ${typedError.message}`);
+ // }
+ // else {
+ // // Log server errors (5xx) as errors, all other errors as warnings
+ // if (typedError instanceof http_client_1.HttpClientError &&
+ // typeof typedError.statusCode === 'number' &&
+ // typedError.statusCode >= 500) {
+ // core.error(`Failed to save: ${typedError.message}`);
+ // }
+ // else {
+ // core.warning(`Failed to save: ${typedError.message}`);
+ // }
+ // }
+ //}
finally {
// Try to delete the archive to save space
try {
@@ -504,4 +518,12 @@ function saveCacheV2(paths, key, options, enableCrossOsArchive = false) {
return cacheId;
});
}
+// PATCHED - CacheEntry class
+class CacheEntry {
+ constructor(key, size) {
+ this.key = key;
+ this.size = size;
+ }
+}
+
//# sourceMappingURL=cache.js.map
\ No newline at end of file

View File

@ -5,7 +5,7 @@ import * as dependencyGraph from '../../dependency-graph'
import {parseArgsStringToArgv} from 'string-argv' import {parseArgsStringToArgv} from 'string-argv'
import { import {
DevelocityConfig, BuildScanConfig,
CacheConfig, CacheConfig,
DependencyGraphConfig, DependencyGraphConfig,
DependencyGraphOption, DependencyGraphOption,
@ -15,7 +15,6 @@ import {
} from '../../configuration' } from '../../configuration'
import {saveDeprecationState} from '../../deprecation-collector' import {saveDeprecationState} from '../../deprecation-collector'
import {handleMainActionError} from '../../errors' import {handleMainActionError} from '../../errors'
import {forceExit} from '../../force-exit'
/** /**
* The main entry point for the action, called by Github Actions for the step. * The main entry point for the action, called by Github Actions for the step.
@ -25,7 +24,7 @@ export async function run(): Promise<void> {
setActionId('gradle/actions/dependency-submission') setActionId('gradle/actions/dependency-submission')
// Configure Gradle environment (Gradle User Home) // Configure Gradle environment (Gradle User Home)
await setupGradle.setup(new CacheConfig(), new DevelocityConfig(), new WrapperValidationConfig()) await setupGradle.setup(new CacheConfig(), new BuildScanConfig(), new WrapperValidationConfig())
// Capture the enabled state of dependency-graph // Capture the enabled state of dependency-graph
const originallyEnabled = process.env['GITHUB_DEPENDENCY_GRAPH_ENABLED'] const originallyEnabled = process.env['GITHUB_DEPENDENCY_GRAPH_ENABLED']
@ -68,7 +67,7 @@ export async function run(): Promise<void> {
} }
// Explicit process.exit() to prevent waiting for hanging promises. // Explicit process.exit() to prevent waiting for hanging promises.
await forceExit() process.exit()
} }
run() run()

View File

@ -2,7 +2,6 @@ import * as setupGradle from '../../setup-gradle'
import {CacheConfig, SummaryConfig} from '../../configuration' import {CacheConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors' import {handlePostActionError} from '../../errors'
import {forceExit} from '../../force-exit'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // 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 // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
@ -20,7 +19,7 @@ export async function run(): Promise<void> {
} }
// Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save. // Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save.
await forceExit() process.exit()
} }
run() run()

View File

@ -2,7 +2,7 @@ import * as setupGradle from '../../setup-gradle'
import * as provisioner from '../../execution/provision' import * as provisioner from '../../execution/provision'
import * as dependencyGraph from '../../dependency-graph' import * as dependencyGraph from '../../dependency-graph'
import { import {
DevelocityConfig, BuildScanConfig,
CacheConfig, CacheConfig,
DependencyGraphConfig, DependencyGraphConfig,
GradleExecutionConfig, GradleExecutionConfig,
@ -12,7 +12,6 @@ import {
} from '../../configuration' } from '../../configuration'
import {failOnUseOfRemovedFeature, saveDeprecationState} from '../../deprecation-collector' import {failOnUseOfRemovedFeature, saveDeprecationState} from '../../deprecation-collector'
import {handleMainActionError} from '../../errors' import {handleMainActionError} from '../../errors'
import {forceExit} from '../../force-exit'
/** /**
* The main entry point for the action, called by Github Actions for the step. * The main entry point for the action, called by Github Actions for the step.
@ -28,7 +27,7 @@ export async function run(): Promise<void> {
setActionId('gradle/actions/setup-gradle') setActionId('gradle/actions/setup-gradle')
// Configure Gradle environment (Gradle User Home) // Configure Gradle environment (Gradle User Home)
await setupGradle.setup(new CacheConfig(), new DevelocityConfig(), new WrapperValidationConfig()) await setupGradle.setup(new CacheConfig(), new BuildScanConfig(), new WrapperValidationConfig())
// Configure the dependency graph submission // Configure the dependency graph submission
await dependencyGraph.setup(new DependencyGraphConfig()) await dependencyGraph.setup(new DependencyGraphConfig())
@ -43,7 +42,7 @@ export async function run(): Promise<void> {
} }
// Explicit process.exit() to prevent waiting for hanging promises. // Explicit process.exit() to prevent waiting for hanging promises.
await forceExit() process.exit()
} }
run() run()

View File

@ -4,7 +4,6 @@ import * as dependencyGraph from '../../dependency-graph'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../../configuration' import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors' import {handlePostActionError} from '../../errors'
import {emitDeprecationWarnings, restoreDeprecationState} from '../../deprecation-collector' import {emitDeprecationWarnings, restoreDeprecationState} from '../../deprecation-collector'
import {forceExit} from '../../force-exit'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // 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 // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
@ -28,7 +27,7 @@ export async function run(): Promise<void> {
} }
// Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save. // Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save.
await forceExit() process.exit()
} }
run() run()

View File

@ -1,5 +1,6 @@
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import {versionIsAtLeast} from './execution/gradle'
export interface BuildResult { export interface BuildResult {
get rootProjectName(): string get rootProjectName(): string
@ -13,14 +14,47 @@ export interface BuildResult {
get buildScanFailed(): boolean get buildScanFailed(): boolean
} }
export function loadBuildResults(): BuildResult[] { 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))
}
highestGradleVersion(): string | null {
if (this.results.length === 0) {
return null
}
return this.results
.map(result => result.gradleVersion)
.reduce((maxVersion: string, currentVersion: string) => {
if (!maxVersion) return currentVersion
return versionIsAtLeast(currentVersion, maxVersion) ? currentVersion : maxVersion
})
}
}
export function loadBuildResults(): BuildResults {
const results = getUnprocessedResults().map(filePath => { const results = getUnprocessedResults().map(filePath => {
const content = fs.readFileSync(filePath, 'utf8') const content = fs.readFileSync(filePath, 'utf8')
const buildResult = JSON.parse(content) as BuildResult const buildResult = JSON.parse(content) as BuildResult
addScanResults(filePath, buildResult) addScanResults(filePath, buildResult)
return buildResult return buildResult
}) })
return results return new BuildResults(results)
} }
export function markBuildResultsProcessed(): void { export function markBuildResultsProcessed(): void {

View File

@ -1,104 +0,0 @@
import * as fs from 'fs'
import * as path from 'path'
import {pathToFileURL} from 'url'
import {CacheConfig} from './configuration'
import {BuildResult} from './build-results'
import {CacheOptions, CacheService} from './cache-service'
const NOOP_CACHING_REPORT = `
[Cache was disabled](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#disabling-caching). Gradle User Home was not restored from or saved to the cache.
`
const CACHE_LICENSE_WARNING = `
***********************************************************
LICENSING NOTICE
The caching functionality in \`gradle-actions\` has been extracted into \`gradle-actions-caching\`, a proprietary commercial component that is not covered by the MIT License.
The bundled \`gradle-actions-caching\` component is licensed and governed by a separate license, available at https://gradle.com/legal/terms-of-use/.
The \`gradle-actions-caching\` component is used only when caching is enabled and is not loaded or used when caching is disabled.
Use of the \`gradle-actions-caching\` component is subject to a separate license, available at https://gradle.com/legal/terms-of-use/.
If you do not agree to these license terms, do not use the \`gradle-actions-caching\` component.
You can suppress this message by accepting the terms in your action configuration: see https://github.com/gradle/actions/blob/main/README.md
***********************************************************
`
const CACHE_LICENSE_SUMMARY = `
> [!IMPORTANT]
> #### Licensing notice
>
> The caching functionality in \`gradle-actions\` has been extracted into \`gradle-actions-caching\`, a proprietary commercial component that is not covered by the MIT License.
> The bundled \`gradle-actions-caching\` component is licensed and governed by a separate license, available at https://gradle.com/legal/terms-of-use/.
>
> The \`gradle-actions-caching\` component is used only when caching is enabled and is not loaded or used when caching is disabled.
>
> Use of the \`gradle-actions-caching\` component is subject to a separate license, available at https://gradle.com/legal/terms-of-use/.
> If you do not agree to these license terms, do not use the \`gradle-actions-caching\` component.
>
>You can suppress this message by [accepting the terms in your action configuration](https://github.com/gradle/actions/blob/main/README.md).
`
class NoOpCacheService implements CacheService {
async restore(_gradleUserHome: string, _cacheOptions: CacheOptions): Promise<void> {
return
}
async save(_gradleUserHome: string, _buildResults: BuildResult[], _cacheOptions: CacheOptions): Promise<string> {
return NOOP_CACHING_REPORT
}
}
class LicenseWarningCacheService implements CacheService {
private delegate: CacheService
constructor(delegate: CacheService) {
this.delegate = delegate
}
async restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void> {
await this.delegate.restore(gradleUserHome, cacheOptions)
}
async save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string> {
const cachingReport = await this.delegate.save(gradleUserHome, buildResults, cacheOptions)
return `${cachingReport}\n${CACHE_LICENSE_SUMMARY}`
}
}
export async function getCacheService(cacheConfig: CacheConfig): Promise<CacheService> {
if (cacheConfig.isCacheDisabled()) {
return new NoOpCacheService()
}
const cacheService = await loadVendoredCacheService()
if (cacheConfig.isCacheLicenseAccepted()) {
return cacheService
}
await logCacheLicenseWarning()
return new LicenseWarningCacheService(cacheService)
}
export async function loadVendoredCacheService(): Promise<CacheService> {
const vendoredLibraryPath = findVendoredLibraryPath()
const moduleUrl = pathToFileURL(vendoredLibraryPath).href
return (await import(moduleUrl)) as CacheService
}
function findVendoredLibraryPath(): string {
const moduleDir = import.meta.dirname
const absolutePath = path.resolve(moduleDir, '../../../sources/vendor/gradle-actions-caching/index.js')
if (fs.existsSync(absolutePath)) {
return absolutePath
}
throw new Error(`Unable to locate vendored cache library at ${absolutePath}.`)
}
export async function logCacheLicenseWarning(): Promise<void> {
console.info(CACHE_LICENSE_WARNING)
}

View File

@ -1,18 +0,0 @@
import {BuildResult} from './build-results'
export interface CacheOptions {
disabled: boolean
readOnly: boolean
writeOnly: boolean
overwriteExisting: boolean
strictMatch: boolean
cleanup: string
encryptionKey?: string
includes: string[]
excludes: string[]
}
export interface CacheService {
restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>
save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string>
}

View File

@ -0,0 +1,124 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import fs from 'fs'
import path from 'path'
import * as provisioner from '../execution/provision'
import {BuildResult, BuildResults} from '../build-results'
import {versionIsAtLeast} from '../execution/gradle'
import {gradleWrapperScript} from '../execution/gradlew'
export class CacheCleaner {
private readonly gradleUserHome: string
private readonly tmpDir: string
constructor(gradleUserHome: string, tmpDir: string) {
this.gradleUserHome = gradleUserHome
this.tmpDir = tmpDir
}
async prepare(): Promise<string> {
// Save the current timestamp
const timestamp = Date.now().toString()
core.saveState('clean-timestamp', timestamp)
return timestamp
}
async forceCleanup(buildResults: BuildResults): Promise<void> {
const executable = await this.gradleExecutableForCleanup(buildResults)
const cleanTimestamp = core.getState('clean-timestamp')
await this.forceCleanupFilesOlderThan(cleanTimestamp, executable)
}
/**
* Attempt to use the newest Gradle version that was used to run a build, at least 8.11.
*
* This will avoid the need to provision a Gradle version for the cleanup when not necessary.
*/
private async gradleExecutableForCleanup(buildResults: BuildResults): Promise<string> {
const preferredVersion = buildResults.highestGradleVersion()
if (preferredVersion && versionIsAtLeast(preferredVersion, '8.11')) {
try {
const wrapperScripts = buildResults.results
.map(result => this.findGradleWrapperScript(result))
.filter(Boolean) as string[]
return await provisioner.provisionGradleWithVersionAtLeast(preferredVersion, wrapperScripts)
} catch (_) {
// Ignore the case where the preferred version cannot be located in https://services.gradle.org/versions/all.
// This can happen for snapshot Gradle versions.
core.info(
`Failed to provision Gradle ${preferredVersion} for cache cleanup. Falling back to default version.`
)
}
}
// Fallback to the minimum version required for cache-cleanup
return await provisioner.provisionGradleWithVersionAtLeast('8.11')
}
private findGradleWrapperScript(result: BuildResult): string | null {
try {
const wrapperScript = gradleWrapperScript(result.rootProjectDir)
return path.resolve(result.rootProjectDir, wrapperScript)
} catch (error) {
core.debug(`No Gradle Wrapper found for ${result.rootProjectName}: ${error}`)
return null
}
}
// Visible for testing
async forceCleanupFilesOlderThan(cleanTimestamp: string, executable: string): Promise<void> {
// Run a dummy Gradle build to trigger cache cleanup
const cleanupProjectDir = path.resolve(this.tmpDir, 'dummy-cleanup-project')
fs.mkdirSync(cleanupProjectDir, {recursive: true})
fs.writeFileSync(
path.resolve(cleanupProjectDir, 'settings.gradle'),
'rootProject.name = "dummy-cleanup-project"'
)
fs.writeFileSync(
path.resolve(cleanupProjectDir, 'init.gradle'),
`
beforeSettings { settings ->
def cleanupTime = ${cleanTimestamp}
settings.caches {
cleanup = Cleanup.ALWAYS
releasedWrappers.setRemoveUnusedEntriesOlderThan(cleanupTime)
snapshotWrappers.setRemoveUnusedEntriesOlderThan(cleanupTime)
downloadedResources.setRemoveUnusedEntriesOlderThan(cleanupTime)
createdResources.setRemoveUnusedEntriesOlderThan(cleanupTime)
buildCache.setRemoveUnusedEntriesOlderThan(cleanupTime)
}
}
`
)
fs.writeFileSync(path.resolve(cleanupProjectDir, 'build.gradle'), 'task("noop") {}')
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',
'init.gradle',
'--info',
'--no-daemon',
'--no-scan',
'--build-cache',
'-DGITHUB_DEPENDENCY_GRAPH_ENABLED=false',
'-DGRADLE_ACTIONS_SKIP_BUILD_RESULT_CAPTURE=true',
'noop'
]
await exec.exec(executable, args, {
cwd: cleanupProjectDir
})
}
}

View File

@ -0,0 +1,100 @@
import * as github from '@actions/github'
import {CacheConfig, getJobMatrix} from '../configuration'
import {hashStrings} from './cache-utils'
const CACHE_PROTOCOL_VERSION = 'v1'
const CACHE_KEY_PREFIX_VAR = 'GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX'
const CACHE_KEY_OS_VAR = 'GRADLE_BUILD_ACTION_CACHE_KEY_ENVIRONMENT'
const CACHE_KEY_JOB_VAR = 'GRADLE_BUILD_ACTION_CACHE_KEY_JOB'
const CACHE_KEY_JOB_INSTANCE_VAR = 'GRADLE_BUILD_ACTION_CACHE_KEY_JOB_INSTANCE'
const CACHE_KEY_JOB_EXECUTION_VAR = 'GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION'
/**
* Represents a key used to restore a cache entry.
* The Github Actions cache will first try for an exact match on the key.
* If that fails, it will try for a prefix match on any of the restoreKeys.
*/
export class CacheKey {
key: string
restoreKeys: string[]
constructor(key: string, restoreKeys: string[]) {
this.key = key
this.restoreKeys = restoreKeys
}
}
/**
* Generates a cache key specific to the current job execution.
* The key is constructed from the following inputs (with some user overrides):
* - The cache key prefix: defaults to 'gradle-' but can be overridden by the user
* - The cache protocol version
* - The runner operating system
* - The name of the workflow and Job being executed
* - The matrix values for the Job being executed (job context)
* - The SHA of the commit being executed
*
* Caches are restored by trying to match the these key prefixes in order:
* - The full key with SHA
* - A previous key for this Job + matrix
* - Any previous key for this Job (any matrix)
* - Any previous key for this cache on the current OS
*/
export function generateCacheKey(cacheName: string, config: CacheConfig): CacheKey {
const prefix = process.env[CACHE_KEY_PREFIX_VAR] || ''
const cacheKeyBase = `${prefix}${getCacheKeyBase(cacheName, CACHE_PROTOCOL_VERSION)}`
// At the most general level, share caches for all executions on the same OS
const cacheKeyForEnvironment = `${cacheKeyBase}|${getCacheKeyEnvironment()}`
// Then prefer caches that run job with the same ID
const cacheKeyForJob = `${cacheKeyForEnvironment}|${getCacheKeyJob()}`
// Prefer (even more) jobs that run this job in the same workflow with the same context (matrix)
const cacheKeyForJobContext = `${cacheKeyForJob}[${getCacheKeyJobInstance()}]`
// Exact match on Git SHA
const cacheKey = `${cacheKeyForJobContext}-${getCacheKeyJobExecution()}`
if (config.isCacheStrictMatch()) {
return new CacheKey(cacheKey, [cacheKeyForJobContext])
}
return new CacheKey(cacheKey, [cacheKeyForJobContext, cacheKeyForJob, cacheKeyForEnvironment])
}
export function getCacheKeyBase(cacheName: string, cacheProtocolVersion: string): string {
// Prefix can be used to force change all cache keys (defaults to cache protocol version)
return `gradle-${cacheName}-${cacheProtocolVersion}`
}
function getCacheKeyEnvironment(): string {
const runnerOs = process.env['RUNNER_OS'] || ''
const runnerArch = process.env['RUNNER_ARCH'] || ''
return process.env[CACHE_KEY_OS_VAR] || `${runnerOs}-${runnerArch}`
}
function getCacheKeyJob(): string {
return process.env[CACHE_KEY_JOB_VAR] || github.context.job
}
function getCacheKeyJobInstance(): string {
const override = process.env[CACHE_KEY_JOB_INSTANCE_VAR]
if (override) {
return override
}
// By default, we hash the workflow name and the full `matrix` data for the run, to uniquely identify this job invocation
// The only way we can obtain the `matrix` data is via the `workflow-job-context` parameter in action.yml.
const workflowName = github.context.workflow
const workflowJobContext = getJobMatrix()
return hashStrings([workflowName, workflowJobContext])
}
function getCacheKeyJobExecution(): string {
// Used to associate a cache key with a particular execution (default is bound to the git commit sha)
return process.env[CACHE_KEY_JOB_EXECUTION_VAR] || github.context.sha
}

View File

@ -0,0 +1,294 @@
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). 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). 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.
*/
export class CacheListener {
cacheEntries: CacheEntryListener[] = []
cacheReadOnly = false
cacheWriteOnly = false
cacheDisabled = false
cacheStatusReason: string = DEFAULT_CACHE_ENABLED_REASON
cacheCleanupMessage: string = DEFAULT_CLEANUP_DISABLED_REASON
get fullyRestored(): boolean {
return this.cacheEntries.every(x => !x.wasRequestedButNotRestored())
}
get cacheStatus(): string {
if (!cache.isFeatureAvailable()) return 'not available'
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) {
return entry
}
}
const newEntry = new CacheEntryListener(name)
this.cacheEntries.push(newEntry)
return newEntry
}
stringify(): string {
return JSON.stringify(this)
}
static rehydrate(stringRep: string): CacheListener {
if (stringRep === '') {
return new CacheListener()
}
const rehydrated: CacheListener = Object.assign(new CacheListener(), JSON.parse(stringRep))
const entries = rehydrated.cacheEntries
for (let index = 0; index < entries.length; index++) {
const rawEntry = entries[index]
entries[index] = Object.assign(new CacheEntryListener(rawEntry.entryName), rawEntry)
}
return rehydrated
}
}
/**
* Collects information on the state of a single cache entry.
*/
export class CacheEntryListener {
entryName: string
requestedKey: string | undefined
requestedRestoreKeys: string[] | undefined
restoredKey: string | undefined
restoredSize: number | undefined
restoredTime: number | undefined
notRestored: string | undefined
savedKey: string | undefined
savedSize: number | undefined
savedTime: number | undefined
notSaved: string | undefined
constructor(entryName: string) {
this.entryName = entryName
}
wasRequestedButNotRestored(): boolean {
return this.requestedKey !== undefined && this.restoredKey === undefined
}
markRequested(key: string, restoreKeys: string[] = []): CacheEntryListener {
this.requestedKey = key
this.requestedRestoreKeys = restoreKeys
return this
}
markRestored(key: string, size: number | undefined, time: number): CacheEntryListener {
this.restoredKey = key
this.restoredSize = size
this.restoredTime = time
return this
}
markNotRestored(message: string): CacheEntryListener {
this.notRestored = message
return this
}
markSaved(key: string, size: number | undefined, time: number): CacheEntryListener {
this.savedKey = key
this.savedSize = size
this.savedTime = time
return this
}
markAlreadyExists(key: string): CacheEntryListener {
this.savedKey = key
this.savedSize = 0
return this
}
markNotSaved(message: string): CacheEntryListener {
this.notSaved = message
return this
}
}
export function generateCachingReport(listener: CacheListener): string {
const entries = listener.cacheEntries
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>
<pre>
${renderEntryDetails(listener)}
</pre>
</details>
`
}
function renderEntryTable(entries: CacheEntryListener[]): string {
return `
<table>
<tr><td></td><th>Count</th><th>Total Size (Mb)</th><th>Total Time (ms)</tr>
<tr><td>Entries Restored</td>
<td>${getCount(entries, e => e.restoredSize)}</td>
<td>${getSize(entries, e => e.restoredSize)}</td>
<td>${getTime(entries, e => e.restoredTime)}</td>
</tr>
<tr><td>Entries Saved</td>
<td>${getCount(entries, e => e.savedSize)}</td>
<td>${getSize(entries, e => e.savedSize)}</td>
<td>${getTime(entries, e => e.savedTime)}</td>
</tr>
</table>
`
}
function renderEntryDetails(listener: CacheListener): string {
return listener.cacheEntries
.map(
entry => `Entry: ${entry.entryName}
Requested Key : ${entry.requestedKey ?? ''}
Restored Key : ${entry.restoredKey ?? ''}
Size: ${formatSize(entry.restoredSize)}
Time: ${formatTime(entry.restoredTime)}
${getRestoredMessage(entry, listener.cacheWriteOnly)}
Saved Key : ${entry.savedKey ?? ''}
Size: ${formatSize(entry.savedSize)}
Time: ${formatTime(entry.savedTime)}
${getSavedMessage(entry, listener.cacheReadOnly)}
`
)
.join('---\n')
}
function getRestoredMessage(entry: CacheEntryListener, cacheWriteOnly: boolean): string {
if (entry.notRestored) {
return `(Entry not restored: ${entry.notRestored})`
}
if (cacheWriteOnly) {
return '(Entry not restored: cache is write-only)'
}
if (entry.requestedKey === undefined) {
return '(Entry not restored: not requested)'
}
if (entry.restoredKey === undefined) {
return '(Entry not restored: no match found)'
}
if (entry.restoredKey === entry.requestedKey) {
return '(Entry restored: exact match found)'
}
return '(Entry restored: partial match found)'
}
function getSavedMessage(entry: CacheEntryListener, cacheReadOnly: boolean): string {
if (entry.notSaved) {
return `(Entry not saved: ${entry.notSaved})`
}
if (entry.savedKey === undefined) {
if (cacheReadOnly) {
return '(Entry not saved: cache is read-only)'
}
if (entry.notRestored) {
return '(Entry not saved: not restored)'
}
return '(Entry not saved: reason unknown)'
}
if (entry.savedSize === 0) {
return '(Entry not saved: entry with key already exists)'
}
return '(Entry saved)'
}
function getCount(
cacheEntries: CacheEntryListener[],
predicate: (value: CacheEntryListener) => number | undefined
): number {
return cacheEntries.filter(e => predicate(e)).length
}
function getSize(
cacheEntries: CacheEntryListener[],
predicate: (value: CacheEntryListener) => number | undefined
): number {
const bytes = cacheEntries.map(e => predicate(e) ?? 0).reduce((p, v) => p + v, 0)
return Math.round(bytes / (1024 * 1024))
}
function getTime(
cacheEntries: CacheEntryListener[],
predicate: (value: CacheEntryListener) => number | undefined
): number {
return cacheEntries.map(e => predicate(e) ?? 0).reduce((p, v) => p + v, 0)
}
function formatSize(bytes: number | undefined): string {
if (bytes === undefined || bytes === 0) {
return ''
}
return `${Math.round(bytes / (1024 * 1024))} MB (${bytes} B)`
}
function formatTime(ms: number | undefined): string {
if (ms === undefined || ms === 0) {
return ''
}
return `${ms} ms`
}

View File

@ -0,0 +1,140 @@
import * as core from '@actions/core'
import * as cache from '@actions/cache'
import * as exec from '@actions/exec'
import * as crypto from 'crypto'
import * as path from 'path'
import * as fs from 'fs'
import {CacheEntryListener} from './cache-reporting'
const SEGMENT_DOWNLOAD_TIMEOUT_VAR = 'SEGMENT_DOWNLOAD_TIMEOUT_MINS'
const SEGMENT_DOWNLOAD_TIMEOUT_DEFAULT = 10 * 60 * 1000 // 10 minutes
export function isCacheDebuggingEnabled(): boolean {
if (core.isDebug()) {
return true
}
return process.env['GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED'] ? true : false
}
export function hashFileNames(fileNames: string[]): string {
return hashStrings(fileNames.map(x => x.replace(new RegExp(`\\${path.sep}`, 'g'), '/')))
}
export function hashStrings(values: string[]): string {
const hash = crypto.createHash('md5')
for (const value of values) {
hash.update(value)
}
return hash.digest('hex')
}
export async function restoreCache(
cachePath: string[],
cacheKey: string,
cacheRestoreKeys: string[],
listener: CacheEntryListener
): Promise<cache.CacheEntry | undefined> {
listener.markRequested(cacheKey, cacheRestoreKeys)
try {
const startTime = Date.now()
// Only override the read timeout if the SEGMENT_DOWNLOAD_TIMEOUT_MINS env var has NOT been set
const cacheRestoreOptions = process.env[SEGMENT_DOWNLOAD_TIMEOUT_VAR]
? {}
: {segmentTimeoutInMs: SEGMENT_DOWNLOAD_TIMEOUT_DEFAULT}
const restoredEntry = await cache.restoreCache(cachePath, cacheKey, cacheRestoreKeys, cacheRestoreOptions)
if (restoredEntry !== undefined) {
const restoreTime = Date.now() - startTime
listener.markRestored(restoredEntry.key, restoredEntry.size, restoreTime)
core.info(`Restored cache entry with key ${cacheKey} to ${cachePath.join()} in ${restoreTime}ms`)
}
return restoredEntry
} catch (error) {
listener.markNotRestored((error as Error).message)
handleCacheFailure(error, `Failed to restore ${cacheKey}`)
return undefined
}
}
export async function saveCache(cachePath: string[], cacheKey: string, listener: CacheEntryListener): Promise<void> {
try {
const startTime = Date.now()
const savedEntry = await cache.saveCache(cachePath, cacheKey)
const saveTime = Date.now() - startTime
listener.markSaved(savedEntry.key, savedEntry.size, saveTime)
core.info(`Saved cache entry with key ${cacheKey} from ${cachePath.join()} in ${saveTime}ms`)
} catch (error) {
if (error instanceof cache.ReserveCacheError) {
listener.markAlreadyExists(cacheKey)
} else {
listener.markNotSaved((error as Error).message)
}
handleCacheFailure(error, `Failed to save cache entry with path '${cachePath}' and key: ${cacheKey}`)
}
}
export function cacheDebug(message: string): void {
if (isCacheDebuggingEnabled()) {
core.info(message)
} else {
core.debug(message)
}
}
export function handleCacheFailure(error: unknown, message: string): void {
if (error instanceof cache.ValidationError) {
// Fail on cache validation errors
throw error
}
if (error instanceof cache.ReserveCacheError) {
// Reserve cache errors are expected if the artifact has been previously cached
core.info(`${message}: ${error}`)
} else {
// Warn on all other errors
core.warning(`${message}: ${error}`)
if (error instanceof Error && error.stack) {
cacheDebug(error.stack)
}
}
}
/**
* Attempt to delete a file or directory, waiting to allow locks to be released
*/
export async function tryDelete(file: string): Promise<void> {
const maxAttempts = 5
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (!fs.existsSync(file)) {
return
}
try {
const stat = fs.lstatSync(file)
if (stat.isDirectory()) {
fs.rmSync(file, {recursive: true})
} else {
fs.unlinkSync(file)
}
return
} catch (error) {
if (attempt === maxAttempts) {
core.warning(`Failed to delete ${file}, which will impact caching.
It is likely locked by another process. Output of 'jps -ml':
${await getJavaProcesses()}`)
throw error
} else {
cacheDebug(`Attempt to delete ${file} failed. Will try again.`)
await delay(1000)
}
}
}
}
async function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function getJavaProcesses(): Promise<string> {
const jpsOutput = await exec.getExecOutput('jps', ['-lm'])
return jpsOutput.stdout
}

View File

@ -0,0 +1,124 @@
import * as core from '@actions/core'
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'
export async function restore(
userHome: string,
gradleUserHome: string,
cacheListener: CacheListener,
cacheConfig: CacheConfig
): Promise<void> {
// Bypass restore cache on all but first action step in workflow.
if (process.env[CACHE_RESTORED_VAR]) {
core.info('Cache only restored on first action step.')
return
}
core.exportVariable(CACHE_RESTORED_VAR, true)
const gradleStateCache = new GradleUserHomeCache(userHome, gradleUserHome, cacheConfig)
if (cacheConfig.isCacheDisabled()) {
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.setDisabled()
return
}
if (gradleStateCache.cacheOutputExists()) {
if (!cacheConfig.isCacheOverwriteExisting()) {
core.info('Gradle User Home already exists: will not restore from cache.')
// Initialize pre-existing Gradle User Home.
gradleStateCache.init()
cacheListener.setDisabled(EXISTING_GRADLE_HOME)
return
}
core.info('Gradle User Home already exists: will overwrite with cached contents.')
}
gradleStateCache.init()
// 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.setWriteOnly()
return
}
await core.group('Restore Gradle state from cache', async () => {
await gradleStateCache.restore(cacheListener)
})
}
export async function save(
userHome: string,
gradleUserHome: string,
cacheListener: CacheListener,
daemonController: DaemonController,
buildResults: BuildResults,
cacheConfig: CacheConfig
): Promise<void> {
if (cacheConfig.isCacheDisabled()) {
core.info('Cache is disabled: will not save state for later builds.')
return
}
if (!core.getState(CACHE_RESTORED_VAR)) {
core.info('Cache will not be saved: not restored in main action step.')
return
}
if (cacheConfig.isCacheReadOnly()) {
core.info('Cache is read-only: will not save state for use in subsequent builds.')
cacheListener.setReadOnly()
return
}
await core.group('Stopping Gradle daemons', async () => {
await daemonController.stopAllDaemons()
})
if (cacheConfig.isCacheCleanupEnabled()) {
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, buildResults)
} else {
core.info('Not performing cache-cleanup due to build failure')
cacheListener.setCacheCleanupDisabled(CLEANUP_DISABLED_DUE_TO_FAILURE)
}
}
await core.group('Caching Gradle state', async () => {
return new GradleUserHomeCache(userHome, gradleUserHome, cacheConfig).save(cacheListener)
})
}
async function performCacheCleanup(gradleUserHome: string, buildResults: BuildResults): Promise<void> {
const cacheCleaner = new CacheCleaner(gradleUserHome, process.env['RUNNER_TEMP']!)
try {
await cacheCleaner.forceCleanup(buildResults)
} catch (e) {
core.warning(`Cache cleanup failed. Will continue. ${String(e)}`)
}
}

View File

@ -0,0 +1,467 @@
import path from 'path'
import fs from 'fs'
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import {CacheEntryListener, CacheListener} from './cache-reporting'
import {cacheDebug, hashFileNames, isCacheDebuggingEnabled, restoreCache, saveCache, tryDelete} from './cache-utils'
import {BuildResult, loadBuildResults} from '../build-results'
import {CacheConfig, ACTION_METADATA_DIR} from '../configuration'
import {getCacheKeyBase} from './cache-key'
import {versionIsAtLeast} from '../execution/gradle'
const SKIP_RESTORE_VAR = 'GRADLE_BUILD_ACTION_SKIP_RESTORE'
const CACHE_PROTOCOL_VERSION = 'v1'
/**
* Represents the result of attempting to load or store an extracted cache entry.
* An undefined cacheKey indicates that the operation did not succeed.
* The collected results are then used to populate the `cache-metadata.json` file for later use.
*/
class ExtractedCacheEntry {
artifactType: string
pattern: string
cacheKey: string | undefined
constructor(artifactType: string, pattern: string, cacheKey: string | undefined) {
this.artifactType = artifactType
this.pattern = pattern
this.cacheKey = cacheKey
}
}
/**
* Representation of all of the extracted cache entries for this Gradle User Home.
* This object is persisted to JSON file in the Gradle User Home directory for storing,
* and subsequently used to restore the Gradle User Home.
*/
class ExtractedCacheEntryMetadata {
entries: ExtractedCacheEntry[] = []
}
/**
* The specification for a type of extracted cache entry.
*/
class ExtractedCacheEntryDefinition {
artifactType: string
pattern: string
bundle: boolean
uniqueFileNames = true
notCacheableReason: string | undefined
constructor(artifactType: string, pattern: string, bundle: boolean) {
this.artifactType = artifactType
this.pattern = pattern
this.bundle = bundle
}
/**
* Indicate that the file names matching the cache entry pattern are NOT sufficient to uniquely identify the contents.
* If the file names are sufficient, then we use a hash of the file names to identify the entry.
* With non-unique-file-names, we hash the file contents to identify the cache entry.
*/
withNonUniqueFileNames(): ExtractedCacheEntryDefinition {
this.uniqueFileNames = false
return this
}
/**
* Specify that the cache entry, should not be saved for some reason, even though the contents exist.
* This is used to prevent configuration-cache entries being cached when they were generated by Gradle < 8.6,
*/
notCacheableBecause(reason: string): ExtractedCacheEntryDefinition {
this.notCacheableReason = reason
return this
}
}
/**
* Caches and restores the entire Gradle User Home directory, extracting entries containing common artifacts
* for more efficient storage.
*/
abstract class AbstractEntryExtractor {
protected readonly cacheConfig: CacheConfig
protected readonly gradleUserHome: string
private extractorName: string
constructor(gradleUserHome: string, extractorName: string, cacheConfig: CacheConfig) {
this.gradleUserHome = gradleUserHome
this.extractorName = extractorName
this.cacheConfig = cacheConfig
}
/**
* Restores any artifacts that were cached separately, based on the information in the `cache-metadata.json` file.
* Each extracted cache entry is restored in parallel, except when debugging is enabled.
*/
async restore(listener: CacheListener): Promise<void> {
const previouslyExtractedCacheEntries = this.loadExtractedCacheEntries()
const processes: Promise<ExtractedCacheEntry>[] = []
for (const cacheEntry of previouslyExtractedCacheEntries) {
const artifactType = cacheEntry.artifactType
const entryListener = listener.entry(cacheEntry.pattern)
// Handle case where the extracted-cache-entry definitions have been changed
const skipRestore = process.env[SKIP_RESTORE_VAR] || ''
if (skipRestore.includes(artifactType)) {
core.info(`Not restoring extracted cache entry for ${artifactType}`)
entryListener.markRequested('SKIP_RESTORE')
} else {
processes.push(
this.awaitForDebugging(
this.restoreExtractedCacheEntry(
artifactType,
cacheEntry.cacheKey!,
cacheEntry.pattern,
entryListener
)
)
)
}
}
this.saveMetadataForCacheResults(await Promise.all(processes))
}
private async restoreExtractedCacheEntry(
artifactType: string,
cacheKey: string,
pattern: string,
listener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
const restoredEntry = await restoreCache(pattern.split('\n'), cacheKey, [], listener)
if (restoredEntry) {
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
} else {
core.info(`Did not restore ${artifactType} with key ${cacheKey} to ${pattern}`)
return new ExtractedCacheEntry(artifactType, pattern, undefined)
}
}
/**
* Saves any artifacts that are configured to be cached separately, based on the extracted cache entry definitions.
* Each entry is extracted and saved in parallel, except when debugging is enabled.
*/
async extract(listener: CacheListener): Promise<void> {
// Load the cache entry definitions (from config) and the previously restored entries (from persisted metadata file)
const cacheEntryDefinitions = this.getExtractedCacheEntryDefinitions()
cacheDebug(
`Extracting cache entries for ${this.extractorName}: ${JSON.stringify(cacheEntryDefinitions, null, 2)}`
)
const previouslyRestoredEntries = this.loadExtractedCacheEntries()
const cacheActions: Promise<ExtractedCacheEntry>[] = []
// For each cache entry definition, determine if it has already been restored, and if not, extract it
for (const cacheEntryDefinition of cacheEntryDefinitions) {
const artifactType = cacheEntryDefinition.artifactType
const pattern = cacheEntryDefinition.pattern
if (cacheEntryDefinition.notCacheableReason) {
listener.entry(pattern).markNotSaved(cacheEntryDefinition.notCacheableReason)
continue
}
// Find all matching files for this cache entry definition
const globber = await glob.create(pattern, {
implicitDescendants: false
})
const matchingFiles = await globber.glob()
if (matchingFiles.length === 0) {
cacheDebug(`No files found to cache for ${artifactType}`)
continue
}
if (cacheEntryDefinition.bundle) {
// For an extracted "bundle", use the defined pattern and cache all matching files in a single entry.
cacheActions.push(
this.awaitForDebugging(
this.saveExtractedCacheEntry(
matchingFiles,
artifactType,
pattern,
cacheEntryDefinition.uniqueFileNames,
previouslyRestoredEntries,
listener.entry(pattern)
)
)
)
} else {
// Otherwise cache each matching file in a separate entry, using the complete file path as the cache pattern.
for (const cacheFile of matchingFiles) {
cacheActions.push(
this.awaitForDebugging(
this.saveExtractedCacheEntry(
[cacheFile],
artifactType,
cacheFile,
cacheEntryDefinition.uniqueFileNames,
previouslyRestoredEntries,
listener.entry(cacheFile)
)
)
)
}
}
}
this.saveMetadataForCacheResults(await Promise.all(cacheActions))
}
private async saveExtractedCacheEntry(
matchingFiles: string[],
artifactType: string,
pattern: string,
uniqueFileNames: boolean,
previouslyRestoredEntries: ExtractedCacheEntry[],
entryListener: CacheEntryListener
): Promise<ExtractedCacheEntry> {
const cacheKey = uniqueFileNames
? this.createCacheKeyFromFileNames(artifactType, matchingFiles)
: await this.createCacheKeyFromFileContents(artifactType, pattern)
const previouslyRestoredKey = previouslyRestoredEntries.find(
x => x.artifactType === artifactType && x.pattern === pattern
)?.cacheKey
if (previouslyRestoredKey === cacheKey) {
cacheDebug(`No change to previously restored ${artifactType}. Not saving.`)
entryListener.markNotSaved('contents unchanged')
} else {
await saveCache(pattern.split('\n'), cacheKey, entryListener)
}
for (const file of matchingFiles) {
tryDelete(file)
}
return new ExtractedCacheEntry(artifactType, pattern, cacheKey)
}
protected createCacheKeyFromFileNames(artifactType: string, files: string[]): string {
const relativeFiles = files.map(x => path.relative(this.gradleUserHome, x))
const key = hashFileNames(relativeFiles)
cacheDebug(`Generating cache key for ${artifactType} from file names: ${relativeFiles}`)
return `${getCacheKeyBase(artifactType, CACHE_PROTOCOL_VERSION)}-${key}`
}
protected async createCacheKeyFromFileContents(artifactType: string, pattern: string): Promise<string> {
const key = await glob.hashFiles(pattern)
cacheDebug(`Generating cache key for ${artifactType} from files matching: ${pattern}`)
return `${getCacheKeyBase(artifactType, CACHE_PROTOCOL_VERSION)}-${key}`
}
// Run actions sequentially if debugging is enabled
private async awaitForDebugging(p: Promise<ExtractedCacheEntry>): Promise<ExtractedCacheEntry> {
if (isCacheDebuggingEnabled()) {
await p
}
return p
}
/**
* Load information about the extracted cache entries previously restored/saved. This is loaded from the 'cache-metadata.json' file.
*/
protected loadExtractedCacheEntries(): ExtractedCacheEntry[] {
const cacheMetadataFile = this.getCacheMetadataFile()
if (!fs.existsSync(cacheMetadataFile)) {
return []
}
const filedata = fs.readFileSync(cacheMetadataFile, 'utf-8')
cacheDebug(`Loaded cache metadata for ${this.extractorName}: ${filedata}`)
const extractedCacheEntryMetadata = JSON.parse(filedata) as ExtractedCacheEntryMetadata
return extractedCacheEntryMetadata.entries
}
/**
* Saves information about the extracted cache entries into the 'cache-metadata.json' file.
*/
protected saveMetadataForCacheResults(results: ExtractedCacheEntry[]): void {
const extractedCacheEntryMetadata = new ExtractedCacheEntryMetadata()
extractedCacheEntryMetadata.entries = results.filter(x => x.cacheKey !== undefined)
const filedata = JSON.stringify(extractedCacheEntryMetadata)
cacheDebug(`Saving cache metadata for ${this.extractorName}: ${filedata}`)
fs.writeFileSync(this.getCacheMetadataFile(), filedata, 'utf-8')
}
private getCacheMetadataFile(): string {
const actionMetadataDirectory = path.resolve(this.gradleUserHome, ACTION_METADATA_DIR)
fs.mkdirSync(actionMetadataDirectory, {recursive: true})
return path.resolve(actionMetadataDirectory, `${this.extractorName}-entry-metadata.json`)
}
protected abstract getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[]
}
export class GradleHomeEntryExtractor extends AbstractEntryExtractor {
constructor(gradleUserHome: string, cacheConfig: CacheConfig) {
super(gradleUserHome, 'gradle-home', cacheConfig)
}
async extract(listener: CacheListener): Promise<void> {
await this.deleteWrapperZips()
return super.extract(listener)
}
/**
* Delete any downloaded wrapper zip files that are not needed after extraction.
* These files are cleaned up by Gradle >= 7.5, but for older versions we remove them manually.
*/
private async deleteWrapperZips(): Promise<void> {
const wrapperZips = path.resolve(this.gradleUserHome, 'wrapper/dists/*/*/*.zip')
const globber = await glob.create(wrapperZips, {
implicitDescendants: false
})
for (const wrapperZip of await globber.glob()) {
cacheDebug(`Deleting wrapper zip: ${wrapperZip}`)
await tryDelete(wrapperZip)
}
}
/**
* Return the extracted cache entry definitions, which determine which artifacts will be cached
* separately from the rest of the Gradle User Home cache entry.
*/
protected getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[] {
const entryDefinition = (
artifactType: string,
patterns: string[],
bundle: boolean
): ExtractedCacheEntryDefinition => {
const resolvedPatterns = patterns
.map(x => {
const isDir = x.endsWith('/')
const resolved = path.resolve(this.gradleUserHome, x)
return isDir ? `${resolved}/` : resolved // Restore trailing '/' removed by path.resolve()
})
.join('\n')
return new ExtractedCacheEntryDefinition(artifactType, resolvedPatterns, bundle)
}
return [
entryDefinition('generated-gradle-jars', ['caches/*/generated-gradle-jars/*.jar'], false),
entryDefinition('wrapper-zips', ['wrapper/dists/*/*/'], false), // Each wrapper directory cached separately
entryDefinition('java-toolchains', ['jdks/*/'], false), // Each extracted JDK cached separately
entryDefinition('dependencies', ['caches/modules-*/files-*/*/*/*/*'], true),
entryDefinition('instrumented-jars', ['caches/jars-*/*/'], true),
entryDefinition('kotlin-dsl', ['caches/*/kotlin-dsl/accessors/*/', 'caches/*/kotlin-dsl/scripts/*/'], true),
entryDefinition('groovy-dsl', ['caches/*/groovy-dsl/*/'], true),
entryDefinition('transforms', ['caches/transforms-4/*/', 'caches/*/transforms/*/'], true)
]
}
}
export class ConfigurationCacheEntryExtractor extends AbstractEntryExtractor {
constructor(gradleUserHome: string, cacheConfig: CacheConfig) {
super(gradleUserHome, 'configuration-cache', cacheConfig)
}
/**
* Handle the case where Gradle User Home has not been fully restored, so that the configuration-cache
* entry is not reusable.
*/
async restore(listener: CacheListener): Promise<void> {
if (!listener.fullyRestored) {
this.markNotRestored(listener, 'Gradle User Home was not fully restored')
return
}
if (!this.cacheConfig.getCacheEncryptionKey()) {
this.markNotRestored(listener, 'Encryption Key was not provided')
return
}
return await super.restore(listener)
}
private markNotRestored(listener: CacheListener, reason: string): void {
const cacheEntries = this.loadExtractedCacheEntries()
if (cacheEntries.length > 0) {
core.info(`Not restoring configuration-cache state, as ${reason}`)
for (const cacheEntry of cacheEntries) {
listener.entry(cacheEntry.pattern).markNotRestored(reason)
}
// Update the results file based on no entries restored
this.saveMetadataForCacheResults([])
}
}
async extract(listener: CacheListener): Promise<void> {
if (!this.cacheConfig.getCacheEncryptionKey()) {
const cacheEntryDefinitions = this.getExtractedCacheEntryDefinitions()
if (cacheEntryDefinitions.length > 0) {
core.info('Not saving configuration-cache state, as no encryption key was provided')
for (const cacheEntry of cacheEntryDefinitions) {
listener.entry(cacheEntry.pattern).markNotSaved('No encryption key provided')
}
}
return
}
await super.extract(listener)
}
/**
* Extract cache entries for the configuration cache in each project.
*/
protected getExtractedCacheEntryDefinitions(): ExtractedCacheEntryDefinition[] {
// Group BuildResult by existing configCacheDir
const groupedResults = this.getConfigCacheDirectoriesWithAssociatedBuildResults()
return Object.entries(groupedResults).map(([configCachePath, pathResults]) => {
// Create a entry definition for each unique configuration cache directory
const definition = new ExtractedCacheEntryDefinition(
'configuration-cache',
configCachePath,
true
).withNonUniqueFileNames()
// If any associated build result used Gradle < 8.6, then mark it as not cacheable
if (
pathResults.find(result => {
return !versionIsAtLeast(result.gradleVersion, '8.6.0')
})
) {
core.info(
`Not saving config-cache data for ${configCachePath}. Configuration cache data is only saved for Gradle 8.6+`
)
definition.notCacheableBecause('Configuration cache data only saved for Gradle 8.6+')
}
return definition
})
}
private getConfigCacheDirectoriesWithAssociatedBuildResults(): Record<string, BuildResult[]> {
return loadBuildResults().results.reduce(
(acc, buildResult) => {
// For each build result, find the config-cache dir
const configCachePath = path.resolve(buildResult.rootProjectDir, '.gradle/configuration-cache')
// Ignore case where config-cache dir doesn't exist
if (!fs.existsSync(configCachePath)) {
return acc
}
// Group by unique config cache directories and collect associated build results
if (!acc[configCachePath]) {
acc[configCachePath] = []
}
acc[configCachePath].push(buildResult)
return acc
},
{} as Record<string, BuildResult[]>
)
}
}

View File

@ -0,0 +1,290 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as glob from '@actions/glob'
import path from 'path'
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, 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 class GradleUserHomeCache {
private readonly cacheName = 'home'
private readonly cacheDescription = 'Gradle User Home'
private readonly userHome: string
private readonly gradleUserHome: string
private readonly cacheConfig: CacheConfig
constructor(userHome: string, gradleUserHome: string, cacheConfig: CacheConfig) {
this.userHome = userHome
this.gradleUserHome = gradleUserHome
this.cacheConfig = cacheConfig
}
init(): void {
this.initializeGradleUserHome()
// Export the GRADLE_ENCRYPTION_KEY variable if provided
const encryptionKey = this.cacheConfig.getCacheEncryptionKey()
if (encryptionKey) {
core.exportVariable('GRADLE_ENCRYPTION_KEY', encryptionKey)
}
}
cacheOutputExists(): boolean {
const cachesDir = path.resolve(this.gradleUserHome, 'caches')
if (fs.existsSync(cachesDir)) {
cacheDebug(`Cache output exists at ${cachesDir}`)
return true
}
return false
}
/**
* Restores the cache entry, finding the closest match to the currently running job.
*/
async restore(listener: CacheListener): Promise<void> {
const entryListener = listener.entry(this.cacheDescription)
const cacheKey = generateCacheKey(this.cacheName, this.cacheConfig)
cacheDebug(
`Requesting ${this.cacheDescription} with
key:${cacheKey.key}
restoreKeys:[${cacheKey.restoreKeys}]`
)
const cachePath = this.getCachePath()
const cacheResult = await restoreCache(cachePath, cacheKey.key, cacheKey.restoreKeys, entryListener)
if (!cacheResult) {
core.info(`${this.cacheDescription} cache not found. Will initialize empty.`)
return
}
core.saveState(RESTORED_CACHE_KEY_KEY, cacheResult.key)
try {
await this.afterRestore(listener)
} catch (error) {
core.warning(`Restore ${this.cacheDescription} failed in 'afterRestore': ${error}`)
}
}
/**
* Restore any extracted cache entries after the main Gradle User Home entry is restored.
*/
async afterRestore(listener: CacheListener): Promise<void> {
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')
}
/**
* Saves the cache entry based on the current cache key unless the cache was restored with the exact key,
* in which case we cannot overwrite it.
*
* If the cache entry was restored with a partial match on a restore key, then
* it is saved with the exact key.
*/
async save(listener: CacheListener): Promise<void> {
const cacheKey = generateCacheKey(this.cacheName, this.cacheConfig).key
const restoredCacheKey = core.getState(RESTORED_CACHE_KEY_KEY)
const gradleHomeEntryListener = listener.entry(this.cacheDescription)
if (restoredCacheKey && cacheKey === restoredCacheKey) {
core.info(`Cache hit occurred on the cache key ${cacheKey}, not saving cache.`)
for (const entryListener of listener.cacheEntries) {
if (entryListener === gradleHomeEntryListener) {
entryListener.markNotSaved('cache key not changed')
} else {
entryListener.markNotSaved(`referencing '${this.cacheDescription}' cache entry not saved`)
}
}
return
}
try {
await this.beforeSave(listener)
} catch (error) {
core.warning(`Save ${this.cacheDescription} failed in 'beforeSave': ${error}`)
return
}
const cachePath = this.getCachePath()
await saveCache(cachePath, cacheKey, gradleHomeEntryListener)
return
}
/**
* Extract and save any defined extracted cache entries prior to the main Gradle User Home entry being saved.
*/
async beforeSave(listener: CacheListener): Promise<void> {
await this.debugReportGradleUserHomeSize('before saving common artifacts')
await this.deleteExcludedPaths()
await Promise.all([
new GradleHomeEntryExtractor(this.gradleUserHome, this.cacheConfig).extract(listener),
new ConfigurationCacheEntryExtractor(this.gradleUserHome, this.cacheConfig).extract(listener)
])
await this.debugReportGradleUserHomeSize(
"after extracting common artifacts (only 'caches' and 'notifications' will be stored)"
)
}
/**
* Delete any file paths that are excluded by the `gradle-home-cache-excludes` parameter.
*/
private async deleteExcludedPaths(): Promise<void> {
const rawPaths: string[] = this.cacheConfig.getCacheExcludes()
rawPaths.push('caches/*/cc-keystore')
const resolvedPaths = rawPaths.map(x => path.resolve(this.gradleUserHome, x))
for (const p of resolvedPaths) {
cacheDebug(`Removing excluded path: ${p}`)
const globber = await glob.create(p, {
implicitDescendants: false
})
for (const toDelete of await globber.glob()) {
cacheDebug(`Removing excluded file: ${toDelete}`)
await tryDelete(toDelete)
}
}
}
/**
* Determines the paths within Gradle User Home to cache.
* By default, this is the 'caches' and 'notifications' directories,
* but this can be overridden by the `gradle-home-cache-includes` parameter.
*/
protected getCachePath(): string[] {
const rawPaths: string[] = this.cacheConfig.getCacheIncludes()
rawPaths.push(ACTION_METADATA_DIR)
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
cacheDebug(`Using cache paths: ${resolvedPaths}`)
return resolvedPaths
}
private resolveCachePath(rawPath: string): string {
if (rawPath.startsWith('!')) {
const resolved = this.resolveCachePath(rawPath.substring(1))
return `!${resolved}`
}
return path.resolve(this.gradleUserHome, rawPath)
}
private initializeGradleUserHome(): void {
// Create a directory for storing action metadata
const actionCacheDir = path.resolve(this.gradleUserHome, ACTION_METADATA_DIR)
fs.mkdirSync(actionCacheDir, {recursive: true})
this.copyInitScripts()
// Copy the default toolchain definitions to `~/.m2/toolchains.xml`
this.registerToolchains()
if (core.isDebug()) {
this.configureInfoLogLevel()
}
}
private copyInitScripts(): void {
// Copy init scripts from src/resources to Gradle UserHome
const initScriptsDir = path.resolve(this.gradleUserHome, 'init.d')
fs.mkdirSync(initScriptsDir, {recursive: true})
const initScriptFilenames = [
'gradle-actions.build-result-capture.init.gradle',
'gradle-actions.build-result-capture-service.plugin.groovy',
'gradle-actions.github-dependency-graph.init.gradle',
'gradle-actions.github-dependency-graph-gradle-plugin-apply.groovy',
'gradle-actions.inject-develocity.init.gradle'
]
for (const initScriptFilename of initScriptFilenames) {
const initScriptContent = readResourceFileAsString('init-scripts', initScriptFilename)
const initScriptPath = path.resolve(initScriptsDir, initScriptFilename)
fs.writeFileSync(initScriptPath, initScriptContent)
}
}
private registerToolchains(): void {
const preInstalledToolchains: string | null = getPredefinedToolchains()
if (preInstalledToolchains == null) return
const m2dir = path.resolve(this.userHome, '.m2')
const toolchainXmlTarget = path.resolve(m2dir, 'toolchains.xml')
if (!fs.existsSync(toolchainXmlTarget)) {
// Write a new toolchains.xml file if it doesn't exist
fs.mkdirSync(m2dir, {recursive: true})
fs.writeFileSync(toolchainXmlTarget, preInstalledToolchains)
core.info(`Wrote default JDK locations to ${toolchainXmlTarget}`)
} else {
// Merge into an existing toolchains.xml file
const existingToolchainContent = fs.readFileSync(toolchainXmlTarget, 'utf8')
const mergedContent = mergeToolchainContent(existingToolchainContent, preInstalledToolchains)
fs.writeFileSync(toolchainXmlTarget, mergedContent)
core.info(`Merged default JDK locations into ${toolchainXmlTarget}`)
}
}
/**
* When the GitHub environment ACTIONS_RUNNER_DEBUG is true, run Gradle with --info and --stacktrace.
* see https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging
*
* @VisibleForTesting
*/
configureInfoLogLevel(): void {
const infoProperties = `org.gradle.logging.level=info\norg.gradle.logging.stacktrace=all\n`
const propertiesFile = path.resolve(this.gradleUserHome, 'gradle.properties')
if (fs.existsSync(propertiesFile)) {
core.info(`Merged --info and --stacktrace into existing ${propertiesFile} file`)
const existingProperties = fs.readFileSync(propertiesFile, 'utf-8')
fs.writeFileSync(propertiesFile, `${infoProperties}\n${existingProperties}`)
} else {
core.info(`Created a new ${propertiesFile} with --info and --stacktrace`)
fs.writeFileSync(propertiesFile, infoProperties)
}
}
/**
* When cache debugging is enabled (or ACTIONS_STEP_DEBUG is on),
* this method will give a detailed report of the Gradle User Home contents.
*/
private async debugReportGradleUserHomeSize(label: string): Promise<void> {
if (!isCacheDebuggingEnabled() && !core.isDebug()) {
return
}
if (!fs.existsSync(this.gradleUserHome)) {
return
}
const result = await exec.getExecOutput('du', ['-h', '-c', '-t', '5M'], {
cwd: this.gradleUserHome,
silent: true,
ignoreReturnCode: true
})
core.info(`Gradle User Home (directories >5M): ${label}`)
core.info(
result.stdout
.trimEnd()
.replace(/\t/g, ' ')
.split('\n')
.map(it => {
return ` ${it}`
})
.join('\n')
)
core.info('-----------------------')
}
}

View File

@ -0,0 +1,54 @@
import path from 'path'
import fs from 'fs'
import {fileURLToPath} from 'url'
export function readResourceFileAsString(...paths: string[]): string {
// Resolving relative to __dirname will allow node to find the resource at runtime
const moduleDir = path.dirname(fileURLToPath(import.meta.url))
const absolutePath = path.resolve(moduleDir, '..', '..', '..', 'sources', 'src', 'resources', ...paths)
return fs.readFileSync(absolutePath, 'utf8')
}
/**
* Iterate over all `JAVA_HOME_{version}_{arch}` envs and construct the toolchain.xml.
*
* @VisibleForTesting
*/
export function getPredefinedToolchains(): string | null {
// Get the version and path for each JAVA_HOME env var
const javaHomeEnvs = Object.entries(process.env)
.filter(([key]) => key.startsWith('JAVA_HOME_') && process.env[key])
.map(([key, value]) => ({
jdkVersion: key.match(/JAVA_HOME_(\d+)_/)?.[1] ?? null,
jdkPath: value as string
}))
.filter(env => env.jdkVersion !== null)
if (javaHomeEnvs.length === 0) {
return null
}
// language=XML
return `<?xml version="1.0" encoding="UTF-8"?>
<toolchains>
<!-- JDK Toolchains installed by default on GitHub-hosted runners -->
${javaHomeEnvs
.map(
({jdkVersion, jdkPath}) => ` <toolchain>
<type>jdk</type>
<provides>
<version>${jdkVersion}</version>
</provides>
<configuration>
<jdkHome>${jdkPath}</jdkHome>
</configuration>
</toolchain>`
)
.join('\n')}
</toolchains>\n`
}
export function mergeToolchainContent(existingToolchainContent: string, preInstalledToolchains: string): string {
const appendedContent = preInstalledToolchains.split('<toolchains>').pop()!
return existingToolchainContent.replace('</toolchains>', appendedContent)
}

View File

@ -132,23 +132,42 @@ export class CacheConfig {
return getBooleanInput('gradle-home-cache-strict-match') return getBooleanInput('gradle-home-cache-strict-match')
} }
getCacheCleanupOption(): string { isCacheCleanupEnabled(): boolean {
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') const legacyVal = getOptionalBooleanInput('gradle-home-cache-cleanup')
if (legacyVal !== undefined) { if (legacyVal !== undefined) {
deprecator.recordDeprecation( deprecator.recordDeprecation(
'The `gradle-home-cache-cleanup` input parameter has been replaced by `cache-cleanup`' 'The `gradle-home-cache-cleanup` input parameter has been replaced by `cache-cleanup`'
) )
return legacyVal ? CacheCleanupOption.Always.toString() : CacheCleanupOption.Never.toString() return legacyVal ? CacheCleanupOption.Always : CacheCleanupOption.Never
} }
const val = core.getInput('cache-cleanup') const val = core.getInput('cache-cleanup')
switch (val.toLowerCase().trim()) { switch (val.toLowerCase().trim()) {
case 'always': case 'always':
return CacheCleanupOption.Always.toString() return CacheCleanupOption.Always
case 'on-success': case 'on-success':
return CacheCleanupOption.OnSuccess.toString() return CacheCleanupOption.OnSuccess
case 'never': case 'never':
return CacheCleanupOption.Never.toString() return CacheCleanupOption.Never
} }
throw TypeError( throw TypeError(
`The value '${val}' is not valid for cache-cleanup. Valid values are: [never, always, on-success].` `The value '${val}' is not valid for cache-cleanup. Valid values are: [never, always, on-success].`
@ -166,11 +185,6 @@ export class CacheConfig {
getCacheExcludes(): string[] { getCacheExcludes(): string[] {
return core.getMultilineInput('gradle-home-cache-excludes') return core.getMultilineInput('gradle-home-cache-excludes')
} }
isCacheLicenseAccepted(): boolean {
const dvConfig = new DevelocityConfig()
return dvConfig.getDevelocityAccessKey() !== '' || dvConfig.hasTermsOfUseAgreement()
}
} }
export enum CacheCleanupOption { export enum CacheCleanupOption {
@ -234,7 +248,7 @@ export enum JobSummaryOption {
OnFailure = 'on-failure' OnFailure = 'on-failure'
} }
export class DevelocityConfig { export class BuildScanConfig {
static DevelocityAccessKeyEnvVar = 'DEVELOCITY_ACCESS_KEY' static DevelocityAccessKeyEnvVar = 'DEVELOCITY_ACCESS_KEY'
static GradleEnterpriseAccessKeyEnvVar = 'GRADLE_ENTERPRISE_ACCESS_KEY' static GradleEnterpriseAccessKeyEnvVar = 'GRADLE_ENTERPRISE_ACCESS_KEY'
@ -242,19 +256,19 @@ export class DevelocityConfig {
return getBooleanInput('build-scan-publish') && this.verifyTermsOfUseAgreement() return getBooleanInput('build-scan-publish') && this.verifyTermsOfUseAgreement()
} }
getTermsOfUseUrl(): string { getBuildScanTermsOfUseUrl(): string {
return core.getInput('build-scan-terms-of-use-url') return core.getInput('build-scan-terms-of-use-url')
} }
getTermsOfUseAgree(): string { getBuildScanTermsOfUseAgree(): string {
return core.getInput('build-scan-terms-of-use-agree') return core.getInput('build-scan-terms-of-use-agree')
} }
getDevelocityAccessKey(): string { getDevelocityAccessKey(): string {
return ( return (
core.getInput('develocity-access-key') || core.getInput('develocity-access-key') ||
process.env[DevelocityConfig.DevelocityAccessKeyEnvVar] || process.env[BuildScanConfig.DevelocityAccessKeyEnvVar] ||
process.env[DevelocityConfig.GradleEnterpriseAccessKeyEnvVar] || process.env[BuildScanConfig.GradleEnterpriseAccessKeyEnvVar] ||
'' ''
) )
} }
@ -295,17 +309,12 @@ export class DevelocityConfig {
return new PluginRepositoryConfig() return new PluginRepositoryConfig()
} }
hasTermsOfUseAgreement(): boolean {
const develocityAccessKeySet = this.getDevelocityAccessKey() !== ''
const termsUrlSet =
this.getTermsOfUseUrl() === 'https://gradle.com/terms-of-service' ||
this.getTermsOfUseUrl() === 'https://gradle.com/help/legal-terms-of-use'
const termsAgreed = this.getTermsOfUseAgree() === 'yes'
return develocityAccessKeySet || (termsUrlSet && termsAgreed)
}
private verifyTermsOfUseAgreement(): boolean { private verifyTermsOfUseAgreement(): boolean {
if (!this.hasTermsOfUseAgreement()) { if (
(this.getBuildScanTermsOfUseUrl() !== 'https://gradle.com/terms-of-service' &&
this.getBuildScanTermsOfUseUrl() !== 'https://gradle.com/help/legal-terms-of-use') ||
this.getBuildScanTermsOfUseAgree() !== 'yes'
) {
core.warning( core.warning(
`Terms of use at 'https://gradle.com/help/legal-terms-of-use' must be agreed in order to publish build scans.` `Terms of use at 'https://gradle.com/help/legal-terms-of-use' must be agreed in order to publish build scans.`
) )

View File

@ -0,0 +1,33 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as path from 'path'
import {BuildResults} from './build-results'
export class DaemonController {
private readonly gradleHomes
constructor(buildResults: BuildResults) {
this.gradleHomes = buildResults.uniqueGradleHomes()
}
async stopAllDaemons(): Promise<void> {
const executions: Promise<number>[] = []
const args = ['--stop']
for (const gradleHome of this.gradleHomes) {
const executable = path.resolve(gradleHome, 'bin', 'gradle')
if (!fs.existsSync(executable)) {
core.warning(`Gradle executable not found at ${executable}. Could not stop Gradle daemons.`)
continue
}
core.info(`Stopping Gradle daemons for ${gradleHome}`)
executions.push(
exec.exec(executable, args, {
ignoreReturnCode: true
})
)
}
await Promise.all(executions)
}
}

View File

@ -1,8 +1,8 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {DevelocityConfig} from '../configuration' import {BuildScanConfig} from '../configuration'
import {setupToken} from './short-lived-token' import {setupToken} from './short-lived-token'
export async function setup(config: DevelocityConfig): Promise<void> { export async function setup(config: BuildScanConfig): Promise<void> {
maybeExportVariable('DEVELOCITY_INJECTION_INIT_SCRIPT_NAME', 'gradle-actions.inject-develocity.init.gradle') maybeExportVariable('DEVELOCITY_INJECTION_INIT_SCRIPT_NAME', 'gradle-actions.inject-develocity.init.gradle')
maybeExportVariable('DEVELOCITY_INJECTION_CUSTOM_VALUE', 'gradle-actions') maybeExportVariable('DEVELOCITY_INJECTION_CUSTOM_VALUE', 'gradle-actions')
@ -36,8 +36,8 @@ export async function setup(config: DevelocityConfig): Promise<void> {
maybeExportVariable('DEVELOCITY_INJECTION_ENABLED', 'true') maybeExportVariable('DEVELOCITY_INJECTION_ENABLED', 'true')
maybeExportVariable('DEVELOCITY_INJECTION_DEVELOCITY_PLUGIN_VERSION', '4.3.2') maybeExportVariable('DEVELOCITY_INJECTION_DEVELOCITY_PLUGIN_VERSION', '4.3.2')
maybeExportVariable('DEVELOCITY_INJECTION_CCUD_PLUGIN_VERSION', '2.1') maybeExportVariable('DEVELOCITY_INJECTION_CCUD_PLUGIN_VERSION', '2.1')
maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_URL', config.getTermsOfUseUrl()) maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_URL', config.getBuildScanTermsOfUseUrl())
maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_AGREE', config.getTermsOfUseAgree()) maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_AGREE', config.getBuildScanTermsOfUseAgree())
} }
return setupToken( return setupToken(

View File

@ -1,6 +1,6 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as httpm from '@actions/http-client' import * as httpm from '@actions/http-client'
import {DevelocityConfig} from '../configuration' import {BuildScanConfig} from '../configuration'
import {recordDeprecation} from '../deprecation-collector' import {recordDeprecation} from '../deprecation-collector'
export async function setupToken( export async function setupToken(
@ -28,7 +28,7 @@ export async function setupToken(
} }
function exportAccessKeyEnvVars(value: string): void { function exportAccessKeyEnvVars(value: string): void {
;[DevelocityConfig.DevelocityAccessKeyEnvVar, DevelocityConfig.GradleEnterpriseAccessKeyEnvVar].forEach(key => ;[BuildScanConfig.DevelocityAccessKeyEnvVar, BuildScanConfig.GradleEnterpriseAccessKeyEnvVar].forEach(key =>
core.exportVariable(key, value) core.exportVariable(key, value)
) )
} }
@ -36,14 +36,12 @@ function exportAccessKeyEnvVars(value: string): void {
function handleMissingAccessToken(): void { function handleMissingAccessToken(): void {
core.warning(`Failed to fetch short-lived token for Develocity`) core.warning(`Failed to fetch short-lived token for Develocity`)
if (process.env[DevelocityConfig.GradleEnterpriseAccessKeyEnvVar]) { if (process.env[BuildScanConfig.GradleEnterpriseAccessKeyEnvVar]) {
// We do not clear the GRADLE_ENTERPRISE_ACCESS_KEY env var in v3, to let the users upgrade to DV 2024.1 // We do not clear the GRADLE_ENTERPRISE_ACCESS_KEY env var in v3, to let the users upgrade to DV 2024.1
recordDeprecation(`The ${DevelocityConfig.GradleEnterpriseAccessKeyEnvVar} env var is deprecated`) recordDeprecation(`The ${BuildScanConfig.GradleEnterpriseAccessKeyEnvVar} env var is deprecated`)
} }
if (process.env[DevelocityConfig.DevelocityAccessKeyEnvVar]) { if (process.env[BuildScanConfig.DevelocityAccessKeyEnvVar]) {
core.warning( core.warning(`The ${BuildScanConfig.DevelocityAccessKeyEnvVar} env var should be mapped to a short-lived token`)
`The ${DevelocityConfig.DevelocityAccessKeyEnvVar} env var should be mapped to a short-lived token`
)
} }
} }

View File

@ -6,8 +6,9 @@ import * as core from '@actions/core'
import * as cache from '@actions/cache' import * as cache from '@actions/cache'
import * as toolCache from '@actions/tool-cache' import * as toolCache from '@actions/tool-cache'
import {determineGradleVersion, findGradleExecutableOnPath} from './gradle' import {determineGradleVersion, findGradleExecutableOnPath, versionIsAtLeast} from './gradle'
import * as gradlew from './gradlew' import * as gradlew from './gradlew'
import {handleCacheFailure} from '../caching/cache-utils'
import {CacheConfig} from '../configuration' import {CacheConfig} from '../configuration'
const gradleVersionsBaseUrl = 'https://services.gradle.org/versions' const gradleVersionsBaseUrl = 'https://services.gradle.org/versions'
@ -108,6 +109,34 @@ async function installGradleVersion(versionInfo: GradleVersionInfo): Promise<str
}) })
} }
/**
* Find (or install) a Gradle executable that meets the specified version requirement.
* The Gradle version on PATH and all candidates are first checked for version compatibility.
* If no existing Gradle version meets the requirement, the required version is installed.
* @return Gradle executable with at least the required version.
*/
export async function provisionGradleWithVersionAtLeast(
minimumVersion: string,
candidates: string[] = []
): Promise<string> {
const gradleOnPath = await findGradleExecutableOnPath()
const allCandidates = gradleOnPath ? [gradleOnPath, ...candidates] : candidates
return core.group(`Provision Gradle >= ${minimumVersion}`, async () => {
for (const candidate of allCandidates) {
const candidateVersion = await determineGradleVersion(candidate)
if (candidateVersion && versionIsAtLeast(candidateVersion, minimumVersion)) {
core.info(
`Gradle version ${candidateVersion} is available at ${candidate} and >= ${minimumVersion}. Not installing.`
)
return candidate
}
}
return locateGradleAndDownloadIfRequired(await gradleRelease(minimumVersion))
})
}
async function locateGradleAndDownloadIfRequired(versionInfo: GradleVersionInfo): Promise<string> { async function locateGradleAndDownloadIfRequired(versionInfo: GradleVersionInfo): Promise<string> {
const installsDir = path.join(getProvisionDir(), 'installs') const installsDir = path.join(getProvisionDir(), 'installs')
const installDir = path.join(installsDir, `gradle-${versionInfo.version}`) const installDir = path.join(installsDir, `gradle-${versionInfo.version}`)
@ -193,20 +222,3 @@ interface GradleVersionInfo {
version: string version: string
downloadUrl: string downloadUrl: string
} }
function handleCacheFailure(error: unknown, message: string): void {
if (error instanceof cache.ValidationError) {
// Fail on cache validation errors
throw error
}
if (error instanceof cache.ReserveCacheError) {
// Reserve cache errors are expected if the artifact has been previously cached
core.info(`${message}: ${error}`)
} else {
// Warn on all other errors
core.warning(`${message}: ${error}`)
if (error instanceof Error && error.stack) {
core.info(error.stack)
}
}
}

View File

@ -1,14 +0,0 @@
const WINDOWS_EXIT_DELAY_MS = 50
export function getForcedExitDelayMs(platform: NodeJS.Platform = process.platform): number {
return platform === 'win32' ? WINDOWS_EXIT_DELAY_MS : 0
}
export async function forceExit(platform: NodeJS.Platform = process.platform): Promise<never> {
const exitDelayMs = getForcedExitDelayMs(platform)
if (exitDelayMs > 0) {
await new Promise(resolve => setTimeout(resolve, exitDelayMs))
}
return process.exit()
}

View File

@ -1,135 +0,0 @@
import * as core from '@actions/core'
import fs from 'fs'
import path from 'path'
import {ACTION_METADATA_DIR} from './configuration'
export function initializeGradleUserHome(userHome: string, gradleUserHome: string, encryptionKey?: string): void {
// Create a directory for storing action metadata
const actionCacheDir = path.resolve(gradleUserHome, ACTION_METADATA_DIR)
fs.mkdirSync(actionCacheDir, {recursive: true})
copyInitScripts(gradleUserHome)
// Copy the default toolchain definitions to `~/.m2/toolchains.xml`
registerToolchains(userHome)
if (core.isDebug()) {
configureInfoLogLevel(gradleUserHome)
}
if (encryptionKey) {
core.exportVariable('GRADLE_ENCRYPTION_KEY', encryptionKey)
}
}
function copyInitScripts(gradleUserHome: string): void {
// Copy init scripts from src/resources to Gradle UserHome
const initScriptsDir = path.resolve(gradleUserHome, 'init.d')
fs.mkdirSync(initScriptsDir, {recursive: true})
const initScriptFilenames = [
'gradle-actions.build-result-capture.init.gradle',
'gradle-actions.build-result-capture-service.plugin.groovy',
'gradle-actions.github-dependency-graph.init.gradle',
'gradle-actions.github-dependency-graph-gradle-plugin-apply.groovy',
'gradle-actions.inject-develocity.init.gradle'
]
for (const initScriptFilename of initScriptFilenames) {
const initScriptContent = readResourceFileAsString('init-scripts', initScriptFilename)
const initScriptPath = path.resolve(initScriptsDir, initScriptFilename)
fs.writeFileSync(initScriptPath, initScriptContent)
}
}
function registerToolchains(userHome: string): void {
const preInstalledToolchains: string | null = getPredefinedToolchains()
if (preInstalledToolchains == null) return
const m2dir = path.resolve(userHome, '.m2')
const toolchainXmlTarget = path.resolve(m2dir, 'toolchains.xml')
if (!fs.existsSync(toolchainXmlTarget)) {
// Write a new toolchains.xml file if it doesn't exist
fs.mkdirSync(m2dir, {recursive: true})
fs.writeFileSync(toolchainXmlTarget, preInstalledToolchains)
core.info(`Wrote default JDK locations to ${toolchainXmlTarget}`)
} else {
// Merge into an existing toolchains.xml file
const existingToolchainContent = fs.readFileSync(toolchainXmlTarget, 'utf8')
const mergedContent = mergeToolchainContent(existingToolchainContent, preInstalledToolchains)
fs.writeFileSync(toolchainXmlTarget, mergedContent)
core.info(`Merged default JDK locations into ${toolchainXmlTarget}`)
}
}
/**
* When the GitHub environment ACTIONS_RUNNER_DEBUG is true, run Gradle with --info and --stacktrace.
* see https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging
*
* @VisibleForTesting
*/
export function configureInfoLogLevel(gradleUserHome: string): void {
const infoProperties = `org.gradle.logging.level=info\norg.gradle.logging.stacktrace=all\n`
const propertiesFile = path.resolve(gradleUserHome, 'gradle.properties')
if (fs.existsSync(propertiesFile)) {
core.info(`Merged --info and --stacktrace into existing ${propertiesFile} file`)
const existingProperties = fs.readFileSync(propertiesFile, 'utf-8')
fs.writeFileSync(propertiesFile, `${infoProperties}\n${existingProperties}`)
} else {
core.info(`Created a new ${propertiesFile} with --info and --stacktrace`)
fs.writeFileSync(propertiesFile, infoProperties)
}
}
function readResourceFileAsString(...paths: string[]): string {
// Resolving relative to `dist/<action>/main/index.js` will allow node to find the resource at runtime
const moduleDir = import.meta.dirname
const absolutePath = path.resolve(moduleDir, '..', '..', '..', 'sources', 'src', 'resources', ...paths)
return fs.readFileSync(absolutePath, 'utf8')
}
/**
* Iterate over all `JAVA_HOME_{version}_{arch}` envs and construct the toolchain.xml.
*
* @VisibleForTesting
*/
export function getPredefinedToolchains(): string | null {
// Get the version and path for each JAVA_HOME env var
const javaHomeEnvs = Object.entries(process.env)
.filter(([key]) => key.startsWith('JAVA_HOME_') && process.env[key])
.map(([key, value]) => ({
jdkVersion: key.match(/JAVA_HOME_(\d+)_/)?.[1] ?? null,
jdkPath: value as string
}))
.filter(env => env.jdkVersion !== null)
if (javaHomeEnvs.length === 0) {
return null
}
// language=XML
return `<?xml version="1.0" encoding="UTF-8"?>
<toolchains>
<!-- JDK Toolchains installed by default on GitHub-hosted runners -->
${javaHomeEnvs
.map(
({jdkVersion, jdkPath}) => ` <toolchain>
<type>jdk</type>
<provides>
<version>${jdkVersion}</version>
</provides>
<configuration>
<jdkHome>${jdkPath}</jdkHome>
</configuration>
</toolchain>`
)
.join('\n')}
</toolchains>\n`
}
export function mergeToolchainContent(existingToolchainContent: string, preInstalledToolchains: string): string {
const appendedContent = preInstalledToolchains.split('<toolchains>').pop()!
return existingToolchainContent.replace('</toolchains>', appendedContent)
}

View File

@ -1,12 +1,12 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import {BuildResult} from './build-results' import {BuildResults, BuildResult} from './build-results'
import {SummaryConfig, getActionId, getGithubToken} from './configuration' import {SummaryConfig, getActionId, getGithubToken} from './configuration'
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector' import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
export async function generateJobSummary( export async function generateJobSummary(
buildResults: BuildResult[], buildResults: BuildResults,
cachingReport: string, cachingReport: string,
config: SummaryConfig config: SummaryConfig
): Promise<void> { ): Promise<void> {
@ -17,8 +17,9 @@ export async function generateJobSummary(
return return
} }
const summaryTable = renderSummaryTable(buildResults) const summaryTable = renderSummaryTable(buildResults.results)
const hasFailure = anyFailed(buildResults)
const hasFailure = buildResults.anyFailed()
if (config.shouldGenerateJobSummary(hasFailure)) { if (config.shouldGenerateJobSummary(hasFailure)) {
core.info('Generating Job Summary') core.info('Generating Job Summary')
@ -132,10 +133,6 @@ function renderBuildResults(results: BuildResult[]): string {
` `
} }
function anyFailed(results: BuildResult[]): boolean {
return results.some(result => result.buildFailed)
}
function renderBuildResultRow(result: BuildResult): string { function renderBuildResultRow(result: BuildResult): string {
return ` return `
<tr> <tr>

View File

@ -3,28 +3,30 @@ import * as exec from '@actions/exec'
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import * as os from 'os' import * as os from 'os'
import * as caches from './caching/caches'
import * as jobSummary from './job-summary' import * as jobSummary from './job-summary'
import * as buildScan from './develocity/build-scan' import * as buildScan from './develocity/build-scan'
import {loadBuildResults, markBuildResultsProcessed} from './build-results' import {loadBuildResults, markBuildResultsProcessed} from './build-results'
import {getCacheService} from './cache-service-loader' import {CacheListener, generateCachingReport} from './caching/cache-reporting'
import {CacheOptions} from './cache-service' import {DaemonController} from './daemon-controller'
import { import {
DevelocityConfig, BuildScanConfig,
CacheConfig, CacheConfig,
SummaryConfig, SummaryConfig,
WrapperValidationConfig, WrapperValidationConfig,
getWorkspaceDirectory getWorkspaceDirectory
} from './configuration' } from './configuration'
import * as wrapperValidator from './wrapper-validation/wrapper-validator' import * as wrapperValidator from './wrapper-validation/wrapper-validator'
import {initializeGradleUserHome} from './gradle-user-home'
const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED' const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED'
const USER_HOME = 'USER_HOME'
const GRADLE_USER_HOME = 'GRADLE_USER_HOME' const GRADLE_USER_HOME = 'GRADLE_USER_HOME'
const CACHE_LISTENER = 'CACHE_LISTENER'
export async function setup( export async function setup(
cacheConfig: CacheConfig, cacheConfig: CacheConfig,
develocityConfig: DevelocityConfig, buildScanConfig: BuildScanConfig,
wrapperValidationConfig: WrapperValidationConfig wrapperValidationConfig: WrapperValidationConfig
): Promise<boolean> { ): Promise<boolean> {
const userHome = await determineUserHome() const userHome = await determineUserHome()
@ -35,21 +37,23 @@ export async function setup(
core.info('Gradle setup only performed on first gradle/actions step in workflow.') core.info('Gradle setup only performed on first gradle/actions step in workflow.')
return false return false
} }
// Record setup complete: visible to subsequent actions and prevents duplicate setup // Record setup complete: visible to all subsequent actions and prevents duplicate setup
core.exportVariable(GRADLE_SETUP_VAR, true) core.exportVariable(GRADLE_SETUP_VAR, true)
// Record setup complete: visible in post-action, to control action completion // Record setup complete: visible in post-action, to control action completion
core.saveState(GRADLE_SETUP_VAR, true) core.saveState(GRADLE_SETUP_VAR, true)
// Save the Gradle User Home for use in the post-action step.
// Save the User Home and Gradle User Home for use in the post-action step.
core.saveState(USER_HOME, userHome)
core.saveState(GRADLE_USER_HOME, gradleUserHome) core.saveState(GRADLE_USER_HOME, gradleUserHome)
initializeGradleUserHome(userHome, gradleUserHome, cacheConfig.getCacheEncryptionKey()) const cacheListener = new CacheListener()
await caches.restore(userHome, gradleUserHome, cacheListener, cacheConfig)
const cacheService = await getCacheService(cacheConfig) core.saveState(CACHE_LISTENER, cacheListener.stringify())
await cacheService.restore(gradleUserHome, cacheOptionsFrom(cacheConfig))
await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome) await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome)
await buildScan.setup(develocityConfig) await buildScan.setup(buildScanConfig)
return true return true
} }
@ -63,9 +67,14 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
const buildResults = loadBuildResults() const buildResults = loadBuildResults()
const userHome = core.getState(USER_HOME)
const gradleUserHome = core.getState(GRADLE_USER_HOME) const gradleUserHome = core.getState(GRADLE_USER_HOME)
const cacheService = await getCacheService(cacheConfig) const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER))
const cachingReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
const daemonController = new DaemonController(buildResults)
await caches.save(userHome, gradleUserHome, cacheListener, daemonController, buildResults, cacheConfig)
const cachingReport = generateCachingReport(cacheListener)
await jobSummary.generateJobSummary(buildResults, cachingReport, summaryConfig) await jobSummary.generateJobSummary(buildResults, cachingReport, summaryConfig)
markBuildResultsProcessed() markBuildResultsProcessed()
@ -75,20 +84,6 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
return true return true
} }
function cacheOptionsFrom(config: CacheConfig): CacheOptions {
return {
disabled: config.isCacheDisabled(),
readOnly: config.isCacheReadOnly(),
writeOnly: config.isCacheWriteOnly(),
overwriteExisting: config.isCacheOverwriteExisting(),
strictMatch: config.isCacheStrictMatch(),
cleanup: config.getCacheCleanupOption(),
encryptionKey: config.getCacheEncryptionKey() || undefined,
includes: config.getCacheIncludes(),
excludes: config.getCacheExcludes()
}
}
async function determineGradleUserHome(): Promise<string> { async function determineGradleUserHome(): Promise<string> {
const customGradleUserHome = process.env['GRADLE_USER_HOME'] const customGradleUserHome = process.env['GRADLE_USER_HOME']
if (customGradleUserHome) { if (customGradleUserHome) {

View File

@ -1,32 +1,4 @@
[ [
{
"version": "9.4.1",
"checksum": "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"
},
{
"version": "9.5.0-milestone-7",
"checksum": "7ef3d73bd95c047814d76ec8324f72deefb96593eb9ce87aa06ecdcdaba7ffe8"
},
{
"version": "9.4.0",
"checksum": "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"
},
{
"version": "9.4.0-rc-2",
"checksum": "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"
},
{
"version": "9.5.0-milestone-6",
"checksum": "7ef3d73bd95c047814d76ec8324f72deefb96593eb9ce87aa06ecdcdaba7ffe8"
},
{
"version": "9.5.0-milestone-5",
"checksum": "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"
},
{
"version": "9.5.0-milestone-4",
"checksum": "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"
},
{ {
"version": "9.5.0-milestone-3", "version": "9.5.0-milestone-3",
"checksum": "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c" "checksum": "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"

View File

@ -20,7 +20,7 @@ dependencies {
testImplementation ('io.ratpack:ratpack-groovy-test:1.9.0') { testImplementation ('io.ratpack:ratpack-groovy-test:1.9.0') {
exclude group: 'org.codehaus.groovy', module: 'groovy-all' exclude group: 'org.codehaus.groovy', module: 'groovy-all'
} }
testImplementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.2' testImplementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.21.1'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }

View File

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.

View File

@ -0,0 +1,110 @@
import * as exec from '@actions/exec'
import * as glob from '@actions/glob'
import fs from 'fs'
import path from 'path'
import {expect, test, jest} from '@jest/globals'
import {CacheCleaner} from '../../src/caching/cache-cleaner'
jest.setTimeout(120000)
test('will cleanup unused dependency jars and build-cache entries', async () => {
const projectRoot = prepareTestProject()
const gradleUserHome = path.resolve(projectRoot, 'HOME')
const tmpDir = path.resolve(projectRoot, 'tmp')
const cacheCleaner = new CacheCleaner(gradleUserHome, tmpDir)
await runGradleBuild(projectRoot, 'build', '3.1')
const timestamp = await cacheCleaner.prepare()
await runGradleBuild(projectRoot, 'build', '3.1.1')
const commonsMath31 = path.resolve(gradleUserHome, "caches/modules-2/files-2.1/org.apache.commons/commons-math3/3.1")
const commonsMath311 = path.resolve(gradleUserHome, "caches/modules-2/files-2.1/org.apache.commons/commons-math3/3.1.1")
const buildCacheDir = path.resolve(gradleUserHome, "caches/build-cache-1")
expect(fs.existsSync(commonsMath31)).toBe(true)
expect(fs.existsSync(commonsMath311)).toBe(true)
expect(fs.readdirSync(buildCacheDir).length).toBe(4) // gc.properties, build-cache-1.lock, and 2 task entries
await cacheCleaner.forceCleanupFilesOlderThan(timestamp, 'gradle')
expect(fs.existsSync(commonsMath31)).toBe(false)
expect(fs.existsSync(commonsMath311)).toBe(true)
expect(fs.readdirSync(buildCacheDir).length).toBe(3) // 1 task entry has been cleaned up
})
test('will cleanup unused gradle versions', async () => {
const projectRoot = prepareTestProject()
const gradleUserHome = path.resolve(projectRoot, 'HOME')
const tmpDir = path.resolve(projectRoot, 'tmp')
const cacheCleaner = new CacheCleaner(gradleUserHome, tmpDir)
// Initialize HOME with 2 different Gradle versions
await runGradleWrapperBuild(projectRoot, 'build')
await runGradleBuild(projectRoot, 'build')
const timestamp = await cacheCleaner.prepare()
// Run with only one of these versions
await runGradleBuild(projectRoot, 'build')
const gradle802 = path.resolve(gradleUserHome, "caches/8.0.2")
const transforms3 = path.resolve(gradleUserHome, "caches/transforms-3")
const metadata100 = path.resolve(gradleUserHome, "caches/modules-2/metadata-2.100")
const wrapper802 = path.resolve(gradleUserHome, "wrapper/dists/gradle-8.0.2-bin")
const gradleCurrent = path.resolve(gradleUserHome, "caches/8.14.2")
const metadataCurrent = path.resolve(gradleUserHome, "caches/modules-2/metadata-2.107")
expect(fs.existsSync(gradle802)).toBe(true)
expect(fs.existsSync(transforms3)).toBe(true)
expect(fs.existsSync(metadata100)).toBe(true)
expect(fs.existsSync(wrapper802)).toBe(true)
expect(fs.existsSync(gradleCurrent)).toBe(true)
expect(fs.existsSync(metadataCurrent)).toBe(true)
// The wrapper won't be removed if it was recently downloaded. Age it.
setUtimes(wrapper802, new Date(Date.now() - 48 * 60 * 60 * 1000))
await cacheCleaner.forceCleanupFilesOlderThan(timestamp, 'gradle')
expect(fs.existsSync(gradle802)).toBe(false)
expect(fs.existsSync(transforms3)).toBe(false)
expect(fs.existsSync(metadata100)).toBe(false)
expect(fs.existsSync(wrapper802)).toBe(false)
expect(fs.existsSync(gradleCurrent)).toBe(true)
expect(fs.existsSync(metadataCurrent)).toBe(true)
})
async function runGradleBuild(projectRoot: string, args: string, version: string = '3.1'): Promise<void> {
await exec.exec(`gradle -g HOME --no-daemon --build-cache -Dcommons_math3_version="${version}" ${args}`, [], {
cwd: projectRoot
})
console.log(`Gradle User Home initialized with commons_math3_version=${version} ${args}`)
}
async function runGradleWrapperBuild(projectRoot: string, args: string, version: string = '3.1'): Promise<void> {
await exec.exec(`./gradlew -g HOME --no-daemon --build-cache -Dcommons_math3_version="${version}" ${args}`, [], {
cwd: projectRoot
})
console.log(`Gradle User Home initialized with commons_math3_version="${version}" ${args}`)
}
function prepareTestProject(): string {
const projectRoot = 'test/jest/resources/cache-cleanup'
fs.rmSync(path.resolve(projectRoot, 'HOME'), { recursive: true, force: true })
fs.rmSync(path.resolve(projectRoot, 'tmp'), { recursive: true, force: true })
fs.rmSync(path.resolve(projectRoot, 'build'), { recursive: true, force: true })
fs.rmSync(path.resolve(projectRoot, '.gradle'), { recursive: true, force: true })
return projectRoot
}
async function setUtimes(pattern: string, timestamp: Date): Promise<void> {
const globber = await glob.create(pattern)
for await (const file of globber.globGenerator()) {
fs.utimesSync(file, timestamp, timestamp)
}
}

View File

@ -2,7 +2,8 @@ import * as path from 'path'
import * as fs from 'fs' import * as fs from 'fs'
import {describe, expect, it} from '@jest/globals' import {describe, expect, it} from '@jest/globals'
import {configureInfoLogLevel} from '../../src/gradle-user-home' import {GradleUserHomeCache} from "../../src/caching/gradle-user-home-cache"
import {CacheConfig} from "../../src/configuration"
const testTmp = 'test/jest/tmp' const testTmp = 'test/jest/tmp'
fs.rmSync(testTmp, {recursive: true, force: true}) fs.rmSync(testTmp, {recursive: true, force: true})
@ -13,7 +14,8 @@ describe("--info and --stacktrace", () => {
const emptyGradleHome = `${testTmp}/empty-gradle-home` const emptyGradleHome = `${testTmp}/empty-gradle-home`
fs.mkdirSync(emptyGradleHome, {recursive: true}) fs.mkdirSync(emptyGradleHome, {recursive: true})
configureInfoLogLevel(emptyGradleHome) const stateCache = new GradleUserHomeCache("ignored", emptyGradleHome, new CacheConfig())
stateCache.configureInfoLogLevel()
expect(fs.readFileSync(path.resolve(emptyGradleHome, "gradle.properties"), 'utf-8')) expect(fs.readFileSync(path.resolve(emptyGradleHome, "gradle.properties"), 'utf-8'))
.toBe("org.gradle.logging.level=info\norg.gradle.logging.stacktrace=all\n") .toBe("org.gradle.logging.level=info\norg.gradle.logging.stacktrace=all\n")
@ -25,7 +27,8 @@ describe("--info and --stacktrace", () => {
fs.mkdirSync(existingGradleHome, {recursive: true}) fs.mkdirSync(existingGradleHome, {recursive: true})
fs.writeFileSync(path.resolve(existingGradleHome, "gradle.properties"), "org.gradle.logging.level=debug\n") fs.writeFileSync(path.resolve(existingGradleHome, "gradle.properties"), "org.gradle.logging.level=debug\n")
configureInfoLogLevel(existingGradleHome) const stateCache = new GradleUserHomeCache("ignored", existingGradleHome, new CacheConfig())
stateCache.configureInfoLogLevel()
expect(fs.readFileSync(path.resolve(existingGradleHome, "gradle.properties"), 'utf-8')) expect(fs.readFileSync(path.resolve(existingGradleHome, "gradle.properties"), 'utf-8'))
.toBe("org.gradle.logging.level=info\norg.gradle.logging.stacktrace=all\n\norg.gradle.logging.level=debug\n") .toBe("org.gradle.logging.level=info\norg.gradle.logging.stacktrace=all\n\norg.gradle.logging.level=debug\n")

View File

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

View File

@ -0,0 +1,22 @@
import {describe, expect, it} from '@jest/globals'
import * as cacheUtils from '../../src/caching/cache-utils'
describe('cacheUtils-utils', () => {
describe('can hash', () => {
it('a string', async () => {
const hash = cacheUtils.hashStrings(['foo'])
expect(hash).toBe('acbd18db4cc2f85cedef654fccc4a4d8')
})
it('multiple strings', async () => {
const hash = cacheUtils.hashStrings(['foo', 'bar', 'baz'])
expect(hash).toBe('6df23dc03f9b54cc38a0fc1483df6e21')
})
it('normalized filenames', async () => {
const fileNames = ['/foo/bar/baz.zip', '../boo.html']
const posixHash = cacheUtils.hashFileNames(fileNames)
const windowsHash = cacheUtils.hashFileNames(fileNames)
expect(posixHash).toBe(windowsHash)
})
})
})

View File

@ -1,39 +0,0 @@
import {afterEach, describe, expect, it, jest} from '@jest/globals'
import {forceExit, getForcedExitDelayMs} from '../../src/force-exit'
describe('forceExit', () => {
afterEach(() => {
jest.restoreAllMocks()
jest.useRealTimers()
})
it('adds a short delay on Windows before exiting', async () => {
jest.useFakeTimers()
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as never)
const exitPromise = forceExit('win32')
await jest.advanceTimersByTimeAsync(49)
expect(exitSpy).not.toHaveBeenCalled()
await jest.advanceTimersByTimeAsync(1)
await expect(exitPromise).resolves.toBeUndefined()
expect(exitSpy).toHaveBeenCalledTimes(1)
})
it('exits immediately on non-Windows platforms', async () => {
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as never)
await expect(forceExit('linux')).resolves.toBeUndefined()
expect(exitSpy).toHaveBeenCalledTimes(1)
})
it('only delays on Windows', () => {
expect(getForcedExitDelayMs('win32')).toBe(50)
expect(getForcedExitDelayMs('linux')).toBe(0)
expect(getForcedExitDelayMs('darwin')).toBe(0)
})
})

View File

@ -1,6 +1,6 @@
import {afterAll, describe, expect, it, jest} from '@jest/globals' import {afterAll, describe, expect, it, jest} from '@jest/globals'
import {getPredefinedToolchains, mergeToolchainContent} from '../../src/gradle-user-home' import {getPredefinedToolchains, mergeToolchainContent} from "../../src/caching/gradle-user-home-utils";
describe('predefined-toolchains', () => { describe('predefined-toolchains', () => {
const OLD_ENV = process.env const OLD_ENV = process.env

Some files were not shown because too many files have changed in this diff Show More