mirror of
https://github.com/gradle/actions.git
synced 2026-03-26 14:24:31 +08:00
Compare commits
No commits in common. "main" and "v5.0.2" have entirely different histories.
4
.github/actions/build-dist/action.yml
vendored
4
.github/actions/build-dist/action.yml
vendored
@ -3,7 +3,7 @@ name: 'Build and upload distribution'
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
@ -23,7 +23,7 @@ runs:
|
||||
cp -r sources/dist .
|
||||
|
||||
- name: Upload distribution
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
2
.github/actions/init-integ-test/action.yml
vendored
2
.github/actions/init-integ-test/action.yml
vendored
@ -23,7 +23,7 @@ runs:
|
||||
# Downloads a 'dist' directory artifact that was uploaded in an earlier 'build-dist' step
|
||||
- name: Download dist
|
||||
if: ${{ env.SKIP_DIST != 'true' && !env.ACT }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (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.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
2
.github/workflow-samples/groovy-dsl/gradlew
vendored
2
.github/workflow-samples/groovy-dsl/gradlew
vendored
@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (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.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (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.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
2
.github/workflow-samples/kotlin-dsl/gradlew
vendored
2
.github/workflow-samples/kotlin-dsl/gradlew
vendored
@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (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.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
4
.github/workflows/ci-check-and-unit-test.yml
vendored
4
.github/workflows/ci-check-and-unit-test.yml
vendored
@ -19,14 +19,14 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: sources/package-lock.json
|
||||
- name: Setup Gradle
|
||||
# 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:
|
||||
ALLOWED_GRADLE_WRAPPER_CHECKSUMS: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # Invalid wrapper jar used for testing
|
||||
with:
|
||||
|
||||
@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
|
||||
uses: tj-actions/changed-files@7dee1b0c1557f278e5c7dc244927139d78c0e22a # v47.0.4
|
||||
with:
|
||||
files: |
|
||||
dist/**
|
||||
|
||||
4
.github/workflows/ci-codeql.yml
vendored
4
.github/workflows/ci-codeql.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5
|
||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config: |
|
||||
@ -43,4 +43,4 @@ jobs:
|
||||
- sources/src
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v3.29.5
|
||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5
|
||||
|
||||
2
.github/workflows/ci-init-script-check.yml
vendored
2
.github/workflows/ci-init-script-check.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
java-version: 17
|
||||
- name: Setup Gradle
|
||||
# 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:
|
||||
ALLOWED_GRADLE_WRAPPER_CHECKSUMS: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # Invalid wrapper jar used for testing
|
||||
- name: Run integration tests
|
||||
|
||||
6
.github/workflows/ci-integ-test-full.yml
vendored
6
.github/workflows/ci-integ-test-full.yml
vendored
@ -5,10 +5,12 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'bot/gradle-actions-caching'
|
||||
paths:
|
||||
- 'dist/**'
|
||||
- 'sources/vendor/gradle-actions-caching/**'
|
||||
|
||||
concurrency:
|
||||
group: integ-test
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
10
.github/workflows/ci-integ-test.yml
vendored
10
.github/workflows/ci-integ-test.yml
vendored
@ -11,6 +11,10 @@ on:
|
||||
paths-ignore:
|
||||
- 'dist/**'
|
||||
|
||||
concurrency:
|
||||
group: integ-test
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@ -27,9 +31,6 @@ jobs:
|
||||
caching-integ-tests:
|
||||
needs: build-distribution
|
||||
uses: ./.github/workflows/suite-integ-test-caching.yml
|
||||
concurrency:
|
||||
group: CI-integ-test
|
||||
cancel-in-progress: false
|
||||
with:
|
||||
skip-dist: false
|
||||
secrets: inherit
|
||||
@ -39,9 +40,6 @@ jobs:
|
||||
contents: write
|
||||
needs: build-distribution
|
||||
uses: ./.github/workflows/suite-integ-test-other.yml
|
||||
concurrency:
|
||||
group: CI-integ-test
|
||||
cancel-in-progress: false
|
||||
with:
|
||||
skip-dist: false
|
||||
secrets: inherit
|
||||
|
||||
8
.github/workflows/ci-ossf-scorecard.yml
vendored
8
.github/workflows/ci-ossf-scorecard.yml
vendored
@ -44,8 +44,14 @@ jobs:
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: 'Upload artifact'
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
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
|
||||
|
||||
4
.github/workflows/ci-update-dist.yml
vendored
4
.github/workflows/ci-update-dist.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
cp -r sources/dist .
|
||||
|
||||
- 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:
|
||||
gpg_private_key: ${{ secrets.GH_BOT_PGP_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GH_BOT_PGP_PASSPHRASE }}
|
||||
|
||||
24
.github/workflows/ci-validate-typings.yml
vendored
24
.github/workflows/ci-validate-typings.yml
vendored
@ -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
|
||||
2
.github/workflows/ci-validate-wrappers.yml
vendored
2
.github/workflows/ci-validate-wrappers.yml
vendored
@ -12,6 +12,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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:
|
||||
allow-checksums: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
|
||||
52
.github/workflows/demo-job-summary.yml
vendored
52
.github/workflows/demo-job-summary.yml
vendored
@ -114,55 +114,3 @@ jobs:
|
||||
- name: Build kotlin-dsl project
|
||||
working-directory: .github/workflow-samples/kotlin-dsl
|
||||
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
|
||||
|
||||
@ -41,7 +41,7 @@ jobs:
|
||||
with:
|
||||
cache-read-only: false # For testing, allow writing cache entries on non-default branches
|
||||
- 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
|
||||
|
||||
# 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-cleanup: 'on-success'
|
||||
- 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
|
||||
|
||||
# Third build will restore cache entry from second, and verify stale content removed
|
||||
|
||||
@ -178,7 +178,7 @@ jobs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download dependency-graph artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
path: downloaded-dependency-graphs
|
||||
pattern: dependency-graph_*dependency-graph-generate-submit-and-upload.json
|
||||
|
||||
235
.github/workflows/integ-test-restore-configuration-cache.yml
vendored
Normal file
235
.github/workflows/integ-test-restore-configuration-cache.yml
vendored
Normal 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
|
||||
@ -111,12 +111,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github
|
||||
dist
|
||||
wrapper-validation
|
||||
|
||||
- name: Initialize integ-test
|
||||
uses: ./.github/actions/init-integ-test
|
||||
|
||||
@ -124,8 +118,10 @@ jobs:
|
||||
id: action-test
|
||||
uses: ./wrapper-validation
|
||||
with:
|
||||
min-wrapper-count: 7
|
||||
# There are only 6 wrappers in workflow-samples, so expected to fail
|
||||
# to allow the invalid wrapper jar present in test data
|
||||
allow-checksums: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
min-wrapper-count: 11
|
||||
# Expected to fail; validated below
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check outcome
|
||||
|
||||
@ -26,6 +26,14 @@ jobs:
|
||||
runner-os: '${{ inputs.runner-os }}'
|
||||
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:
|
||||
uses: ./.github/workflows/integ-test-restore-containerized-gradle-home.yml
|
||||
with:
|
||||
|
||||
4
.github/workflows/update-checksums-file.yml
vendored
4
.github/workflows/update-checksums-file.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
@ -38,7 +38,7 @@ jobs:
|
||||
working-directory: sources
|
||||
|
||||
- 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:
|
||||
gpg_private_key: ${{ secrets.GH_BOT_PGP_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GH_BOT_PGP_PASSPHRASE }}
|
||||
|
||||
28
CLAUDE.md
28
CLAUDE.md
@ -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
11
NOTICE
@ -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.
|
||||
28
README.md
28
README.md
@ -1,23 +1,9 @@
|
||||
# GitHub Actions for Gradle builds
|
||||
|
||||
[](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.
|
||||
|
||||
> [!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 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
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
@ -77,9 +63,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
@ -112,7 +98,7 @@ jobs:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/actions/wrapper-validation@v5
|
||||
```
|
||||
|
||||
|
||||
@ -20,9 +20,8 @@
|
||||
- Include a Full changelog link in the format https://github.com/gradle/actions/compare/v2.12.0...v3.0.0
|
||||
- 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.
|
||||
- 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.
|
||||
- Your HEAD must point at the commit to be tagged.
|
||||
|
||||
## Post release steps
|
||||
|
||||
|
||||
@ -22,9 +22,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
|
||||
@ -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
|
||||
519
dist/dependency-submission/main/index.js
vendored
519
dist/dependency-submission/main/index.js
vendored
File diff suppressed because one or more lines are too long
8
dist/dependency-submission/main/index.js.map
vendored
8
dist/dependency-submission/main/index.js.map
vendored
File diff suppressed because one or more lines are too long
489
dist/dependency-submission/post/index.js
vendored
489
dist/dependency-submission/post/index.js
vendored
File diff suppressed because one or more lines are too long
8
dist/dependency-submission/post/index.js.map
vendored
8
dist/dependency-submission/post/index.js.map
vendored
File diff suppressed because one or more lines are too long
517
dist/setup-gradle/main/index.js
vendored
517
dist/setup-gradle/main/index.js
vendored
File diff suppressed because one or more lines are too long
8
dist/setup-gradle/main/index.js.map
vendored
8
dist/setup-gradle/main/index.js.map
vendored
File diff suppressed because one or more lines are too long
564
dist/setup-gradle/post/index.js
vendored
564
dist/setup-gradle/post/index.js
vendored
File diff suppressed because one or more lines are too long
8
dist/setup-gradle/post/index.js.map
vendored
8
dist/setup-gradle/post/index.js.map
vendored
File diff suppressed because one or more lines are too long
353
dist/wrapper-validation/main/index.js
vendored
353
dist/wrapper-validation/main/index.js
vendored
File diff suppressed because one or more lines are too long
8
dist/wrapper-validation/main/index.js.map
vendored
8
dist/wrapper-validation/main/index.js.map
vendored
File diff suppressed because one or more lines are too long
@ -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.
|
||||
|
||||
> [!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
|
||||
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
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
|
||||
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
|
||||
|
||||
You can automatically publish a free Develocity Build Scan on every run of `gradle/actions/dependency-submission`.
|
||||
@ -366,8 +343,8 @@ jobs:
|
||||
dependency-submission:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -428,8 +405,8 @@ jobs:
|
||||
dependency-submission:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -466,7 +443,7 @@ jobs:
|
||||
# Gradle version compatibility
|
||||
|
||||
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.
|
||||
|
||||
@ -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
|
||||
- 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
|
||||
|
||||
|
||||
@ -2,25 +2,9 @@
|
||||
|
||||
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?
|
||||
|
||||
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:
|
||||
|
||||
@ -54,8 +38,8 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -106,8 +90,8 @@ jobs:
|
||||
gradle-rc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -228,8 +212,8 @@ jobs:
|
||||
gradle-with-configuration-cache:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -481,8 +465,8 @@ jobs:
|
||||
run-gradle-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -518,8 +502,8 @@ jobs:
|
||||
gradle:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -626,8 +610,8 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -678,8 +662,8 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
@ -708,8 +692,8 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
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-keys>
|
||||
```
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ jobs:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
- 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:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true # gradle-wrapper.jar verification will fail without this
|
||||
```
|
||||
|
||||
@ -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.
|
||||
@ -19,9 +19,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
|
||||
@ -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
1878
sources/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"description": "Execute Gradle Build",
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"prettier-write": "prettier --write 'src/**/*.ts'",
|
||||
"prettier-check": "prettier --check 'src/**/*.ts'",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
@ -36,7 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/artifact": "6.1.0",
|
||||
"@actions/cache": "6.0.0",
|
||||
"@actions/cache": "4.0.5",
|
||||
"@actions/core": "3.0.0",
|
||||
"@actions/exec": "3.0.0",
|
||||
"@actions/github": "9.0.0",
|
||||
@ -51,20 +52,21 @@
|
||||
"which": "6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "30.3.0",
|
||||
"@jest/globals": "30.2.0",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/node": "25.5.0",
|
||||
"@types/node": "25.3.0",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/unzipper": "0.10.11",
|
||||
"@types/which": "3.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
"dedent": "1.7.2",
|
||||
"esbuild": "0.27.4",
|
||||
"eslint": "10.0.3",
|
||||
"globals": "17.4.0",
|
||||
"jest": "30.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||
"dedent": "1.7.1",
|
||||
"esbuild": "0.27.3",
|
||||
"eslint": "10.0.1",
|
||||
"globals": "17.3.0",
|
||||
"jest": "30.2.0",
|
||||
"nock": "15.0.0",
|
||||
"npm-run-all": "4.1.5",
|
||||
"patch-package": "8.0.1",
|
||||
"prettier": "3.8.1",
|
||||
"ts-jest": "29.4.6",
|
||||
"typescript": "5.9.3"
|
||||
|
||||
248
sources/patches/@actions+cache+4.0.5.patch
Normal file
248
sources/patches/@actions+cache+4.0.5.patch
Normal 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
|
||||
@ -5,7 +5,7 @@ import * as dependencyGraph from '../../dependency-graph'
|
||||
|
||||
import {parseArgsStringToArgv} from 'string-argv'
|
||||
import {
|
||||
DevelocityConfig,
|
||||
BuildScanConfig,
|
||||
CacheConfig,
|
||||
DependencyGraphConfig,
|
||||
DependencyGraphOption,
|
||||
@ -15,7 +15,6 @@ import {
|
||||
} from '../../configuration'
|
||||
import {saveDeprecationState} from '../../deprecation-collector'
|
||||
import {handleMainActionError} from '../../errors'
|
||||
import {forceExit} from '../../force-exit'
|
||||
|
||||
/**
|
||||
* 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')
|
||||
|
||||
// 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
|
||||
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.
|
||||
await forceExit()
|
||||
process.exit()
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
@ -2,7 +2,6 @@ import * as setupGradle from '../../setup-gradle'
|
||||
|
||||
import {CacheConfig, SummaryConfig} from '../../configuration'
|
||||
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
|
||||
// @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.
|
||||
await forceExit()
|
||||
process.exit()
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
@ -2,7 +2,7 @@ import * as setupGradle from '../../setup-gradle'
|
||||
import * as provisioner from '../../execution/provision'
|
||||
import * as dependencyGraph from '../../dependency-graph'
|
||||
import {
|
||||
DevelocityConfig,
|
||||
BuildScanConfig,
|
||||
CacheConfig,
|
||||
DependencyGraphConfig,
|
||||
GradleExecutionConfig,
|
||||
@ -12,7 +12,6 @@ import {
|
||||
} from '../../configuration'
|
||||
import {failOnUseOfRemovedFeature, saveDeprecationState} from '../../deprecation-collector'
|
||||
import {handleMainActionError} from '../../errors'
|
||||
import {forceExit} from '../../force-exit'
|
||||
|
||||
/**
|
||||
* 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')
|
||||
|
||||
// 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
|
||||
await dependencyGraph.setup(new DependencyGraphConfig())
|
||||
@ -43,7 +42,7 @@ export async function run(): Promise<void> {
|
||||
}
|
||||
|
||||
// Explicit process.exit() to prevent waiting for hanging promises.
|
||||
await forceExit()
|
||||
process.exit()
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
@ -4,7 +4,6 @@ import * as dependencyGraph from '../../dependency-graph'
|
||||
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../../configuration'
|
||||
import {handlePostActionError} from '../../errors'
|
||||
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
|
||||
// @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.
|
||||
await forceExit()
|
||||
process.exit()
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {versionIsAtLeast} from './execution/gradle'
|
||||
|
||||
export interface BuildResult {
|
||||
get rootProjectName(): string
|
||||
@ -13,14 +14,47 @@ export interface BuildResult {
|
||||
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 content = fs.readFileSync(filePath, 'utf8')
|
||||
const buildResult = JSON.parse(content) as BuildResult
|
||||
addScanResults(filePath, buildResult)
|
||||
return buildResult
|
||||
})
|
||||
return results
|
||||
return new BuildResults(results)
|
||||
}
|
||||
|
||||
export function markBuildResultsProcessed(): void {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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>
|
||||
}
|
||||
124
sources/src/caching/cache-cleaner.ts
Normal file
124
sources/src/caching/cache-cleaner.ts
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
100
sources/src/caching/cache-key.ts
Normal file
100
sources/src/caching/cache-key.ts
Normal 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
|
||||
}
|
||||
294
sources/src/caching/cache-reporting.ts
Normal file
294
sources/src/caching/cache-reporting.ts
Normal 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`
|
||||
}
|
||||
140
sources/src/caching/cache-utils.ts
Normal file
140
sources/src/caching/cache-utils.ts
Normal 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
|
||||
}
|
||||
124
sources/src/caching/caches.ts
Normal file
124
sources/src/caching/caches.ts
Normal 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)}`)
|
||||
}
|
||||
}
|
||||
467
sources/src/caching/gradle-home-extry-extractor.ts
Normal file
467
sources/src/caching/gradle-home-extry-extractor.ts
Normal 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[]>
|
||||
)
|
||||
}
|
||||
}
|
||||
290
sources/src/caching/gradle-user-home-cache.ts
Normal file
290
sources/src/caching/gradle-user-home-cache.ts
Normal 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('-----------------------')
|
||||
}
|
||||
}
|
||||
54
sources/src/caching/gradle-user-home-utils.ts
Normal file
54
sources/src/caching/gradle-user-home-utils.ts
Normal 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)
|
||||
}
|
||||
@ -132,23 +132,42 @@ export class CacheConfig {
|
||||
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')
|
||||
if (legacyVal !== undefined) {
|
||||
deprecator.recordDeprecation(
|
||||
'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')
|
||||
switch (val.toLowerCase().trim()) {
|
||||
case 'always':
|
||||
return CacheCleanupOption.Always.toString()
|
||||
return CacheCleanupOption.Always
|
||||
case 'on-success':
|
||||
return CacheCleanupOption.OnSuccess.toString()
|
||||
return CacheCleanupOption.OnSuccess
|
||||
case 'never':
|
||||
return CacheCleanupOption.Never.toString()
|
||||
return CacheCleanupOption.Never
|
||||
}
|
||||
throw TypeError(
|
||||
`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[] {
|
||||
return core.getMultilineInput('gradle-home-cache-excludes')
|
||||
}
|
||||
|
||||
isCacheLicenseAccepted(): boolean {
|
||||
const dvConfig = new DevelocityConfig()
|
||||
return dvConfig.getDevelocityAccessKey() !== '' || dvConfig.hasTermsOfUseAgreement()
|
||||
}
|
||||
}
|
||||
|
||||
export enum CacheCleanupOption {
|
||||
@ -234,7 +248,7 @@ export enum JobSummaryOption {
|
||||
OnFailure = 'on-failure'
|
||||
}
|
||||
|
||||
export class DevelocityConfig {
|
||||
export class BuildScanConfig {
|
||||
static DevelocityAccessKeyEnvVar = 'DEVELOCITY_ACCESS_KEY'
|
||||
static GradleEnterpriseAccessKeyEnvVar = 'GRADLE_ENTERPRISE_ACCESS_KEY'
|
||||
|
||||
@ -242,19 +256,19 @@ export class DevelocityConfig {
|
||||
return getBooleanInput('build-scan-publish') && this.verifyTermsOfUseAgreement()
|
||||
}
|
||||
|
||||
getTermsOfUseUrl(): string {
|
||||
getBuildScanTermsOfUseUrl(): string {
|
||||
return core.getInput('build-scan-terms-of-use-url')
|
||||
}
|
||||
|
||||
getTermsOfUseAgree(): string {
|
||||
getBuildScanTermsOfUseAgree(): string {
|
||||
return core.getInput('build-scan-terms-of-use-agree')
|
||||
}
|
||||
|
||||
getDevelocityAccessKey(): string {
|
||||
return (
|
||||
core.getInput('develocity-access-key') ||
|
||||
process.env[DevelocityConfig.DevelocityAccessKeyEnvVar] ||
|
||||
process.env[DevelocityConfig.GradleEnterpriseAccessKeyEnvVar] ||
|
||||
process.env[BuildScanConfig.DevelocityAccessKeyEnvVar] ||
|
||||
process.env[BuildScanConfig.GradleEnterpriseAccessKeyEnvVar] ||
|
||||
''
|
||||
)
|
||||
}
|
||||
@ -295,17 +309,12 @@ export class DevelocityConfig {
|
||||
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 {
|
||||
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(
|
||||
`Terms of use at 'https://gradle.com/help/legal-terms-of-use' must be agreed in order to publish build scans.`
|
||||
)
|
||||
|
||||
33
sources/src/daemon-controller.ts
Normal file
33
sources/src/daemon-controller.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import * as core from '@actions/core'
|
||||
import {DevelocityConfig} from '../configuration'
|
||||
import {BuildScanConfig} from '../configuration'
|
||||
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_CUSTOM_VALUE', 'gradle-actions')
|
||||
|
||||
@ -36,8 +36,8 @@ export async function setup(config: DevelocityConfig): Promise<void> {
|
||||
maybeExportVariable('DEVELOCITY_INJECTION_ENABLED', 'true')
|
||||
maybeExportVariable('DEVELOCITY_INJECTION_DEVELOCITY_PLUGIN_VERSION', '4.3.2')
|
||||
maybeExportVariable('DEVELOCITY_INJECTION_CCUD_PLUGIN_VERSION', '2.1')
|
||||
maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_URL', config.getTermsOfUseUrl())
|
||||
maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_AGREE', config.getTermsOfUseAgree())
|
||||
maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_URL', config.getBuildScanTermsOfUseUrl())
|
||||
maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_AGREE', config.getBuildScanTermsOfUseAgree())
|
||||
}
|
||||
|
||||
return setupToken(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as httpm from '@actions/http-client'
|
||||
import {DevelocityConfig} from '../configuration'
|
||||
import {BuildScanConfig} from '../configuration'
|
||||
import {recordDeprecation} from '../deprecation-collector'
|
||||
|
||||
export async function setupToken(
|
||||
@ -28,7 +28,7 @@ export async function setupToken(
|
||||
}
|
||||
|
||||
function exportAccessKeyEnvVars(value: string): void {
|
||||
;[DevelocityConfig.DevelocityAccessKeyEnvVar, DevelocityConfig.GradleEnterpriseAccessKeyEnvVar].forEach(key =>
|
||||
;[BuildScanConfig.DevelocityAccessKeyEnvVar, BuildScanConfig.GradleEnterpriseAccessKeyEnvVar].forEach(key =>
|
||||
core.exportVariable(key, value)
|
||||
)
|
||||
}
|
||||
@ -36,14 +36,12 @@ function exportAccessKeyEnvVars(value: string): void {
|
||||
function handleMissingAccessToken(): void {
|
||||
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
|
||||
recordDeprecation(`The ${DevelocityConfig.GradleEnterpriseAccessKeyEnvVar} env var is deprecated`)
|
||||
recordDeprecation(`The ${BuildScanConfig.GradleEnterpriseAccessKeyEnvVar} env var is deprecated`)
|
||||
}
|
||||
if (process.env[DevelocityConfig.DevelocityAccessKeyEnvVar]) {
|
||||
core.warning(
|
||||
`The ${DevelocityConfig.DevelocityAccessKeyEnvVar} env var should be mapped to a short-lived token`
|
||||
)
|
||||
if (process.env[BuildScanConfig.DevelocityAccessKeyEnvVar]) {
|
||||
core.warning(`The ${BuildScanConfig.DevelocityAccessKeyEnvVar} env var should be mapped to a short-lived token`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,9 @@ import * as core from '@actions/core'
|
||||
import * as cache from '@actions/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 {handleCacheFailure} from '../caching/cache-utils'
|
||||
import {CacheConfig} from '../configuration'
|
||||
|
||||
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> {
|
||||
const installsDir = path.join(getProvisionDir(), 'installs')
|
||||
const installDir = path.join(installsDir, `gradle-${versionInfo.version}`)
|
||||
@ -193,20 +222,3 @@ interface GradleVersionInfo {
|
||||
version: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
|
||||
import {BuildResult} from './build-results'
|
||||
import {BuildResults, BuildResult} from './build-results'
|
||||
import {SummaryConfig, getActionId, getGithubToken} from './configuration'
|
||||
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
|
||||
|
||||
export async function generateJobSummary(
|
||||
buildResults: BuildResult[],
|
||||
buildResults: BuildResults,
|
||||
cachingReport: string,
|
||||
config: SummaryConfig
|
||||
): Promise<void> {
|
||||
@ -17,8 +17,9 @@ export async function generateJobSummary(
|
||||
return
|
||||
}
|
||||
|
||||
const summaryTable = renderSummaryTable(buildResults)
|
||||
const hasFailure = anyFailed(buildResults)
|
||||
const summaryTable = renderSummaryTable(buildResults.results)
|
||||
|
||||
const hasFailure = buildResults.anyFailed()
|
||||
if (config.shouldGenerateJobSummary(hasFailure)) {
|
||||
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 {
|
||||
return `
|
||||
<tr>
|
||||
|
||||
@ -3,28 +3,30 @@ import * as exec from '@actions/exec'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
import * as caches from './caching/caches'
|
||||
import * as jobSummary from './job-summary'
|
||||
import * as buildScan from './develocity/build-scan'
|
||||
|
||||
import {loadBuildResults, markBuildResultsProcessed} from './build-results'
|
||||
import {getCacheService} from './cache-service-loader'
|
||||
import {CacheOptions} from './cache-service'
|
||||
import {CacheListener, generateCachingReport} from './caching/cache-reporting'
|
||||
import {DaemonController} from './daemon-controller'
|
||||
import {
|
||||
DevelocityConfig,
|
||||
BuildScanConfig,
|
||||
CacheConfig,
|
||||
SummaryConfig,
|
||||
WrapperValidationConfig,
|
||||
getWorkspaceDirectory
|
||||
} from './configuration'
|
||||
import * as wrapperValidator from './wrapper-validation/wrapper-validator'
|
||||
import {initializeGradleUserHome} from './gradle-user-home'
|
||||
|
||||
const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED'
|
||||
const USER_HOME = 'USER_HOME'
|
||||
const GRADLE_USER_HOME = 'GRADLE_USER_HOME'
|
||||
const CACHE_LISTENER = 'CACHE_LISTENER'
|
||||
|
||||
export async function setup(
|
||||
cacheConfig: CacheConfig,
|
||||
develocityConfig: DevelocityConfig,
|
||||
buildScanConfig: BuildScanConfig,
|
||||
wrapperValidationConfig: WrapperValidationConfig
|
||||
): Promise<boolean> {
|
||||
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.')
|
||||
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)
|
||||
// Record setup complete: visible in post-action, to control action completion
|
||||
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)
|
||||
|
||||
initializeGradleUserHome(userHome, gradleUserHome, cacheConfig.getCacheEncryptionKey())
|
||||
const cacheListener = new CacheListener()
|
||||
await caches.restore(userHome, gradleUserHome, cacheListener, cacheConfig)
|
||||
|
||||
const cacheService = await getCacheService(cacheConfig)
|
||||
await cacheService.restore(gradleUserHome, cacheOptionsFrom(cacheConfig))
|
||||
core.saveState(CACHE_LISTENER, cacheListener.stringify())
|
||||
|
||||
await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome)
|
||||
|
||||
await buildScan.setup(develocityConfig)
|
||||
await buildScan.setup(buildScanConfig)
|
||||
|
||||
return true
|
||||
}
|
||||
@ -63,9 +67,14 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
|
||||
|
||||
const buildResults = loadBuildResults()
|
||||
|
||||
const userHome = core.getState(USER_HOME)
|
||||
const gradleUserHome = core.getState(GRADLE_USER_HOME)
|
||||
const cacheService = await getCacheService(cacheConfig)
|
||||
const cachingReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
|
||||
const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER))
|
||||
|
||||
const daemonController = new DaemonController(buildResults)
|
||||
await caches.save(userHome, gradleUserHome, cacheListener, daemonController, buildResults, cacheConfig)
|
||||
|
||||
const cachingReport = generateCachingReport(cacheListener)
|
||||
await jobSummary.generateJobSummary(buildResults, cachingReport, summaryConfig)
|
||||
|
||||
markBuildResultsProcessed()
|
||||
@ -75,20 +84,6 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
|
||||
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> {
|
||||
const customGradleUserHome = process.env['GRADLE_USER_HOME']
|
||||
if (customGradleUserHome) {
|
||||
|
||||
@ -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",
|
||||
"checksum": "55243ef57851f12b070ad14f7f5bb8302daceeebc5bce5ece5fa6edb23e1145c"
|
||||
|
||||
@ -20,7 +20,7 @@ dependencies {
|
||||
testImplementation ('io.ratpack:ratpack-groovy-test:1.9.0') {
|
||||
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'
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
|
||||
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
2
sources/test/init-scripts/gradlew
vendored
2
sources/test/init-scripts/gradlew
vendored
@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (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.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
110
sources/test/jest/cache-cleanup.test.ts
Normal file
110
sources/test/jest/cache-cleanup.test.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,8 @@ import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
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'
|
||||
fs.rmSync(testTmp, {recursive: true, force: true})
|
||||
@ -13,7 +14,8 @@ describe("--info and --stacktrace", () => {
|
||||
const emptyGradleHome = `${testTmp}/empty-gradle-home`
|
||||
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'))
|
||||
.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.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'))
|
||||
.toBe("org.gradle.logging.level=info\norg.gradle.logging.stacktrace=all\n\norg.gradle.logging.level=debug\n")
|
||||
99
sources/test/jest/cache-reporting.test.ts
Normal file
99
sources/test/jest/cache-reporting.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
22
sources/test/jest/cache-utils.test.ts
Normal file
22
sources/test/jest/cache-utils.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
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', () => {
|
||||
const OLD_ENV = process.env
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user