Add support for short-lived tokens (#224)

The setup-gradle action tries to get a short-lived access token given the supplied Develocity access key.
This key can be passed either with the `DEVELOCITY_ACCESS_KEY` env var or via the  `develocity-access-key` input parameter.
If a token can be retrieved, then the `DEVELOCITY_ACCESS_KEY` env var will be set to the token. 
Otherwise the `DEVELOCITY_ACCESS_KEY` will be set to a blank string, to avoid a leak.

---------

Co-authored-by: daz <daz@gradle.com>
This commit is contained in:
Alexis Tual 2024-05-16 00:49:55 +02:00 committed by GitHub
parent eb13cf7170
commit 500e0ee5b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 535 additions and 61 deletions

View File

@ -26,14 +26,19 @@ jobs:
DEVELOCITY_URL: https://ge.solutions-team.gradle.com DEVELOCITY_URL: https://ge.solutions-team.gradle.com
DEVELOCITY_PLUGIN_VERSION: ${{ matrix.plugin-version }} DEVELOCITY_PLUGIN_VERSION: ${{ matrix.plugin-version }}
DEVELOCITY_CCUD_PLUGIN_VERSION: '2.0' DEVELOCITY_CCUD_PLUGIN_VERSION: '2.0'
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} # required to test against GE plugin 3.16.2 ${{matrix.accessKeyEnv}}: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
gradle: [current, 7.6.2, 6.9.4, 5.6.4] gradle: [current, 7.6.2, 6.9.4, 5.6.4]
os: ${{fromJSON(inputs.runner-os)}} os: ${{fromJSON(inputs.runner-os)}}
plugin-version: [3.16.2, 3.17.3] plugin-version: [3.16.2, 3.17.3]
include:
- plugin-version: 3.16.2
accessKeyEnv: GRADLE_ENTERPRISE_ACCESS_KEY
- plugin-version: 3.17.3
accessKeyEnv: DEVELOCITY_ACCESS_KEY
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout sources - name: Checkout sources
@ -61,4 +66,83 @@ jobs:
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
script: | script: |
core.setFailed('No Build Scan detected') core.setFailed('No Build Scan detected')
- name: Check short lived token
if: ${{ matrix.plugin-version == '3.17.3' }}
run: "[ ${#DEVELOCITY_ACCESS_KEY} -gt 500 ] || (echo 'DEVELOCITY_ACCESS_KEY does not look like a short lived token'; exit 1)"
inject-develocity-with-access-key:
env:
DEVELOCITY_INJECTION_ENABLED: true
DEVELOCITY_URL: 'https://ge.solutions-team.gradle.com'
DEVELOCITY_PLUGIN_VERSION: 3.17.3
DEVELOCITY_CCUD_PLUGIN_VERSION: '2.0'
strategy:
fail-fast: false
matrix:
gradle: [current, 7.6.2, 6.9.4, 5.6.4]
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 8
- name: Setup Gradle
id: setup-gradle
uses: ./setup-gradle
with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
gradle-version: ${{ matrix.gradle }}
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
develocity-token-expiry: 1
- name: Run Gradle build
id: gradle
working-directory: .github/workflow-samples/no-ge
run: gradle help
- name: Check short lived token
run: "[ ${#DEVELOCITY_ACCESS_KEY} -gt 500 ] || (echo 'DEVELOCITY_ACCESS_KEY does not look like a short lived token'; exit 1)"
- name: Check Build Scan url
if: ${{ !steps.gradle.outputs.build-scan-url }}
uses: actions/github-script@v7
with:
script: |
core.setFailed('No Build Scan detected')
inject-develocity-short-lived-token-failed:
env:
DEVELOCITY_INJECTION_ENABLED: true
DEVELOCITY_URL: 'https://localhost:3333/'
DEVELOCITY_PLUGIN_VERSION: 3.17.3
DEVELOCITY_CCUD_PLUGIN_VERSION: '2.0'
# Access key also set as an env var, we want to check it does not leak
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Initialize integ-test
uses: ./.github/actions/init-integ-test
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 8
- name: Setup Gradle
id: setup-gradle
uses: ./setup-gradle
with:
cache-read-only: false # For testing, allow writing cache entries on non-default branches
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
- name: Run Gradle build
id: gradle
working-directory: .github/workflow-samples/no-ge
run: gradle help
- name: Check access key is blank
run: "[ \"${DEVELOCITY_ACCESS_KEY}\" == \"\" ] || (echo 'DEVELOCITY_ACCESS_KEY has leaked!'; exit 1)"

8
build
View File

@ -16,12 +16,18 @@ case "$1" in
# Run act # Run act
$@ $@
# Revert the changes to the dist directory # Revert the changes to the dist directory
git co -- dist git checkout -- dist
;; ;;
init-scripts) init-scripts)
cd test/init-scripts cd test/init-scripts
./gradlew check ./gradlew check
;; ;;
dist)
npm install
npm run build
cd ..
cp -r sources/dist .
;;
*) *)
npm install npm install
npm run build npm run build

View File

@ -15,13 +15,13 @@ inputs:
dependency-resolution-task: dependency-resolution-task:
description: | description: |
Task(s) that should be executed in order to resolve all project dependencies. Task(s) that should be executed in order to resolve all project dependencies.
By default, the built-in `:ForceDependencyResolutionPlugin_resolveAllDependencies` task is executed. By default, the built-in `:ForceDependencyResolutionPlugin_resolveAllDependencies` task is executed.
required: false required: false
additional-arguments: additional-arguments:
description: | description: |
Additional arguments to pass to Gradle when generating the dependency graph. Additional arguments to pass to Gradle when generating the dependency graph.
For example, `--no-configuration-cache --stacktrace`. For example, `--no-configuration-cache --stacktrace`.
required: false required: false
@ -40,7 +40,7 @@ inputs:
cache-write-only: cache-write-only:
description: | description: |
When 'true', entries will not be restored from the cache but will be saved at the end of the Job. When 'true', entries will not be restored from the cache but will be saved at the end of the Job.
Setting this to 'true' implies cache-read-only will be 'false'. Setting this to 'true' implies cache-read-only will be 'false'.
required: false required: false
default: false default: false
@ -52,7 +52,7 @@ inputs:
cache-encryption-key: cache-encryption-key:
description: | description: |
A base64 encoded AES key used to encrypt the configuration-cache data. The key is exported as 'GRADLE_ENCRYPTION_KEY' for later steps. A base64 encoded AES key used to encrypt the configuration-cache data. The key is exported as 'GRADLE_ENCRYPTION_KEY' for later steps.
A suitable key can be generated with `openssl rand -base64 16`. A suitable key can be generated with `openssl rand -base64 16`.
Configuration-cache data will not be saved/restored without an encryption key being provided. Configuration-cache data will not be saved/restored without an encryption key being provided.
required: false required: false
@ -92,7 +92,7 @@ inputs:
'generate-and-submit' (default): Generates a dependency graph for the project and submits it in the same Job. 'generate-and-submit' (default): Generates a dependency graph for the project and submits it in the same Job.
'generate-and-upload': Generates a dependency graph for the project and saves it as a workflow artifact. 'generate-and-upload': Generates a dependency graph for the project and saves it as a workflow artifact.
'download-and-submit': Retrieves a previously saved dependency-graph and submits it to the repository. 'download-and-submit': Retrieves a previously saved dependency-graph and submits it to the repository.
The `generate-and-upload` and `download-and-submit` options are designed to be used in an untrusted workflow scenario, The `generate-and-upload` and `download-and-submit` options are designed to be used in an untrusted workflow scenario,
where the workflow generating the dependency-graph cannot (or should not) be given the `contents: write` permissions where the workflow generating the dependency-graph cannot (or should not) be given the `contents: write` permissions
required to submit via the Dependency Submission API. required to submit via the Dependency Submission API.
@ -120,11 +120,19 @@ inputs:
build-scan-terms-of-use-url: build-scan-terms-of-use-url:
description: The URL to the Build Scan® terms of use. This input must be set to 'https://gradle.com/terms-of-service' or 'https://gradle.com/help/legal-terms-of-use'. description: The URL to the Build Scan® terms of use. This input must be set to 'https://gradle.com/terms-of-service' or 'https://gradle.com/help/legal-terms-of-use'.
required: false required: false
build-scan-terms-of-use-agree: build-scan-terms-of-use-agree:
description: Indicate that you agree to the Build Scan® terms of use. This input value must be "yes". description: Indicate that you agree to the Build Scan® terms of use. This input value must be "yes".
required: false required: false
develocity-access-key:
description: Develocity access key. Should be set to a secret containing the Develocity Access key.
required: false
develocity-token-expiry:
description: The Develocity short-lived access tokens expiry in hours. Default is 2 hours.
required: false
# DEPRECATED ACTION INPUTS # DEPRECATED ACTION INPUTS
build-scan-terms-of-service-url: build-scan-terms-of-service-url:
description: The URL to the Build Scan® terms of use. This input must be set to 'https://gradle.com/terms-of-service'. description: The URL to the Build Scan® terms of use. This input must be set to 'https://gradle.com/terms-of-service'.
@ -150,7 +158,7 @@ inputs:
description: When 'true', the action will not attempt to restore the Gradle User Home entries from other Jobs. description: When 'true', the action will not attempt to restore the Gradle User Home entries from other Jobs.
required: false required: false
default: false default: false
# INTERNAL ACTION INPUTS # INTERNAL ACTION INPUTS
# These inputs should not be configured directly, and are only used to pass environmental information to the action # These inputs should not be configured directly, and are only used to pass environmental information to the action
workflow-job-context: workflow-job-context:

View File

@ -4,22 +4,22 @@ This GitHub Action can be used to configure Gradle for optimal execution on any
## Why use the `setup-gradle` action? ## Why use the `setup-gradle` action?
It is possible to directly invoke Gradle in your workflow, and the `actions/setup-java@v4` action provides a simple way to cache Gradle dependencies. It is possible to directly invoke Gradle in your workflow, and the `actions/setup-java@v4` action provides a simple way to cache Gradle dependencies.
However, the `setup-gradle` action offers a several advantages over this approach: However, the `setup-gradle` action offers a several advantages over this approach:
- Easily [configure your workflow to use a specific version of Gradle](#build-with-a-specific-gradle-version) using the `gradle-version` parameter. Gradle distributions are automatically downloaded and cached. - Easily [configure your workflow to use a specific version of Gradle](#build-with-a-specific-gradle-version) using the `gradle-version` parameter. Gradle distributions are automatically downloaded and cached.
- More sophisticated and more efficient caching of Gradle User Home between invocations, compared to `setup-java` and most custom configurations using `actions/cache`. [More details below](#caching-build-state-between-jobs). - More sophisticated and more efficient caching of Gradle User Home between invocations, compared to `setup-java` and most custom configurations using `actions/cache`. [More details below](#caching-build-state-between-jobs).
- Detailed reporting of cache usage and cache configuration options allow you to [optimize the use of the GitHub actions cache](#optimizing-cache-effectiveness). - Detailed reporting of cache usage and cache configuration options allow you to [optimize the use of the GitHub actions cache](#optimizing-cache-effectiveness).
- [Generate and Submit a GitHub Dependency Graph](#github-dependency-graph-support) for your project, enabling Dependabot security alerts. - [Generate and Submit a GitHub Dependency Graph](#github-dependency-graph-support) for your project, enabling Dependabot security alerts.
- [Automatic capture of Build Scan® links](#build-reporting) from the build, making them easier to locate in workflow runs. - [Automatic capture of Build Scan® links](#build-reporting) from the build, making them easier to locate in workflow runs.
The `setup-gradle` action is designed to provide these benefits with minimal configuration. The `setup-gradle` action is designed to provide these benefits with minimal configuration.
These features work both when Gradle is executed via `setup-gradle` and for any Gradle execution in subsequent steps. These features work both when Gradle is executed via `setup-gradle` and for any Gradle execution in subsequent steps.
## General usage ## General usage
The `setup-gradle` action works by configuring environment variables and by adding a set of Gradle init-scripts to the Gradle User Home. These will apply to all Gradle executions on the runner, no matter how Gradle is invoked. The `setup-gradle` action works by configuring environment variables and by adding a set of Gradle init-scripts to the Gradle User Home. These will apply to all Gradle executions on the runner, no matter how Gradle is invoked.
This means that if you have an existing workflow that executes Gradle with a `run` step, you can add an initial "Setup Gradle" Step to benefit from caching, build-scan capture, and other features of this action. This means that if you have an existing workflow that executes Gradle with a `run` step, you can add an initial "Setup Gradle" Step to benefit from caching, build-scan capture, and other features of this action.
The recommended way to execute any Gradle build is with the help of the [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html), and the following examples assume that the Gradle Wrapper has been configured for the project. See [this example](#build-with-a-specific-gradle-version) if your project doesn't use the Gradle Wrapper. The recommended way to execute any Gradle build is with the help of the [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html), and the following examples assume that the Gradle Wrapper has been configured for the project. See [this example](#build-with-a-specific-gradle-version) if your project doesn't use the Gradle Wrapper.
@ -46,7 +46,7 @@ jobs:
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v3 uses: gradle/actions/setup-gradle@v3
- name: Execute Gradle build - name: Execute Gradle build
run: ./gradlew build run: ./gradlew build
``` ```
@ -126,7 +126,7 @@ cache-disabled: true
### Using the cache read-only ### Using the cache read-only
By default, The `setup-gradle` action will only write to the cache from Jobs on the default (`main`/`master`) branch. By default, The `setup-gradle` action will only write to the cache from Jobs on the default (`main`/`master`) branch.
Jobs on other branches will read entries from the cache but will not write updated entries. Jobs on other branches will read entries from the cache but will not write updated entries.
See [Optimizing cache effectiveness](#select-which-branches-should-write-to-the-cache) for a more detailed explanation. See [Optimizing cache effectiveness](#select-which-branches-should-write-to-the-cache) for a more detailed explanation.
In some circumstances, it makes sense to change this default and configure a workflow Job to read existing cache entries but not to write changes back. In some circumstances, it makes sense to change this default and configure a workflow Job to read existing cache entries but not to write changes back.
@ -158,7 +158,7 @@ cache-write-only: true
When the action detects that the Gradle User Home caches directory already exists (`~/.gradle/caches`), then by default it will not overwrite the existing content of this directory. When the action detects that the Gradle User Home caches directory already exists (`~/.gradle/caches`), then by default it will not overwrite the existing content of this directory.
This can occur when a prior action initializes this directory, or when using a self-hosted runner that retains this directory between uses. This can occur when a prior action initializes this directory, or when using a self-hosted runner that retains this directory between uses.
In this case, the Job Summary will display a message like: In this case, the Job Summary will display a message like:
> Caching for Gradle actions was disabled due to pre-existing Gradle User Home > Caching for Gradle actions was disabled due to pre-existing Gradle User Home
If you want to override the default and have the caches of the `setup-gradle` action overwrite existing content in the Gradle User Home, you can set the `cache-overwrite-existing` parameter to `true`: If you want to override the default and have the caches of the `setup-gradle` action overwrite existing content in the Gradle User Home, you can set the `cache-overwrite-existing` parameter to `true`:
@ -204,7 +204,7 @@ jobs:
### Incompatibility with other caching mechanisms ### Incompatibility with other caching mechanisms
When using `setup-gradle` we recommend that you avoid using other mechanisms to save and restore the Gradle User Home. When using `setup-gradle` we recommend that you avoid using other mechanisms to save and restore the Gradle User Home.
Specifically: Specifically:
- Avoid using `actions/cache` configured to cache the Gradle User Home, [as described in this example](https://github.com/actions/cache/blob/main/examples.md#java---gradle). - Avoid using `actions/cache` configured to cache the Gradle User Home, [as described in this example](https://github.com/actions/cache/blob/main/examples.md#java---gradle).
@ -256,7 +256,7 @@ The Gradle User Home cache key is composed of:
Specifically, the cache key is: `${cache-protocol}-gradle|${runner-os}|${job-id}[${hash-of-job-matrix-and-workflow-name}]-${git-sha}` Specifically, the cache key is: `${cache-protocol}-gradle|${runner-os}|${job-id}[${hash-of-job-matrix-and-workflow-name}]-${git-sha}`
As such, the cache key is likely to change on each subsequent run of GitHub actions. As such, the cache key is likely to change on each subsequent run of GitHub actions.
This allows the most recent state to always be available in the GitHub actions cache. This allows the most recent state to always be available in the GitHub actions cache.
### Finding a matching cache entry ### Finding a matching cache entry
@ -271,7 +271,7 @@ Due to branch scoping of cache entries, the above match will be first performed
After the Job is complete, the current Gradle User Home state will be collected and written as a new cache entry with the complete cache key. Old entries will be expunged from the GitHub Actions cache on a least recently used basis. After the Job is complete, the current Gradle User Home state will be collected and written as a new cache entry with the complete cache key. Old entries will be expunged from the GitHub Actions cache on a least recently used basis.
Note that while effective, this mechanism is not inherently efficient. It requires the entire Gradle User Home directory to be stored separately for each branch, for every OS+Job+Matrix combination. In addition, it writes a new cache entry on every GitHub Actions run. Note that while effective, this mechanism is not inherently efficient. It requires the entire Gradle User Home directory to be stored separately for each branch, for every OS+Job+Matrix combination. In addition, it writes a new cache entry on every GitHub Actions run.
This inefficiency is effectively mitigated by [Deduplication of Gradle User Home cache entries](#deduplication-of-gradle-user-home-cache-entries) and can be further optimized for a workflow using the techniques described in [Optimizing cache effectiveness](#optimizing-cache-effectiveness). This inefficiency is effectively mitigated by [Deduplication of Gradle User Home cache entries](#deduplication-of-gradle-user-home-cache-entries) and can be further optimized for a workflow using the techniques described in [Optimizing cache effectiveness](#optimizing-cache-effectiveness).
@ -289,7 +289,7 @@ For example, this means that all jobs executing a particular version of the Grad
### Stopping the Gradle daemon ### Stopping the Gradle daemon
By default, the action will stop all running Gradle daemons in the post-action step, before saving the Gradle User Home state. By default, the action will stop all running Gradle daemons in the post-action step, before saving the Gradle User Home state.
This allows for any Gradle User Home cleanup to occur, and avoid file-locking issues on Windows. This allows for any Gradle User Home cleanup to occur, and avoid file-locking issues on Windows.
If caching is disabled or the cache is in read-only mode, the daemon will not be stopped and will continue running after the job is completed. If caching is disabled or the cache is in read-only mode, the daemon will not be stopped and will continue running after the job is completed.
@ -316,23 +316,23 @@ Some techniques can be used to avoid/mitigate this issue:
### Select which branches should write to the cache ### Select which branches should write to the cache
GitHub cache entries are not shared between builds on different branches. GitHub cache entries are not shared between builds on different branches.
Workflow runs can restore caches created in either the current branch or the default branch (usually main). Workflow runs can restore caches created in either the current branch or the default branch (usually main).
This means that each branch will have its own Gradle User Home cache scope, and will not benefit from cache entries written for other (non-default) branches. This means that each branch will have its own Gradle User Home cache scope, and will not benefit from cache entries written for other (non-default) branches.
By default, The `setup-gradle` action will only _write_ to the cache for builds run on the default (`master`/`main`) branch. By default, The `setup-gradle` action will only _write_ to the cache for builds run on the default (`master`/`main`) branch.
Jobs running on other branches will only read from the cache. In most cases, this is the desired behavior. Jobs running on other branches will only read from the cache. In most cases, this is the desired behavior.
This is because Jobs running on other branches will benefit from the cached Gradle User Home from `main`, This is because Jobs running on other branches will benefit from the cached Gradle User Home from `main`,
without writing private cache entries which could lead to evicting these shared entries. without writing private cache entries which could lead to evicting these shared entries.
If you have other long-lived development branches that would benefit from writing to the cache, If you have other long-lived development branches that would benefit from writing to the cache,
you can configure this by disabling the `cache-read-only` action parameter for these branches. you can configure this by disabling the `cache-read-only` action parameter for these branches.
See [Using the cache read-only](#using-the-cache-read-only) for more details. See [Using the cache read-only](#using-the-cache-read-only) for more details.
Note there are some cases where writing cache entries is typically unhelpful (these are disabled by default): Note there are some cases where writing cache entries is typically unhelpful (these are disabled by default):
- For `pull_request` triggered runs, the cache scope is limited to the merge ref (`refs/pull/.../merge`) and can only be restored by re-runs of the same pull request. - For `pull_request` triggered runs, the cache scope is limited to the merge ref (`refs/pull/.../merge`) and can only be restored by re-runs of the same pull request.
- For `merge_group` triggered runs, the cache scope is limited to a temporary branch with a special prefix created to validate pull request changes, and won't be available on subsequent Merge Queue executions. - For `merge_group` triggered runs, the cache scope is limited to a temporary branch with a special prefix created to validate pull request changes, and won't be available on subsequent Merge Queue executions.
### Exclude content from Gradle User Home cache ### Exclude content from Gradle User Home cache
As well as any wrapper distributions, the action will attempt to save and restore the `caches` and `notifications` directories from Gradle User Home. As well as any wrapper distributions, the action will attempt to save and restore the `caches` and `notifications` directories from Gradle User Home.
@ -353,7 +353,7 @@ gradle-home-cache-excludes: |
caches/keyrings caches/keyrings
``` ```
You can specify any number of fixed paths or patterns to include or exclude. You can specify any number of fixed paths or patterns to include or exclude.
File pattern support is documented at https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#patterns-to-match-file-paths. File pattern support is documented at https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#patterns-to-match-file-paths.
### Remove unused files from Gradle User Home before saving to the cache ### Remove unused files from Gradle User Home before saving to the cache
@ -362,7 +362,7 @@ The Gradle User Home directory tends to grow over time. When you switch to a new
the old files are not automatically and immediately removed. While this can make sense in a local environment, in a GitHub Actions environment the old files are not automatically and immediately removed. While this can make sense in a local environment, in a GitHub Actions environment
it can lead to ever-larger Gradle User Home cache entries being saved and restored. it can lead to ever-larger Gradle User Home cache entries being saved and restored.
To avoid this situation, The `setup-gradle` action supports the `gradle-home-cache-cleanup` parameter. To avoid this situation, The `setup-gradle` action supports the `gradle-home-cache-cleanup` parameter.
When enabled, this feature will attempt to delete any files in the Gradle User Home that were not used by Gradle during the GitHub Actions Workflow, before saving the Gradle User Home to the GitHub Actions cache. When enabled, this feature will attempt to delete any files in the Gradle User Home that were not used by Gradle during the GitHub Actions Workflow, before saving the Gradle User Home to the GitHub Actions cache.
Gradle Home cache cleanup is considered experimental and is disabled by default. You can enable this feature for the action as follows: Gradle Home cache cleanup is considered experimental and is disabled by default. You can enable this feature for the action as follows:
@ -376,7 +376,7 @@ If you have a remote build-cache available for your build, then it is recommende
- Enable [remote build-cache push](https://docs.gradle.org/current/userguide/build_cache.html#sec:build_cache_configure_use_cases) for your GitHub Actions builds - Enable [remote build-cache push](https://docs.gradle.org/current/userguide/build_cache.html#sec:build_cache_configure_use_cases) for your GitHub Actions builds
- Disable [local build-cache]() for your GitHub Actions build - Disable [local build-cache]() for your GitHub Actions build
As well as reducing the content that needs to be saved to the GitHub Actions cache, As well as reducing the content that needs to be saved to the GitHub Actions cache,
this setup will ensure that your CI builds populate the remote cache and keep the cache entries fresh by reading these entries. this setup will ensure that your CI builds populate the remote cache and keep the cache entries fresh by reading these entries.
Local builds can then benefit from the remote cache. Local builds can then benefit from the remote cache.
@ -389,8 +389,8 @@ You can enable debug logging either by:
### Increased logging from Gradle builds ### Increased logging from Gradle builds
When debug logging is enabled, this action will cause all builds to run with the `--info` and `--stacktrace` options. When debug logging is enabled, this action will cause all builds to run with the `--info` and `--stacktrace` options.
This is done by inserting the relevant [Gradle properties](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties) This is done by inserting the relevant [Gradle properties](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties)
at the top of the `${GRADLE_USER_HOME}/gradle.properties` file. at the top of the `${GRADLE_USER_HOME}/gradle.properties` file.
If the additional Gradle logging produced is problematic, you may opt out of this behavior by setting these properties manually in your project `gradle.properties` file: If the additional Gradle logging produced is problematic, you may opt out of this behavior by setting these properties manually in your project `gradle.properties` file:
@ -403,7 +403,7 @@ org.gradle.logging.stacktrace=internal
### Cache debugging and analysis ### Cache debugging and analysis
A report of all cache entries restored and saved is printed to the Job Summary when saving the cache entries. A report of all cache entries restored and saved is printed to the Job Summary when saving the cache entries.
This report can provide valuable insight into how much cache space is being used. This report can provide valuable insight into how much cache space is being used.
When debug logging is enabled, more detailed logging of cache operations is included in the GitHub actions log. When debug logging is enabled, more detailed logging of cache operations is included in the GitHub actions log.
@ -461,18 +461,18 @@ Note that to add a Pull Request comment, the workflow must be configured with th
As well as reporting all [Build Scan](https://gradle.com/build-scans/) links in the Job Summary, As well as reporting all [Build Scan](https://gradle.com/build-scans/) links in the Job Summary,
The `setup-gradle` action makes this link available as an output of any Step that executes Gradle. The `setup-gradle` action makes this link available as an output of any Step that executes Gradle.
The output name is `build-scan-url`. You can then use the build scan link in subsequent actions of your workflow. The output name is `build-scan-url`. You can then use the build scan link in subsequent actions of your workflow.
### Saving arbitrary build outputs ### Saving arbitrary build outputs
By default, a GitHub Actions workflow using `setup-gradle` will record the log output and any Build Scan By default, a GitHub Actions workflow using `setup-gradle` will record the log output and any Build Scan
links for your build, but any output files generated by the build will not be saved. links for your build, but any output files generated by the build will not be saved.
To save selected files from your build execution, you can use the core [Upload-Artifact](https://github.com/actions/upload-artifact) action. To save selected files from your build execution, you can use the core [Upload-Artifact](https://github.com/actions/upload-artifact) action.
For example: For example:
```yaml ```yaml
jobs: jobs:
gradle: gradle:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -506,7 +506,7 @@ Since Gradle applies init scripts in alphabetical order, one way to ensure this
## Gradle Wrapper validation ## Gradle Wrapper validation
Instead of using the [wrapper-validation action](./wrapper-validation.md) separately, you can enable Instead of using the [wrapper-validation action](./wrapper-validation.md) separately, you can enable
wrapper validation directly in your Setup Gradle step. wrapper validation directly in your Setup Gradle step.
```yaml ```yaml
@ -530,7 +530,7 @@ You can use the `setup-gradle` action on GitHub Enterprise Server, and benefit f
## GitHub Dependency Graph support ## GitHub Dependency Graph support
> [!IMPORTANT] > [!IMPORTANT]
> The simplest (and recommended) way to generate a dependency graph is via a separate workflow > The simplest (and recommended) way to generate a dependency graph is via a separate workflow
> using `gradle/actions/dependency-submission`. This action will attempt to detect all dependencies used by your build > using `gradle/actions/dependency-submission`. This action will attempt to detect all dependencies used by your build
> without building and testing the project itself. > without building and testing the project itself.
> >
@ -544,7 +544,7 @@ The dependency graph snapshot is generated via integration with the [GitHub Depe
The generated dependency graph snapshot reports all of the dependencies that were resolved during a build execution, and is used by GitHub to generate [Dependabot Alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts/about-dependabot-alerts) for vulnerable dependencies, as well as to populate the [Dependency Graph insights view](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/exploring-the-dependencies-of-a-repository#viewing-the-dependency-graph). The generated dependency graph snapshot reports all of the dependencies that were resolved during a build execution, and is used by GitHub to generate [Dependabot Alerts](https://docs.github.com/en/code-security/dependabot/dependabot-alerts/about-dependabot-alerts) for vulnerable dependencies, as well as to populate the [Dependency Graph insights view](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/exploring-the-dependencies-of-a-repository#viewing-the-dependency-graph).
### Basic usage ### Basic usage
You enable GitHub Dependency Graph support by setting the `dependency-graph` action parameter. Valid values are: You enable GitHub Dependency Graph support by setting the `dependency-graph` action parameter. Valid values are:
| Option | Behaviour | | Option | Behaviour |
@ -560,7 +560,7 @@ Example of a CI workflow that generates and submits a dependency graph:
name: CI build name: CI build
on: on:
push: push:
permissions: permissions:
contents: write contents: write
@ -582,13 +582,13 @@ jobs:
run: ./gradlew build run: ./gradlew build
``` ```
The `contents: write` permission is required to submit (but not generate) the dependency graph file. The `contents: write` permission is required to submit (but not generate) the dependency graph file.
Depending on [repository settings](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token), this permission may be available by default or may need to be explicitly enabled in the workflow file (as above). Depending on [repository settings](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token), this permission may be available by default or may need to be explicitly enabled in the workflow file (as above).
> [!IMPORTANT] > [!IMPORTANT]
> The above configuration will work for workflows that run as a result of commits to a repository branch, > The above configuration will work for workflows that run as a result of commits to a repository branch,
> but not when a workflow is triggered by a PR from a repository fork. > but not when a workflow is triggered by a PR from a repository fork.
> This is because the `contents: write` permission is not available when executing a workflow > This is because the `contents: write` permission is not available when executing a workflow
> for a PR submitted from a forked repository. > for a PR submitted from a forked repository.
> For a configuration that supports this setup, see [Dependency Graphs for pull request workflows](dependency-submission.md#usage-with-pull-requests-from-public-forked-repositories). > For a configuration that supports this setup, see [Dependency Graphs for pull request workflows](dependency-submission.md#usage-with-pull-requests-from-public-forked-repositories).
@ -610,7 +610,7 @@ graph cannot be generated or submitted. You can enable this behavior with the `d
### Using a custom plugin repository ### Using a custom plugin repository
By default, the action downloads the `github-dependency-graph-gradle-plugin` from the Gradle Plugin Portal (https://plugins.gradle.org). If your GitHub Actions environment does not have access to this URL, you can specify a custom plugin repository to use. By default, the action downloads the `github-dependency-graph-gradle-plugin` from the Gradle Plugin Portal (https://plugins.gradle.org). If your GitHub Actions environment does not have access to this URL, you can specify a custom plugin repository to use.
Do so by setting the `GRADLE_PLUGIN_REPOSITORY_URL` environment variable with your Gradle invocation. Do so by setting the `GRADLE_PLUGIN_REPOSITORY_URL` environment variable with your Gradle invocation.
The `GRADLE_PLUGIN_REPOSITORY_USERNAME` and `GRADLE_PLUGIN_REPOSITORY_PASSWORD` can be used when the plugin repository requires authentication. The `GRADLE_PLUGIN_REPOSITORY_USERNAME` and `GRADLE_PLUGIN_REPOSITORY_PASSWORD` can be used when the plugin repository requires authentication.
@ -670,7 +670,7 @@ jobs:
### Filtering which Gradle Configurations contribute to the dependency graph ### Filtering which Gradle Configurations contribute to the dependency graph
If you do not want the dependency graph to include every dependency configuration in every project in your build, If you do not want the dependency graph to include every dependency configuration in every project in your build,
you can limit the dependency extraction to a subset of these. you can limit the dependency extraction to a subset of these.
See the documentation for [dependency-submission](dependency-submission.md) and the See the documentation for [dependency-submission](dependency-submission.md) and the
@ -678,7 +678,7 @@ See the documentation for [dependency-submission](dependency-submission.md) and
### Gradle version compatibility ### Gradle version compatibility
Dependency-graph generation is compatible with most versions of Gradle >= `5.2`, and is tested regularly against Dependency-graph generation is compatible with most versions of Gradle >= `5.2`, and is tested regularly against
Gradle versions `5.2.1`, `5.6.4`, `6.0.1`, `6.9.4`, `7.1.1` and `7.6.3`, as well as all patched versions of Gradle 8.x. Gradle versions `5.2.1`, `5.6.4`, `6.0.1`, `6.9.4`, `7.1.1` and `7.6.3`, as well as all patched versions of Gradle 8.x.
A known exception to this is that Gradle `7.0`, `7.0.1`, and `7.0.2` are not supported. A known exception to this is that Gradle `7.0`, `7.0.1`, and `7.0.2` are not supported.
@ -687,7 +687,7 @@ See [here](https://github.com/gradle/github-dependency-graph-gradle-plugin?tab=r
### Reducing storage costs for saved dependency graph artifacts ### Reducing storage costs for saved dependency graph artifacts
When `generate` or `generate-and-submit` is used with the action, the dependency graph that is generated is stored as a workflow artifact. When `generate` or `generate-and-submit` is used with the action, the dependency graph that is generated is stored as a workflow artifact.
By default, these artifacts are retained for 30 days (or as configured for the repository). By default, these artifacts are retained for 30 days (or as configured for the repository).
To reduce storage costs for these artifacts, you can set the `artifact-retention-days` value to a lower number. To reduce storage costs for these artifacts, you can set the `artifact-retention-days` value to a lower number.
@ -708,7 +708,7 @@ The same auto-injection behavior is available for the Common Custom User Data Gr
## Enabling Develocity injection ## Enabling Develocity injection
To enable Develocity injection for your build, you must provide the required configuration via environment variables. To enable Develocity injection for your build, you must provide the required configuration via environment variables.
Here's a minimal example: Here's a minimal example:
@ -727,8 +727,29 @@ Here's a minimal example:
This configuration will automatically apply `v3.17.3` of the [Develocity Gradle plugin](https://docs.gradle.com/develocity/gradle-plugin/), and publish build scans to https://develocity.your-server.com. This configuration will automatically apply `v3.17.3` of the [Develocity Gradle plugin](https://docs.gradle.com/develocity/gradle-plugin/), and publish build scans to https://develocity.your-server.com.
This example assumes that the `develocity.your-server.com` server allows anonymous publishing of build scans. This example assumes that the `develocity.your-server.com` server allows anonymous publishing of build scans.
In the likely scenario that your Develocity server requires authentication, you will also need to configure an additional environment variable In the likely scenario that your Develocity server requires authentication, you will also need to pass a valid [Develocity access key](https://docs.gradle.com/develocity/gradle-plugin/#via_environment_variable) taken from a secret:
with a valid [Develocity access key](https://docs.gradle.com/develocity/gradle-plugin/#via_environment_variable).
```yaml
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
develocity-access-key: ${{ secrets.MY_DEVELOCITY_ACCESS_KEY }}
- name: Run a Gradle build with Develocity injection enabled
run: ./gradlew build
env:
DEVELOCITY_INJECTION_ENABLED: true
DEVELOCITY_URL: https://develocity.your-server.com
DEVELOCITY_PLUGIN_VERSION: 3.17
```
This access key will be used during the action execution to get a short-lived token and set it to the DEVELOCITY_ACCESS_KEY environment variable.
### Short-lived access tokens
Develocity access keys are long-lived, creating risks if they are leaked. To avoid this, users can use short-lived access tokens to authenticate with Develocity. Access tokens can be used wherever an access key would be used. Access tokens are only valid for the Develocity instance that created them.
If a short-lived token fails to be retrieved (for example, if the Develocity server version is lower than `2024.1`), no access key will be set.
In that case, Develocity authenticated operations like build cache read/write and build scan publication will fail without failing the build.
For more information on short-lived tokens, see [Develocity API documentation](https://docs.gradle.com/develocity/api-manual/#short_lived_access_tokens).
## Configuring Develocity injection ## Configuring Develocity injection

View File

@ -23,7 +23,7 @@ inputs:
cache-write-only: cache-write-only:
description: | description: |
When 'true', entries will not be restored from the cache but will be saved at the end of the Job. When 'true', entries will not be restored from the cache but will be saved at the end of the Job.
Setting this to 'true' implies cache-read-only will be 'false'. Setting this to 'true' implies cache-read-only will be 'false'.
required: false required: false
default: false default: false
@ -35,7 +35,7 @@ inputs:
cache-encryption-key: cache-encryption-key:
description: | description: |
A base64 encoded AES key used to encrypt the configuration-cache data. The key is exported as 'GRADLE_ENCRYPTION_KEY' for later steps. A base64 encoded AES key used to encrypt the configuration-cache data. The key is exported as 'GRADLE_ENCRYPTION_KEY' for later steps.
A suitable key can be generated with `openssl rand -base64 16`. A suitable key can be generated with `openssl rand -base64 16`.
Configuration-cache data will not be saved/restored without an encryption key being provided. Configuration-cache data will not be saved/restored without an encryption key being provided.
required: false required: false
@ -70,7 +70,7 @@ inputs:
# Dependency Graph configuration # Dependency Graph configuration
dependency-graph: dependency-graph:
description: | description: |
Specifies if a GitHub dependency snapshot should be generated for each Gradle build, and if so, how. Specifies if a GitHub dependency snapshot should be generated for each Gradle build, and if so, how.
Valid values are 'disabled' (default), 'generate', 'generate-and-submit', 'generate-and-upload', 'download-and-submit' and 'clear'. Valid values are 'disabled' (default), 'generate', 'generate-and-submit', 'generate-and-upload', 'download-and-submit' and 'clear'.
required: false required: false
default: 'disabled' default: 'disabled'
@ -95,11 +95,19 @@ inputs:
build-scan-terms-of-use-url: build-scan-terms-of-use-url:
description: The URL to the Build Scan® terms of use. This input must be set to 'https://gradle.com/terms-of-service' or 'https://gradle.com/help/legal-terms-of-use'. description: The URL to the Build Scan® terms of use. This input must be set to 'https://gradle.com/terms-of-service' or 'https://gradle.com/help/legal-terms-of-use'.
required: false required: false
build-scan-terms-of-use-agree: build-scan-terms-of-use-agree:
description: Indicate that you agree to the Build Scan® terms of use. This input value must be "yes". description: Indicate that you agree to the Build Scan® terms of use. This input value must be "yes".
required: false required: false
develocity-access-key:
description: Develocity access key. Should be set to a secret containing the Develocity Access key.
required: false
develocity-token-expiry:
description: The Develocity short-lived access tokens expiry in hours. Default is 2 hours.
required: false
# Wrapper validation configuration # Wrapper validation configuration
validate-wrappers: validate-wrappers:
description: | description: |
@ -143,7 +151,7 @@ inputs:
description: When 'true', the action will not attempt to restore the Gradle User Home entries from other Jobs. description: When 'true', the action will not attempt to restore the Gradle User Home entries from other Jobs.
required: false required: false
default: false default: false
# INTERNAL ACTION INPUTS # INTERNAL ACTION INPUTS
# These inputs should not be configured directly, and are only used to pass environmental information to the action # These inputs should not be configured directly, and are only used to pass environmental information to the action
workflow-job-context: workflow-job-context:

View File

@ -200,6 +200,14 @@ export class BuildScanConfig {
return this.getTermsOfUseProp('build-scan-terms-of-use-agree', 'build-scan-terms-of-service-agree') return this.getTermsOfUseProp('build-scan-terms-of-use-agree', 'build-scan-terms-of-service-agree')
} }
getDevelocityAccessKey(): string {
return core.getInput('develocity-access-key') || process.env['DEVELOCITY_ACCESS_KEY'] || ''
}
getDevelocityTokenExpiry(): string {
return core.getInput('develocity-token-expiry')
}
private verifyTermsOfUseAgreement(): boolean { private verifyTermsOfUseAgreement(): boolean {
if ( if (
(this.getBuildScanTermsOfUseUrl() !== 'https://gradle.com/terms-of-service' && (this.getBuildScanTermsOfUseUrl() !== 'https://gradle.com/terms-of-service' &&

View File

@ -1,7 +1,8 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {BuildScanConfig} from './configuration' import {BuildScanConfig} from '../configuration'
import {setupToken} from './short-lived-token'
export function setup(config: BuildScanConfig): void { export async function setup(config: BuildScanConfig): Promise<void> {
maybeExportVariable('DEVELOCITY_INJECTION_INIT_SCRIPT_NAME', 'gradle-actions.inject-develocity.init.gradle') maybeExportVariable('DEVELOCITY_INJECTION_INIT_SCRIPT_NAME', 'gradle-actions.inject-develocity.init.gradle')
maybeExportVariable('DEVELOCITY_AUTO_INJECTION_CUSTOM_VALUE', 'gradle-actions') maybeExportVariable('DEVELOCITY_AUTO_INJECTION_CUSTOM_VALUE', 'gradle-actions')
if (config.getBuildScanPublishEnabled()) { if (config.getBuildScanPublishEnabled()) {
@ -11,6 +12,16 @@ export function setup(config: BuildScanConfig): void {
maybeExportVariable('DEVELOCITY_TERMS_OF_USE_URL', config.getBuildScanTermsOfUseUrl()) maybeExportVariable('DEVELOCITY_TERMS_OF_USE_URL', config.getBuildScanTermsOfUseUrl())
maybeExportVariable('DEVELOCITY_TERMS_OF_USE_AGREE', config.getBuildScanTermsOfUseAgree()) maybeExportVariable('DEVELOCITY_TERMS_OF_USE_AGREE', config.getBuildScanTermsOfUseAgree())
} }
setupToken(
config.getDevelocityAccessKey(),
config.getDevelocityTokenExpiry(),
getEnv('DEVELOCITY_ENFORCE_URL'),
getEnv('DEVELOCITY_URL')
)
}
function getEnv(variableName: string): string | undefined {
return process.env[variableName]
} }
function maybeExportVariable(variableName: string, value: unknown): void { function maybeExportVariable(variableName: string, value: unknown): void {

View File

@ -0,0 +1,191 @@
import * as httpm from 'typed-rest-client/HttpClient'
import * as core from '@actions/core'
export async function setupToken(
develocityAccessKey: string,
develocityTokenExpiry: string,
enforceUrl: string | undefined,
develocityUrl: string | undefined
): Promise<void> {
const develocityAccesskeyEnvVar = 'DEVELOCITY_ACCESS_KEY'
if (develocityAccessKey) {
try {
core.debug('Fetching short-lived token...')
const tokens = await getToken(enforceUrl, develocityUrl, develocityAccessKey, develocityTokenExpiry)
if (tokens != null && !tokens.isEmpty()) {
core.debug(`Got token(s), setting the ${develocityAccesskeyEnvVar} env var`)
const token = tokens.raw()
core.setSecret(token)
core.exportVariable(develocityAccesskeyEnvVar, token)
} else {
// In case of not being able to generate a token we set the env variable to empty to avoid leaks
core.exportVariable(develocityAccesskeyEnvVar, '')
}
} catch (e) {
core.exportVariable(develocityAccesskeyEnvVar, '')
core.warning(`Failed to fetch short-lived token, reason: ${e}`)
}
}
}
export async function getToken(
enforceUrl: string | undefined,
serverUrl: string | undefined,
accessKey: string,
expiry: string
): Promise<DevelocityAccessCredentials | null> {
const empty: Promise<DevelocityAccessCredentials | null> = new Promise(r => r(null))
const develocityAccessKey = DevelocityAccessCredentials.parse(accessKey)
const shortLivedTokenClient = new ShortLivedTokenClient()
async function promiseError(message: string): Promise<DevelocityAccessCredentials | null> {
return new Promise((resolve, reject) => reject(new Error(message)))
}
if (develocityAccessKey == null) {
return empty
}
if (enforceUrl === 'true' || develocityAccessKey.isSingleKey()) {
if (!serverUrl) {
return promiseError('Develocity Server URL not configured')
}
const hostname = extractHostname(serverUrl)
if (hostname == null) {
return promiseError('Could not extract hostname from Develocity server URL')
}
const hostAccessKey = develocityAccessKey.forHostname(hostname)
if (!hostAccessKey) {
return promiseError(`Could not find corresponding key for hostname ${hostname}`)
}
try {
const token = await shortLivedTokenClient.fetchToken(serverUrl, hostAccessKey, expiry)
return DevelocityAccessCredentials.of([token])
} catch (e) {
return new Promise((resolve, reject) => reject(e))
}
}
const tokens = new Array<HostnameAccessKey>()
for (const k of develocityAccessKey.keys) {
try {
const token = await shortLivedTokenClient.fetchToken(`https://${k.hostname}`, k, expiry)
tokens.push(token)
} catch (e) {
// Ignoring failed token, TODO: log this ?
}
}
if (tokens.length > 0) {
return DevelocityAccessCredentials.of(tokens)
}
return empty
}
function extractHostname(serverUrl: string): string | null {
try {
const parsedUrl = new URL(serverUrl)
return parsedUrl.hostname
} catch (error) {
return null
}
}
class ShortLivedTokenClient {
httpc = new httpm.HttpClient('gradle/setup-gradle')
maxRetries = 3
retryInterval = 1000
async fetchToken(serverUrl: string, accessKey: HostnameAccessKey, expiry: string): Promise<HostnameAccessKey> {
const queryParams = expiry ? `?expiresInHours${expiry}` : ''
const sanitizedServerUrl = !serverUrl.endsWith('/') ? `${serverUrl}/` : serverUrl
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessKey.key}`
}
let attempts = 0
while (attempts < this.maxRetries) {
try {
const requestUrl = `${sanitizedServerUrl}api/auth/token${queryParams}`
core.debug(`Attempt ${attempts} to fetch short lived token at ${requestUrl}`)
const response = await this.httpc.post(requestUrl, '', headers)
if (response.message.statusCode === 200) {
const text = await response.readBody()
return new Promise<HostnameAccessKey>(resolve => resolve({hostname: accessKey.hostname, key: text}))
}
// This should be only 404
attempts++
if (attempts === this.maxRetries) {
return new Promise((resolve, reject) =>
reject(
new Error(
`Develocity short lived token request failed ${serverUrl} with status code ${response.message.statusCode}`
)
)
)
}
} catch (error) {
attempts++
if (attempts === this.maxRetries) {
return new Promise((resolve, reject) => reject(error))
}
}
await new Promise(resolve => setTimeout(resolve, this.retryInterval))
}
return new Promise((resolve, reject) => reject(new Error('Illegal state')))
}
}
type HostnameAccessKey = {
hostname: string
key: string
}
export class DevelocityAccessCredentials {
static readonly accessKeyRegexp = /^(\S+=\w+)(;\S+=\w+)*$/
readonly keys: HostnameAccessKey[]
private constructor(allKeys: HostnameAccessKey[]) {
this.keys = allKeys
}
static of(allKeys: HostnameAccessKey[]): DevelocityAccessCredentials {
return new DevelocityAccessCredentials(allKeys)
}
private static readonly keyDelimiter = ';'
private static readonly hostDelimiter = '='
static parse(rawKey: string): DevelocityAccessCredentials | null {
if (!this.isValid(rawKey)) {
return null
}
return new DevelocityAccessCredentials(
rawKey.split(this.keyDelimiter).map(hostKey => {
const pair = hostKey.split(this.hostDelimiter)
return {hostname: pair[0], key: pair[1]}
})
)
}
isEmpty(): boolean {
return this.keys.length === 0
}
isSingleKey(): boolean {
return this.keys.length === 1
}
forHostname(hostname: string): HostnameAccessKey | undefined {
return this.keys.find(hostKey => hostKey.hostname === hostname)
}
raw(): string {
return this.keys
.map(k => `${k.hostname}${DevelocityAccessCredentials.hostDelimiter}${k.key}`)
.join(DevelocityAccessCredentials.keyDelimiter)
}
private static isValid(allKeys: string): boolean {
return this.accessKeyRegexp.test(allKeys)
}
}

View File

@ -4,7 +4,7 @@ import * as path from 'path'
import * as os from 'os' import * as os from 'os'
import * as caches from './caching/caches' import * as caches from './caching/caches'
import * as jobSummary from './job-summary' import * as jobSummary from './job-summary'
import * as buildScan from './build-scan' import * as buildScan from './develocity/build-scan'
import {loadBuildResults, markBuildResultsProcessed} from './build-results' import {loadBuildResults, markBuildResultsProcessed} from './build-results'
import {CacheListener, generateCachingReport} from './caching/cache-reporting' import {CacheListener, generateCachingReport} from './caching/cache-reporting'
@ -41,7 +41,7 @@ export async function setup(cacheConfig: CacheConfig, buildScanConfig: BuildScan
core.saveState(CACHE_LISTENER, cacheListener.stringify()) core.saveState(CACHE_LISTENER, cacheListener.stringify())
buildScan.setup(buildScanConfig) await buildScan.setup(buildScanConfig)
return true return true
} }

View File

@ -0,0 +1,137 @@
import {DevelocityAccessCredentials, getToken} from "../../src/develocity/short-lived-token";
import nock from "nock";
describe('short lived tokens', () => {
it('parse valid access key should return an object', async () => {
let develocityAccessCredentials = DevelocityAccessCredentials.parse('some-host.local=key1;host2=key2');
expect(develocityAccessCredentials).toStrictEqual(DevelocityAccessCredentials.of([
{hostname: 'some-host.local', key: 'key1'},
{hostname: 'host2', key: 'key2'}])
)
})
it('parse wrong access key should return null', async () => {
let develocityAccessCredentials = DevelocityAccessCredentials.parse('random;foo');
expect(develocityAccessCredentials).toBeNull()
})
it('parse empty access key should return null', async () => {
let develocityAccessCredentials = DevelocityAccessCredentials.parse('');
expect(develocityAccessCredentials).toBeNull()
})
it('access key as raw string', async () => {
let develocityAccessCredentials = DevelocityAccessCredentials.parse('host1=key1;host2=key2');
expect(develocityAccessCredentials?.raw()).toBe('host1=key1;host2=key2')
})
it('get short lived token fails when cannot connect', async () => {
nock('http://localhost:3333')
.post('/api/auth/token')
.times(3)
.replyWithError({
message: 'connect ECONNREFUSED 127.0.0.1:3333',
code: 'ECONNREFUSED'
})
try {
await getToken('true', 'http://localhost:3333', 'localhost=xyz;host1=key1', '')
expect('should have thrown').toBeUndefined()
} catch (e) {
// @ts-ignore
expect(e.code).toBe('ECONNREFUSED')
}
})
it('get short lived token fails when request fails', async () => {
nock('http://dev:3333')
.post('/api/auth/token')
.times(3)
.reply(500, 'Internal error')
expect.assertions(1)
await expect(getToken('true', 'http://dev:3333', 'dev=xyz;host1=key1', ''))
.rejects
.toThrow('Develocity short lived token request failed http://dev:3333 with status code 500')
})
it('get short lived token fails when server url is not set', async () => {
expect.assertions(1)
await expect(getToken('true', undefined, 'localhost=xyz;host1=key1', ''))
.rejects
.toThrow('Develocity Server URL not configured')
})
it('get short lived token returns null when access key is empty', async () => {
expect.assertions(1)
await expect(getToken('true', 'http://dev:3333', '', ''))
.resolves
.toBeNull()
})
it('get short lived token fails when host cannot be extracted from server url', async () => {
expect.assertions(1)
await expect(getToken('true', 'not_a_url', 'localhost=xyz;host1=key1', ''))
.rejects
.toThrow('Could not extract hostname from Develocity server URL')
})
it('get short lived token fails when access key does not contain corresponding host', async () => {
expect.assertions(1)
await expect(getToken('true', 'http://dev', 'host1=xyz;host2=key2', ''))
.rejects
.toThrow('Could not find corresponding key for hostname dev')
})
it('get short lived token succeeds when enforce url is true', async () => {
nock('https://dev')
.post('/api/auth/token')
.reply(200, 'token')
expect.assertions(1)
await expect(getToken('true', 'https://dev', 'dev=key1;host1=key2', ''))
.resolves
.toEqual({"keys": [{"hostname": "dev", "key": "token"}]})
})
it('get short lived token succeeds when enforce url is false and single key is set', async () => {
nock('https://dev')
.post('/api/auth/token')
.reply(200, 'token')
expect.assertions(1)
await expect(getToken('false', 'https://dev', 'dev=key1', ''))
.resolves
.toEqual({"keys": [{"hostname": "dev", "key": "token"}]})
})
it('get short lived token succeeds when enforce url is false and multiple keys are set', async () => {
nock('https://dev')
.post('/api/auth/token')
.reply(200, 'token1')
nock('https://prod')
.post('/api/auth/token')
.reply(200, 'token2')
expect.assertions(1)
await expect(getToken('false', 'https://dev', 'dev=key1;prod=key2', ''))
.resolves
.toEqual({"keys": [{"hostname": "dev", "key": "token1"}, {"hostname": "prod", "key": "token2"}]})
})
it('get short lived token succeeds when enforce url is false and multiple keys are set and one is failing', async () => {
nock('https://dev')
.post('/api/auth/token')
.reply(200, 'token1')
nock('https://bogus')
.post('/api/auth/token')
.times(3)
.reply(500, 'Internal Error')
nock('https://prod')
.post('/api/auth/token')
.reply(200, 'token2')
expect.assertions(1)
await expect(getToken('false', 'https://dev', 'dev=key1;bogus=key0;prod=key2', ''))
.resolves
.toEqual({"keys": [{"hostname": "dev", "key": "token1"}, {"hostname": "prod", "key": "token2"}]})
})
})