Compare commits

...

12 Commits

Author SHA1 Message Date
bot-githubaction 3f131e8634 [bot] Update dist directory 2026-06-12 15:22:42 +00:00
Daz DeBoer 97715a29bc Redesign the caching Job Summary (#985)
Redesigns the caching section of the Job Summary into a single,
consistent layout across every cache provider and state, and integrates
the provider message into the report rather than appending it
disconnected at the bottom.

## Motivation

The caching report was produced by three divergent code paths (NoOp /
basic / enhanced), each rendering its own markdown:

- **Explicitly disabled** → a one-line message, no expand, no provider
note.
- **Enhanced** (incl. skipped-due-to-existing-home) → a full `<details>`
block.
- **Basic** → a one-line message with **no** expandable details at all.

The Enhanced/Basic provider note floated at the very bottom,
disconnected from the report.

## What changed

`save()` now returns structured `CacheReport` data instead of
pre-rendered HTML, and a single renderer (`caching-report.ts`) produces
one unified layout for all variants:

- **Section heading**: `#### <icon> Gradle Caching — <Provider>
(<status>)`
- **Status line** explaining what the cache did
- **Integrated provider note** woven in under the heading — now shown
**unconditionally** (no longer gated on license acceptance)
- **Expandable cache-entry details** when there are entries — basic
caching now gets this too

The two disabled variants (explicitly disabled, and skipped due to a
pre-existing Gradle User Home) render as **compact callouts with no
expandable section**.

### Main repo
- `caching-report.ts` (new): central renderer + all framing copy + entry
table/`<pre>` helpers.
- `cache-service.ts`: `CacheReport` / `CacheEntryReport` / status types;
`save()` returns `CacheReport`.
- `cache-service-loader.ts`: `NoOp` returns a report;
`LicenseWarningCacheService` removed; new `getProviderNote()`.
- `cache-service-basic.ts`: builds a `CacheReport`.
- `job-summary.ts` / `setup-gradle.ts`: thread `CacheReport` +
`ProviderNote`.
- `configuration.ts`: remove now-unused `isCacheLicenseAccepted()`.

### Vendored library
The structured contract requires **gradle-actions-caching v0.7.0**
(gradle/actions-caching#74). This PR updates the vendored library to
that release — the official `Update gradle-actions-caching library to
v0.7.0` vendor commit is included here, so merging this PR ships the
redesign together with the library it depends on.

## Testing

- Both repos build; prettier + eslint clean.
- `gradle/actions`: 363/363 Jest tests pass, including new
`caching-report.test.ts` covering every variant.
- `gradle-actions-caching`: 74/74 pass under JDK 17.
- Rendered markdown verified for all five variants (enhanced/basic
enabled & read-only, disabled, skipped).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Bot Githubaction <bot-githubaction@gradle.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 15:21:52 +00:00
Daz DeBoer 8b6cdb5f58 CI: add requireable aggregate/no-op checks for branch protection (#984)
Prepares CI so a small, stable set of **required status checks** can be
enabled (which in turn unlocks auto-merge), instead of having to list
every fanned-out matrix job. GitHub required checks match by exact name
— no wildcards — so this reduces the surface to a handful of high-level
checks.

## Changes

- **`ci-integ-test.yml`**: add an aggregate gate job
`integ-test-success` that `needs:` all four top-level jobs (the three
suite jobs each wrap a reusable workflow that fans out into many nested
checks) and fails if any did not succeed. `if: always()` ensures it
reports even when a dependency fails. This collapses dozens of nested
integ-test checks into a single requireable check.

- **`ci-init-script-check.yml`**: remove the workflow-level
`pull_request.paths` filter so the workflow runs on every PR and always
reports a status check (previously it was absent on most PRs, which
would deadlock a required check). Relevant-change detection moves into
the job via `tj-actions/changed-files` (same pinned action already used
by `ci-check-no-dist-update.yml`). On a PR the Java/Gradle/test steps
run only when init-script files changed; otherwise the job is a fast
no-op that still succeeds. Push and `workflow_dispatch` runs execute
fully as before.

## Suggested required-check set (all run on every PR, none can deadlock)

- `CI-check-and-unit-test / check-format-and-unit-test`
- `ci-validate-typings.yml / validate-typings`
- `CI-validate-wrappers / validation`
- `CI-codeql / Analyze (javascript-typescript)`
- `CI-integ-test / integ-test-success`
- `CI-init-script-check / test-init-scripts`

`ci-check-no-dist-update` is intentionally **omitted** — it only runs on
`dist/**` edits and is designed to fail, so it shouldn't be a required
gate.

> Confirm the exact check names from the list GitHub shows after this
branch runs once.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:04:23 -06:00
bot-githubaction 5852e0e5d8 [bot] Update dist directory 2026-06-10 16:15:39 +00:00
Simon Marquis 318eed7038 Hide obsolete Job summaries (#902)
- Injects a `<!-- gradle-job-summary: ${jobCorrelator} -->` marker on
each job summary
- Lists 100 last comments: unfortunately there is no API to specifically
filter for comments, and checking the last 100 comments (the limit) is
usually enough and does not require iterating over pages
- Mutate comments having this expected marker

I tried to add some tests, but I'm not familiar enough to setup a
complete test suite with proper mocking of GitHub/Octokit with jest.

I could potentially extract the `prComment` creation to check for the
marker presence, let me know.

Note: it seems like there is currently an issue on mutating comments as
`OUTDATED` through graphql. Although it does not work as expected
(flagging as OUTDATED) the comments are still minimized, which is what
we want.
- https://github.com/orgs/community/discussions/19865

Implements #176

---------

Co-authored-by: Daz DeBoer <daz@gradle.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 10:14:48 -06:00
Björn Kautler a740661292 Improve typings (#938)
This PR adds a missing enum value, and makes strings that are actually
delimited lists those lists, so that you for example in the generated
Kotlin bindings can simply do
```kotlin
additionalArguments = listOf(
    "--info",
    "--stacktrace",
    "--show-version"
)
```
instead of needing to do
```kotlin
additionalArguments = listOf(
    "--info",
    "--stacktrace",
    "--show-version"
).joinToString(" ")
```
or writing it all in one line as one string.

This is also how the typings for older versions are in the typing
catalog.

Theoretically, this is a breaking changes as the typings define the API
surface of the action and from the typings bindings are generated. I'll
leave it up to you how you handle it regarding version increase or when
to merge.
2026-06-10 08:54:56 -06:00
Bot Githubaction 7ae0d0208c Update gradle-actions-caching library to v0.6.0 (#982)
Updates to the latest gradle-actions-caching library.
2026-06-10 08:51:32 -06:00
Daz DeBoer e473973a5b Scope CI-integ-test concurrency groups per-branch
The concurrency groups used fixed names spanning all branches, so a push
to main could cancel a pending PR run (and vice versa), leaving PRs not
fully tested.

Append ${{ github.ref }} to each group so runs only supersede pending
runs on the same branch, while different branches run in parallel. Also
drop the `queue: max` key, which is not a valid GitHub Actions
concurrency option and was ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 07:33:01 -07:00
Daz DeBoer 35a4a3f355 Queue up integ-test runs 2026-06-10 08:24:34 -06:00
bot-githubaction b6eebf33f1 [bot] Update dist directory 2026-06-10 14:15:10 +00:00
Daz DeBoer 9901393644 Remove unnecessary dependency overrides (#981)
Removes all `overrides` from `sources/package.json`. Two commits, each
independently verified:

## 1. Remove redundant security overrides

The `shell-quote`, `fast-xml-parser`, `fast-xml-builder` and `eslint >
brace-expansion` overrides added in #980 are **no-ops**: npm's natural
resolution already lands on the exact same patched versions, so they
upgrade nothing. The vulnerabilities were actually resolved by
regenerating the lockfile, not by the overrides.

## 2. Remove obsolete Octokit/Azure overrides

`@azure/logger`, `@octokit/request`, `@octokit/request-error` and
`@octokit/plugin-paginate-rest` were point-in-time pins added to
force-upgrade then-vulnerable transitive deps (5d947f45, #601). The
parent packages (`@actions/github`, `@actions/artifact`) have since
advanced and now resolve **newer, non-vulnerable** versions naturally —
so the overrides only pinned stale versions:

| Package | Pinned (override) | Natural |
|---|---|---|
| `@octokit/request` | 8.4.1 | 10.0.10 |
| `@octokit/request-error` | 5.1.1 | 7.1.0 |
| `@octokit/plugin-paginate-rest` | 9.2.2 | 14.0.0 |
| `@azure/logger` | 1.1.4 | 1.3.0 |

## Verification

- `npm audit` → **0 vulnerabilities**
- `./build` → passes
- `npm test` → **352/352 passing**

### Note on a flaky test
While testing I saw the `wrapper-validation` test *"fetches wrapper jar
checksums for snapshots"* intermittently fail (1–2 failures, then pass
on retry). It is a **pre-existing flaky network test** — it makes ~175
live calls to Gradle services and sits right at its 60s timeout. Its
code path imports neither Octokit nor Azure (`src/wrapper-validation/`
uses only `@actions/http-client`/`nock`/`cheerio`), so it is unrelated
to these overrides; the `nock`/`@mswjs/interceptors`/`undici` versions
are identical before and after.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 08:14:07 -06:00
Daz DeBoer 20ce680c89 Update RELEASING.md 2026-06-09 19:55:28 -06:00
34 changed files with 1564 additions and 921 deletions
+18 -4
View File
@@ -8,10 +8,6 @@ on:
paths-ignore:
- 'dist/**'
pull_request:
paths:
- '.github/workflows/ci-init-script-check.yml'
- 'sources/src/resources/init-scripts/**'
- 'sources/test/init-scripts/**'
workflow_dispatch:
permissions:
@@ -23,16 +19,34 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
# Detect whether anything relevant to the init-script tests changed.
# The workflow always runs (so it always reports a status check, making it safe
# to mark as required), but the heavy steps below are skipped on pull requests
# that don't touch the init-scripts. Pushes and manual runs always execute fully.
- name: Check for relevant changes
id: changes
if: github.event_name == 'pull_request'
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
with:
files: |
.github/workflows/ci-init-script-check.yml
sources/src/resources/init-scripts/**
sources/test/init-scripts/**
- name: Setup Java
if: steps.changes.outputs.any_changed == 'true' || github.event_name != 'pull_request'
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
if: steps.changes.outputs.any_changed == 'true' || github.event_name != 'pull_request'
# Use a released version to avoid breakages
uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
env:
ALLOWED_GRADLE_WRAPPER_CHECKSUMS: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 # Invalid wrapper jar used for testing
- name: Run integration tests
if: steps.changes.outputs.any_changed == 'true' || github.event_name != 'pull_request'
working-directory: sources/test/init-scripts
run: ./gradlew check
+24 -3
View File
@@ -28,7 +28,7 @@ jobs:
needs: build-distribution
uses: ./.github/workflows/suite-integ-test-caching.yml
concurrency:
group: CI-integ-test-caching
group: CI-integ-test-caching-${{ github.ref }}
cancel-in-progress: false
with:
skip-dist: false
@@ -40,7 +40,7 @@ jobs:
needs: caching-integ-tests
uses: ./.github/workflows/suite-integ-test-other.yml
concurrency:
group: CI-integ-test-other
group: CI-integ-test-other-${{ github.ref }}
cancel-in-progress: false
with:
skip-dist: false
@@ -52,8 +52,29 @@ jobs:
needs: other-integ-tests
uses: ./.github/workflows/suite-integ-test-dependency-submission.yml
concurrency:
group: CI-integ-test-dependency-submission
group: CI-integ-test-dependency-submission-${{ github.ref }}
cancel-in-progress: false
with:
skip-dist: false
secrets: inherit
# Aggregate gate: a single check that succeeds only when all integ-test jobs succeed.
# Require this one check in branch protection instead of every fanned-out matrix job.
integ-test-success:
if: ${{ always() }}
needs:
- build-distribution
- caching-integ-tests
- other-integ-tests
- dependency-submission-integ-tests
runs-on: ubuntu-latest
steps:
- name: Fail if any integ-test job failed or was cancelled
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: |
echo "One or more integ-test jobs did not succeed:"
echo " build-distribution: ${{ needs.build-distribution.result }}"
echo " caching-integ-tests: ${{ needs.caching-integ-tests.result }}"
echo " other-integ-tests: ${{ needs.other-integ-tests.result }}"
echo " dependency-submission-integ-tests: ${{ needs.dependency-submission-integ-tests.result }}"
exit 1
+4 -4
View File
@@ -11,16 +11,16 @@
- Note: The gradle actions follow the GitHub Actions convention of including a .0 patch number for the first release of a minor version, unlike the Gradle convention which omits the trailing .0.
## Release gradle/actions
- Create a tag for the release. The tag should have the format `v5.0.0`
- From CLI: `git tag -s -m "v5.0.0" v5.0.0 && git push --tags`
- Create a tag for the release. The tag should have the format `v6.2.0`
- From CLI: `git tag -s -m "v6.2.0" v6.2.0 && git push --tags`
- Note that we sign the tag and set the commit message for the tag to the newly released version.
- Go to https://github.com/gradle/actions/releases and "Draft new release"
- Use the newly created tag and copy the tag name exactly as the release title.
- Craft release notes content based on issues closed, PRs merged and commits
- 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`
- Force push the `v6` 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 "v6.2.0" v6 && 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.
+9 -3
View File
@@ -8,10 +8,16 @@ inputs:
type: string
dependency-resolution-task:
type: string
type: list
separator: ' '
list-item:
type: string
additional-arguments:
type: string
type: list
separator: ' '
list-item:
type: string
# Cache configuration
cache-provider:
@@ -115,7 +121,7 @@ inputs:
build-scan-terms-of-use-agree:
type: enum
allowed-values:
- 'yes'
- yes
develocity-access-key:
type: string
+133 -132
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+180 -127
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+132 -131
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+205 -151
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+76 -62
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5
View File
@@ -548,6 +548,11 @@ jobs:
- run: ./gradlew build --scan
```
When a comment is added, any earlier Job Summary comments posted by the same job on that Pull Request are
automatically minimized, so only the most recent result remains expanded. This also applies when
`add-job-summary-as-pr-comment: 'on-failure'` is used: once a later run of the job succeeds, the previous
failure comment is collapsed.
Note that to add a Pull Request comment, the workflow must be configured with the `pull-requests: write` permission.
+6 -2
View File
@@ -69,6 +69,7 @@ inputs:
- disabled
- generate
- generate-and-submit
- generate-submit-and-upload
- generate-and-upload
- download-and-submit
@@ -106,7 +107,7 @@ inputs:
build-scan-terms-of-use-agree:
type: enum
allowed-values:
- 'yes'
- yes
develocity-access-key:
type: string
@@ -153,7 +154,10 @@ inputs:
# Deprecated action inputs
arguments:
type: string
type: list
separator: ' '
list-item:
type: string
# Experimental action inputs
gradle-home-cache-strict-match:
+125 -172
View File
@@ -92,6 +92,19 @@
"node": ">= 20"
}
},
"node_modules/@actions/artifact/node_modules/@octokit/endpoint": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
"integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@actions/artifact/node_modules/@octokit/graphql": {
"version": "9.0.3",
"license": "MIT",
@@ -129,6 +142,35 @@
"@octokit/core": ">=7"
}
},
"node_modules/@actions/artifact/node_modules/@octokit/request": {
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.10.tgz",
"integrity": "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.3",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"content-type": "^2.0.0",
"json-with-bigint": "^3.5.3",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@actions/artifact/node_modules/@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@actions/artifact/node_modules/before-after-hook": {
"version": "4.0.0",
"license": "Apache-2.0"
@@ -227,6 +269,19 @@
"node": ">= 20"
}
},
"node_modules/@actions/github/node_modules/@octokit/endpoint": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
"integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@actions/github/node_modules/@octokit/graphql": {
"version": "9.0.3",
"license": "MIT",
@@ -239,6 +294,21 @@
"node": ">= 20"
}
},
"node_modules/@actions/github/node_modules/@octokit/plugin-paginate-rest": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@actions/github/node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
"license": "MIT",
@@ -252,6 +322,35 @@
"@octokit/core": ">=6"
}
},
"node_modules/@actions/github/node_modules/@octokit/request": {
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.10.tgz",
"integrity": "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.3",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"content-type": "^2.0.0",
"json-with-bigint": "^3.5.3",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@actions/github/node_modules/@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@actions/github/node_modules/before-after-hook": {
"version": "4.0.0",
"license": "Apache-2.0"
@@ -520,13 +619,16 @@
}
},
"node_modules/@azure/logger": {
"version": "1.1.4",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz",
"integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==",
"license": "MIT",
"dependencies": {
"@typespec/ts-http-runtime": "^0.3.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
}
},
"node_modules/@azure/storage-blob": {
@@ -1793,167 +1895,10 @@
],
"license": "MIT"
},
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core": {
"version": "5.2.1",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"license": "MIT",
"peer": true
},
"node_modules/@octokit/core/node_modules/@octokit/types": {
"version": "13.10.0",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/endpoint": {
"version": "9.0.6",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"license": "MIT"
},
"node_modules/@octokit/endpoint/node_modules/@octokit/types": {
"version": "13.10.0",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/graphql": {
"version": "7.1.1",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/request": "^8.4.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"license": "MIT",
"peer": true
},
"node_modules/@octokit/graphql/node_modules/@octokit/types": {
"version": "13.10.0",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/openapi-types": {
"version": "27.0.0",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "9.2.2",
"license": "MIT",
"dependencies": {
"@octokit/types": "^12.6.0"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "5"
}
},
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
"version": "20.0.0",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
"version": "12.6.0",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^20.0.0"
}
},
"node_modules/@octokit/request": {
"version": "8.4.1",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^9.0.6",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/request-error": {
"version": "5.1.1",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"license": "MIT"
},
"node_modules/@octokit/request-error/node_modules/@octokit/types": {
"version": "13.10.0",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/request/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"license": "MIT"
},
"node_modules/@octokit/request/node_modules/@octokit/types": {
"version": "13.10.0",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/types": {
"version": "16.0.0",
"license": "MIT",
@@ -3210,11 +3155,6 @@
],
"license": "MIT"
},
"node_modules/before-after-hook": {
"version": "2.2.3",
"license": "Apache-2.0",
"peer": true
},
"node_modules/binary": {
"version": "0.3.0",
"license": "MIT",
@@ -3640,6 +3580,19 @@
"version": "0.0.1",
"license": "MIT"
},
"node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"dev": true,
@@ -3848,10 +3801,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/deprecation": {
"version": "2.3.1",
"license": "ISC"
},
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -6246,6 +6195,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/json-with-bigint": {
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz",
"integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==",
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
"dev": true,
@@ -6803,6 +6758,7 @@
},
"node_modules/once": {
"version": "1.4.0",
"dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -8385,10 +8341,6 @@
"version": "1.0.6",
"license": "MIT"
},
"node_modules/universal-user-agent": {
"version": "6.0.1",
"license": "ISC"
},
"node_modules/unrs-resolver": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
@@ -8714,6 +8666,7 @@
},
"node_modules/wrappy": {
"version": "1.0.2",
"dev": true,
"license": "ISC"
},
"node_modules/write-file-atomic": {
-12
View File
@@ -68,17 +68,5 @@
"prettier": "3.8.4",
"ts-jest": "29.4.11",
"typescript": "5.9.3"
},
"overrides": {
"@azure/logger": "1.1.4",
"@octokit/request": "8.4.1",
"@octokit/request-error": "5.1.1",
"@octokit/plugin-paginate-rest": "9.2.2",
"shell-quote": "1.8.4",
"fast-xml-parser": "5.8.0",
"fast-xml-builder": "1.2.0",
"eslint": {
"brace-expansion": "5.0.6"
}
}
}
+75 -13
View File
@@ -4,7 +4,9 @@ import * as glob from '@actions/glob'
import * as path from 'path'
import {BuildResult} from './build-results'
import {CacheOptions, CacheService} from './cache-service'
import {CacheEntryReport, CacheOptions, CacheReport, CacheService} from './cache-service'
const ENTRY_NAME = 'Gradle User Home'
const PRIMARY_KEY_STATE = 'BASIC_CACHE_PRIMARY_KEY'
const RESTORED_KEY_STATE = 'BASIC_CACHE_RESTORED_KEY'
@@ -40,21 +42,39 @@ export class BasicCacheService implements CacheService {
}
}
async save(gradleUserHome: string, _buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string> {
if (cacheOptions.readOnly) {
const restoredKey = core.getState(RESTORED_KEY_STATE)
if (restoredKey) {
return `Basic caching was read-only. Restored from cache key \`${restoredKey}\`.`
}
return 'Basic caching was read-only. No cache entry was found to restore.'
}
async save(gradleUserHome: string, _buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<CacheReport> {
const primaryKey = core.getState(PRIMARY_KEY_STATE)
const restoredKey = core.getState(RESTORED_KEY_STATE)
if (cacheOptions.readOnly) {
return {
status: 'read-only',
entries: [
entryReport({
primaryKey,
restoredKey,
restoredOutcome: restoredKey
? '(Entry restored: exact match found)'
: '(Entry not restored: no match found)',
savedOutcome: '(Entry not saved: cache is read-only)'
})
]
}
}
if (restoredKey === primaryKey) {
core.info(`Basic caching restored entry with key \`${primaryKey}\`. Save was skipped.`)
return `Basic caching restored entry with key \`${primaryKey}\`. Save was skipped.`
return {
status: 'enabled',
entries: [
entryReport({
primaryKey,
restoredKey,
restoredOutcome: '(Entry restored: exact match found)',
savedOutcome: '(Entry not saved: entry with key already exists)'
})
]
}
}
const cachePaths = getCachePaths(gradleUserHome)
@@ -62,14 +82,56 @@ export class BasicCacheService implements CacheService {
try {
await cache.saveCache(cachePaths, primaryKey)
core.info(`Basic caching saved entry with key: ${primaryKey}`)
return `Basic caching saved entry with key \`${primaryKey}\`.`
return {
status: 'enabled',
entries: [
entryReport({
primaryKey,
restoredKey,
savedKey: primaryKey,
restoredOutcome: restoredKey
? '(Entry restored: exact match found)'
: '(Entry not restored: no match found)',
savedOutcome: '(Entry saved)'
})
]
}
} catch (error) {
core.warning(`Basic caching failed to save entry with key \`${primaryKey}\`: ${error}`)
return `Basic caching save failed: ${error}`
return {
status: 'enabled',
entries: [
entryReport({
primaryKey,
restoredKey,
restoredOutcome: restoredKey
? '(Entry restored: exact match found)'
: '(Entry not restored: no match found)',
savedOutcome: `(Entry not saved: ${error})`
})
]
}
}
}
}
function entryReport(opts: {
primaryKey: string
restoredKey?: string
savedKey?: string
restoredOutcome: string
savedOutcome: string
}): CacheEntryReport {
return {
entryName: ENTRY_NAME,
requestedKey: opts.primaryKey || undefined,
restoredKey: opts.restoredKey || undefined,
restoredOutcome: opts.restoredOutcome,
savedKey: opts.savedKey || undefined,
savedOutcome: opts.savedOutcome
}
}
function getCachePaths(gradleUserHome: string): string[] {
return [path.join(gradleUserHome, 'caches'), path.join(gradleUserHome, 'wrapper')]
}
+21 -48
View File
@@ -5,57 +5,24 @@ import {pathToFileURL} from 'url'
import {CacheConfig, CacheProvider} from './configuration'
import {BasicCacheService} from './cache-service-basic'
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.
`
import {CacheOptions, CacheReport, CacheService} from './cache-service'
import {ProviderNote} from './caching-report'
const ENHANCED_CACHE_MESSAGE = `Enhanced Caching: This build is using the proprietary 'gradle-actions-caching' provider for optimized caching support. See https://github.com/gradle/actions/blob/main/DISTRIBUTION.md for terms of use and opt-out instructions.`
const ENHANCED_CACHE_SUMMARY = `
> [!NOTE]
> ### ⚡️ Enhanced Caching enabled
> This build provides optimized caching support via the proprietary **gradle-actions-caching** provider.
> See [DISTRIBUTION.md](https://github.com/gradle/actions/blob/main/DISTRIBUTION.md) for terms of use and opt-out instructions.
`
const BASIC_CACHE_MESSAGE = `Basic Caching: This build uses the open-source caching provider for reliable, path-based caching of Gradle dependencies. Upgrade available: for faster builds and advanced features, consider switching to the Enhanced Caching provider. See https://github.com/gradle/actions/blob/main/DISTRIBUTION.md for details.`
const BASIC_CACHE_SUMMARY = `
> [!NOTE]
> ### 🛡️ Basic Caching enabled
> This build uses the open-source caching provider for reliable, path-based caching of Gradle dependencies.
>
> **Upgrade Available:** For faster builds and advanced features, consider switching to the **Enhanced Caching** provider.
> See [DISTRIBUTION.md](https://github.com/gradle/actions/blob/main/DISTRIBUTION.md) for details.`
const BASIC_CACHE_MESSAGE = `Basic Caching: This build uses the basic open-source caching provider. For faster builds and advanced features, consider switching to the Enhanced Caching provider. See https://github.com/gradle/actions/blob/main/DISTRIBUTION.md for details.`
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
private summary: string
constructor(delegate: CacheService, summary: string) {
this.delegate = delegate
this.summary = summary
}
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${this.summary}`
async save(
_gradleUserHome: string,
_buildResults: BuildResult[],
_cacheOptions: CacheOptions
): Promise<CacheReport> {
return {status: 'disabled', entries: []}
}
}
@@ -67,16 +34,22 @@ export async function getCacheService(cacheConfig: CacheConfig): Promise<CacheSe
if (cacheConfig.getCacheProvider() === CacheProvider.Basic) {
logCacheMessage(BASIC_CACHE_MESSAGE)
return new LicenseWarningCacheService(new BasicCacheService(), BASIC_CACHE_SUMMARY)
return new BasicCacheService()
}
logCacheMessage(ENHANCED_CACHE_MESSAGE)
const cacheService = await loadVendoredCacheService()
if (cacheConfig.isCacheLicenseAccepted()) {
return cacheService
}
return loadVendoredCacheService()
}
return new LicenseWarningCacheService(cacheService, ENHANCED_CACHE_SUMMARY)
/**
* Identifies the caching provider for the Job Summary. Returns `undefined` when
* caching is disabled, since no provider is engaged in that case.
*/
export function getProviderNote(cacheConfig: CacheConfig): ProviderNote | undefined {
if (cacheConfig.isCacheDisabled()) {
return undefined
}
return cacheConfig.getCacheProvider() === CacheProvider.Basic ? {kind: 'basic'} : {kind: 'enhanced'}
}
export async function loadVendoredCacheService(): Promise<CacheService> {
+39 -1
View File
@@ -12,7 +12,45 @@ export interface CacheOptions {
excludes: string[]
}
export type CacheStatus =
| 'enabled'
| 'read-only'
| 'write-only'
| 'disabled'
| 'disabled-existing-home'
| 'not-available'
export type CacheCleanupStatus =
| 'enabled'
| 'disabled-param'
| 'disabled-failure'
| 'disabled-config-cache-hit'
| 'disabled-readonly'
export interface CacheEntryReport {
entryName: string
requestedKey?: string
restoredKey?: string
restoredSize?: number
restoredTime?: number
restoredOutcome: string
savedKey?: string
savedSize?: number
savedTime?: number
savedOutcome: string
}
/**
* Structured result of a cache save operation. Rendering this into a human-readable
* Job Summary is handled centrally by `caching-report.ts`.
*/
export interface CacheReport {
status: CacheStatus
cleanup?: CacheCleanupStatus
entries: CacheEntryReport[]
}
export interface CacheService {
restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>
save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string>
save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<CacheReport>
}
+168
View File
@@ -0,0 +1,168 @@
import {CacheCleanupStatus, CacheEntryReport, CacheReport, CacheStatus} from './cache-service'
const DOCS = 'https://github.com/gradle/actions/blob/main/docs/setup-gradle.md'
const DISTRIBUTION = 'https://github.com/gradle/actions/blob/main/DISTRIBUTION.md'
/**
* Identifies the caching provider in use, so the report can attribute the cache
* and surface the relevant terms-of-use / upgrade information.
*/
export interface ProviderNote {
kind: 'enhanced' | 'basic'
}
const STATUS_COPY: Record<CacheStatus, string> = {
enabled: `[Cache was enabled](${DOCS}#caching-build-state-between-jobs) — Gradle User Home was restored from the cache and saved for use by subsequent jobs.`,
'read-only': `[Cache was read-only](${DOCS}#using-the-cache-read-only) — by default, the action only writes to the cache for jobs running on the default branch.`,
'write-only': `[Cache was write-only](${DOCS}#using-the-cache-write-only) — Gradle User Home was not restored from the cache.`,
disabled: `[Caching was disabled](${DOCS}#disabling-caching) — Gradle User Home was not restored from or saved to the cache.`,
'disabled-existing-home': `⚠️ [Caching was skipped](${DOCS}#overwriting-an-existing-gradle-user-home) — a pre-existing Gradle User Home was found, so the cache was not restored or saved.`,
'not-available': `Caching is not available — the GitHub Actions cache service could not be reached, so Gradle User Home was not restored or saved.`
}
const CLEANUP_COPY: Record<CacheCleanupStatus, string> = {
enabled: `[Cache cleanup](${DOCS}#configuring-cache-cleanup) purged stale files from Gradle User Home before saving.`,
'disabled-param': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) was disabled via action parameter.`,
'disabled-failure': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) was skipped due to a build failure. Use \`cache-cleanup: always\` to override.`,
'disabled-config-cache-hit': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) was skipped due to configuration-cache reuse.`,
'disabled-readonly': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) is always disabled when the cache is read-only.`
}
/**
* Renders a cache report into the unified Job Summary markdown, with a consistent
* skeleton across every variant: a section heading, a status line, an integrated
* provider note, and (when there are entries) an expandable details section.
*/
export function renderCachingReport(report: CacheReport, providerNote?: ProviderNote): string {
if (!isActive(report.status)) {
// Disabled / skipped / unavailable: a compact heading + status line, no expandable section.
return `${renderHeading(report.status, providerNote)}\n\n${STATUS_COPY[report.status]}\n`
}
const sections = [
renderHeading(report.status, providerNote),
renderProviderNote(providerNote),
// Status and cleanup messages live inside the details expando; if there are no entries
// to expand, fall back to showing the status line directly.
report.entries.length > 0 ? renderDetails(report) : STATUS_COPY[report.status]
]
return `${sections.filter(section => section !== undefined && section !== '').join('\n\n')}\n`
}
function isActive(status: CacheStatus): boolean {
return status === 'enabled' || status === 'read-only' || status === 'write-only'
}
function renderHeading(status: CacheStatus, providerNote?: ProviderNote): string {
if (!isActive(status)) {
const label =
status === 'disabled-existing-home' ? 'Skipped' : status === 'not-available' ? 'Unavailable' : 'Disabled'
return `<h4>Gradle State Caching - ${label}</h4>`
}
const icon = providerNote?.kind === 'basic' ? '🛡️' : '⚡'
const provider = providerNote?.kind === 'basic' ? 'Basic' : 'Enhanced'
const suffix = status === 'read-only' ? ' (read-only)' : status === 'write-only' ? ' (write-only)' : ''
return `<h4>Gradle State Caching - ${icon} ${provider}${suffix}</h4>`
}
function renderCleanupLine(cleanup?: CacheCleanupStatus): string | undefined {
return cleanup ? CLEANUP_COPY[cleanup] : undefined
}
function renderProviderNote(providerNote?: ProviderNote): string | undefined {
if (!providerNote) {
return undefined
}
if (providerNote.kind === 'enhanced') {
return `**[Enhanced Caching](${DOCS}#enhanced-caching)** uses the proprietary \`gradle-actions-caching\` provider. See [DISTRIBUTION.md](${DISTRIBUTION}) for terms of use and opt-out instructions.`
}
return `**[Basic Caching](${DOCS}#basic-caching)** uses the basic open-source caching provider. For faster builds and advanced features, consider the **[Enhanced Caching](${DOCS}#enhanced-caching)** provider.`
}
function renderDetails(report: CacheReport): string {
const entries = report.entries
const restored = entries.filter(entry => entry.restoredKey).length
const saved = entries.filter(entry => entry.savedKey).length
const summary = hasMetrics(entries)
? `Entries: ${restored} restored (${getSize(entries, e => e.restoredSize)}Mb), ${saved} saved (${getSize(entries, e => e.savedSize)}Mb) - Expand for more details`
: `Entries: ${restored} restored, ${saved} saved - Expand for more details`
const cleanup = report.status === 'enabled' ? renderCleanupLine(report.cleanup) : undefined
const table = renderEntryTable(report.entries)
const pre = `<pre>\n${renderEntryDetails(report.entries)}</pre>`
const body = [STATUS_COPY[report.status], cleanup, table, pre].filter(Boolean).join('\n\n')
return `<details>
<summary>${summary}</summary>
${body}
</details>`
}
function hasMetrics(entries: CacheEntryReport[]): boolean {
return entries.some(entry => entry.restoredSize || entry.restoredTime || entry.savedSize || entry.savedTime)
}
function renderEntryTable(entries: CacheEntryReport[]): string {
if (!hasMetrics(entries)) {
return ''
}
return `<table>
<tr><td></td><th>Count</th><th>Total Size (Mb)</th><th>Total Time (ms)</th></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(entries: CacheEntryReport[]): string {
return entries
.map(
entry => `Entry: ${entry.entryName}
Requested Key : ${entry.requestedKey ?? ''}
Restored Key : ${entry.restoredKey ?? ''}
Size: ${formatSize(entry.restoredSize)}
Time: ${formatTime(entry.restoredTime)}
${entry.restoredOutcome}
Saved Key : ${entry.savedKey ?? ''}
Size: ${formatSize(entry.savedSize)}
Time: ${formatTime(entry.savedTime)}
${entry.savedOutcome}
`
)
.join('---\n')
}
function getCount(entries: CacheEntryReport[], predicate: (value: CacheEntryReport) => number | undefined): number {
return entries.filter(e => predicate(e)).length
}
function getSize(entries: CacheEntryReport[], predicate: (value: CacheEntryReport) => number | undefined): number {
const bytes = entries.map(e => predicate(e) ?? 0).reduce((p, v) => p + v, 0)
return Math.round(bytes / (1024 * 1024))
}
function getTime(entries: CacheEntryReport[], predicate: (value: CacheEntryReport) => number | undefined): number {
return entries.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`
}
+4 -5
View File
@@ -167,11 +167,6 @@ export class CacheConfig {
return core.getMultilineInput('gradle-home-cache-excludes')
}
isCacheLicenseAccepted(): boolean {
const dvConfig = new DevelocityConfig()
return dvConfig.getDevelocityAccessKey() !== '' || dvConfig.hasTermsOfUseAgreement()
}
getCacheProvider(): CacheProvider {
const val = core.getInput('cache-provider')
switch (val.toLowerCase().trim()) {
@@ -206,6 +201,10 @@ export class SummaryConfig {
return this.shouldAddJobSummary(this.getJobSummaryOption(), hasFailure)
}
canAddPRComment(): boolean {
return this.getPRCommentOption() !== JobSummaryOption.Never
}
shouldAddPRComment(hasFailure: boolean): boolean {
return this.shouldAddJobSummary(this.getPRCommentOption(), hasFailure)
}
+87 -3
View File
@@ -2,12 +2,15 @@ import * as core from '@actions/core'
import * as github from '@actions/github'
import {BuildResult} from './build-results'
import {SummaryConfig, getActionId, getGithubToken} from './configuration'
import {CacheReport} from './cache-service'
import {ProviderNote, renderCachingReport} from './caching-report'
import {DependencyGraphConfig, getActionId, getGithubToken, getJobMatrix, SummaryConfig} from './configuration'
import {Deprecation, getDeprecations, getErrors} from './deprecation-collector'
export async function generateJobSummary(
buildResults: BuildResult[],
cachingReport: string,
cacheReport: CacheReport,
providerNote: ProviderNote | undefined,
config: SummaryConfig
): Promise<void> {
const errors = renderErrors()
@@ -18,6 +21,7 @@ export async function generateJobSummary(
}
const summaryTable = renderSummaryTable(buildResults)
const cachingReport = renderCachingReport(cacheReport, providerNote)
const hasFailure = anyFailed(buildResults)
if (config.shouldGenerateJobSummary(hasFailure)) {
core.info('Generating Job Summary')
@@ -33,6 +37,10 @@ export async function generateJobSummary(
core.info('============================')
}
if (config.canAddPRComment()) {
await minimizeObsoletePRComments()
}
if (config.shouldAddPRComment(hasFailure)) {
await addPRComment(summaryTable)
}
@@ -48,7 +56,8 @@ async function addPRComment(jobSummary: string): Promise<void> {
const pull_request_number = context.payload.pull_request.number
core.info(`Adding Job Summary as comment to PR #${pull_request_number}.`)
const prComment = `<h3>Job Summary for Gradle</h3>
const prComment = `${jobMarker(context)}
<h3>Job Summary for Gradle</h3>
<a href="${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}" target="_blank">
<h5>${context.workflow} :: <em>${context.job}</em></h5>
</a>
@@ -57,6 +66,7 @@ ${jobSummary}`
const github_token = getGithubToken()
const octokit = github.getOctokit(github_token)
try {
await octokit.rest.issues.createComment({
...context.repo,
@@ -201,3 +211,77 @@ function truncateString(str: string, maxLength: number): string {
return str
}
}
async function minimizeObsoletePRComments(): Promise<void> {
const context = github.context
if (context.payload.pull_request == null) {
core.info('No pull_request trigger detected: not minimizing obsolete PR comments')
return
}
const prNumber = context.payload.pull_request.number
core.info(`Minimizing obsolete Job Summary comments on PR #${prNumber}.`)
const marker = jobMarker(context)
const octokit = github.getOctokit(getGithubToken())
const {owner, repo} = context.repo
const query = `
query($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
comments(last: 100) {
nodes { id body isMinimized url }
}
}
}
}
`
let comments: PullRequestComment[]
try {
const {repository} = await octokit.graphql<CommentsQueryResult>(query, {owner, repo, prNumber})
comments = repository.pullRequest?.comments?.nodes?.filter((c): c is PullRequestComment => c !== null) ?? []
} catch (error) {
return core.warning(`Failed to fetch comments: ${error}`)
}
const mutation = `
mutation($id: ID!) {
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
clientMutationId
}
}
`
const commentsToMinimize = comments
.filter(c => !c.isMinimized && c.body.includes(marker))
.map(async c =>
octokit
.graphql(mutation, {id: c.id})
.then(() => core.info(`Successfully minimized (id:${c.id}, url:${c.url})`))
.catch(e => core.warning(`Failed to minimize (id:${c.id}, url:${c.url}, error:${e?.message || e})`))
)
await Promise.allSettled(commentsToMinimize)
}
export function jobMarker(context: typeof github.context): string {
const jobCorrelator = DependencyGraphConfig.constructJobCorrelator(context.workflow, context.job, getJobMatrix())
return `<!-- gradle-job-summary: ${jobCorrelator} -->`
}
interface PullRequestComment {
id: string
body: string
isMinimized: boolean
url: string
}
interface CommentsQueryResult {
repository: {
pullRequest?: {
comments?: {
nodes?: (PullRequestComment | null)[] | null
} | null
} | null
}
}
+3 -3
View File
@@ -7,7 +7,7 @@ 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 {getCacheService, getProviderNote} from './cache-service-loader'
import {CacheOptions} from './cache-service'
import {
DevelocityConfig,
@@ -65,8 +65,8 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
const gradleUserHome = core.getState(GRADLE_USER_HOME)
const cacheService = await getCacheService(cacheConfig)
const cachingReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
await jobSummary.generateJobSummary(buildResults, cachingReport, summaryConfig)
const cacheReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
await jobSummary.generateJobSummary(buildResults, cacheReport, getProviderNote(cacheConfig), summaryConfig)
markBuildResultsProcessed()
+11 -7
View File
@@ -138,8 +138,8 @@ describe('BasicCacheService', () => {
})
expect(mockSaveCache).not.toHaveBeenCalled()
expect(report).toContain('read-only')
expect(report).toContain(PRIMARY_KEY)
expect(report.status).toBe('read-only')
expect(report.entries[0].restoredKey).toBe(PRIMARY_KEY)
})
it('reports readOnly with no restore when cache was missed', async () => {
@@ -156,8 +156,9 @@ describe('BasicCacheService', () => {
})
expect(mockSaveCache).not.toHaveBeenCalled()
expect(report).toContain('read-only')
expect(report).toContain('No cache entry')
expect(report.status).toBe('read-only')
expect(report.entries[0].restoredKey).toBeUndefined()
expect(report.entries[0].restoredOutcome).toContain('not restored')
})
it('skips save when restored key equals primary key', async () => {
@@ -179,7 +180,8 @@ describe('BasicCacheService', () => {
})
expect(mockSaveCache).not.toHaveBeenCalled()
expect(report).toContain('Save was skipped')
expect(report.status).toBe('enabled')
expect(report.entries[0].savedOutcome).toContain('already exists')
})
it('saves cache and returns report on success', async () => {
@@ -204,7 +206,9 @@ describe('BasicCacheService', () => {
['/home/.gradle/caches', '/home/.gradle/wrapper'],
PRIMARY_KEY
)
expect(report).toContain('saved entry with key')
expect(report.status).toBe('enabled')
expect(report.entries[0].savedKey).toBe(PRIMARY_KEY)
expect(report.entries[0].savedOutcome).toBe('(Entry saved)')
})
it('warns on save failure instead of throwing', async () => {
@@ -228,7 +232,7 @@ describe('BasicCacheService', () => {
expect(mockWarning).toHaveBeenCalledWith(
expect.stringContaining('failed to save')
)
expect(report).toContain('failed')
expect(report.entries[0].savedOutcome).toContain('not saved')
})
})
})
+39 -10
View File
@@ -12,8 +12,7 @@ describe('getCacheService selection logic', () => {
const {getCacheService} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => true,
getCacheProvider: () => CacheProvider.Enhanced,
isCacheLicenseAccepted: () => true
getCacheProvider: () => CacheProvider.Enhanced
} as unknown as CacheConfig
const service = await getCacheService(mockConfig)
@@ -28,23 +27,53 @@ describe('getCacheService selection logic', () => {
excludes: []
})
// NoOpCacheService returns a specific report mentioning cache was disabled
expect(report).toContain('Cache was disabled')
// NoOpCacheService reports a disabled cache with no entries
expect(report.status).toBe('disabled')
expect(report.entries).toHaveLength(0)
})
it('wraps BasicCacheService with LicenseWarningCacheService when cache-provider is basic', async () => {
it('returns a BasicCacheService when cache-provider is basic', async () => {
const {getCacheService} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => false,
getCacheProvider: () => CacheProvider.Basic,
isCacheLicenseAccepted: () => false
getCacheProvider: () => CacheProvider.Basic
} as unknown as CacheConfig
const service = await getCacheService(mockConfig)
// The service should not be a bare BasicCacheService — it should be wrapped
// with LicenseWarningCacheService that appends the basic caching summary
const {BasicCacheService} = await import('../../src/cache-service-basic')
expect(service).not.toBeInstanceOf(BasicCacheService)
expect(service).toBeInstanceOf(BasicCacheService)
})
describe('getProviderNote', () => {
it('returns undefined when cache is disabled', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => true,
getCacheProvider: () => CacheProvider.Enhanced
} as unknown as CacheConfig
expect(getProviderNote(mockConfig)).toBeUndefined()
})
it('returns basic note for the basic provider', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => false,
getCacheProvider: () => CacheProvider.Basic
} as unknown as CacheConfig
expect(getProviderNote(mockConfig)).toEqual({kind: 'basic'})
})
it('returns enhanced note for the enhanced provider', async () => {
const {getProviderNote} = await import('../../src/cache-service-loader')
const mockConfig = {
isCacheDisabled: () => false,
getCacheProvider: () => CacheProvider.Enhanced
} as unknown as CacheConfig
expect(getProviderNote(mockConfig)).toEqual({kind: 'enhanced'})
})
})
})
+110
View File
@@ -0,0 +1,110 @@
import {describe, expect, it} from '@jest/globals'
import {CacheReport} from '../../src/cache-service'
import {renderCachingReport} from '../../src/caching-report'
const ENHANCED = {kind: 'enhanced'} as const
const BASIC = {kind: 'basic'} as const
function entry(overrides: Partial<CacheReport['entries'][number]> = {}): CacheReport['entries'][number] {
return {
entryName: 'Gradle User Home',
requestedKey: 'gradle-home-v1|key',
restoredKey: 'gradle-home-v1|key',
restoredSize: 535792,
restoredTime: 253,
restoredOutcome: '(Entry restored: exact match found)',
savedKey: 'gradle-home-v1|key',
savedSize: 528509,
savedTime: 257,
savedOutcome: '(Entry saved)',
...overrides
}
}
describe('renderCachingReport', () => {
it('renders an enhanced read-only report with heading, note and details', () => {
const report: CacheReport = {status: 'read-only', cleanup: 'disabled-readonly', entries: [entry()]}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('<h4>Gradle State Caching - ⚡ Enhanced (read-only)</h4>')
expect(md).toContain('[Enhanced Caching]')
expect(md).toContain('`gradle-actions-caching`')
expect(md).toContain('DISTRIBUTION.md')
expect(md).toContain('<details>')
expect(md).toContain('<summary>Entries: 1 restored (1Mb), 1 saved (1Mb) - Expand for more details</summary>')
// status message moves inside the details expando
expect(md).toContain('Cache was read-only')
// read-only does not render the cleanup line
expect(md).not.toContain('Cache cleanup')
})
it('renders an enhanced enabled report with status and cleanup inside the details', () => {
const report: CacheReport = {status: 'enabled', cleanup: 'enabled', entries: [entry()]}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('<h4>Gradle State Caching - ⚡ Enhanced</h4>')
// status and cleanup messages are within the expando, after the summary
const detailsBody = md.slice(md.indexOf('</summary>'))
expect(detailsBody).toContain('Cache was enabled')
expect(detailsBody).toContain('Cache cleanup')
expect(md).toContain('<table>')
expect(md).toContain('Entries Restored')
})
it('renders a basic report with the upgrade note and no metrics table', () => {
const report: CacheReport = {
status: 'read-only',
entries: [
entry({
restoredSize: undefined,
restoredTime: undefined,
savedKey: undefined,
savedSize: undefined,
savedTime: undefined,
savedOutcome: '(Entry not saved: cache is read-only)'
})
]
}
const md = renderCachingReport(report, BASIC)
expect(md).toContain('<h4>Gradle State Caching - 🛡️ Basic (read-only)</h4>')
expect(md).toContain('[Basic Caching]')
expect(md).toContain('[Enhanced Caching]')
// DISTRIBUTION.md is only referenced for the enhanced provider
expect(md).not.toContain('DISTRIBUTION.md')
// No size/time data, so the metrics table is omitted but the entry list remains
expect(md).not.toContain('<table>')
expect(md).toContain('<pre>')
expect(md).toContain('<summary>Entries: 1 restored, 0 saved - Expand for more details</summary>')
})
it('renders a compact disabled report with no note and no details', () => {
const report: CacheReport = {status: 'disabled', entries: []}
const md = renderCachingReport(report, undefined)
expect(md).toContain('<h4>Gradle State Caching - Disabled</h4>')
expect(md).toContain('Caching was disabled')
expect(md).not.toContain('<details>')
expect(md).not.toContain('DISTRIBUTION.md')
})
it('renders a compact skipped report for a pre-existing Gradle User Home', () => {
const report: CacheReport = {status: 'disabled-existing-home', entries: []}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('<h4>Gradle State Caching - Skipped</h4>')
expect(md).toContain('pre-existing Gradle User Home')
expect(md).not.toContain('<details>')
// no provider note for non-active states
expect(md).not.toContain('DISTRIBUTION.md')
})
it('renders an unavailable report compactly', () => {
const report: CacheReport = {status: 'not-available', entries: []}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('<h4>Gradle State Caching - Unavailable</h4>')
expect(md).not.toContain('<details>')
})
})
+33 -2
View File
@@ -1,8 +1,15 @@
import dedent from 'dedent'
import {describe, expect, it} from '@jest/globals'
import * as github from '@actions/github'
import {afterEach, describe, expect, it} from '@jest/globals'
import {BuildResult} from '../../src/build-results'
import {renderSummaryTable} from '../../src/job-summary'
import {jobMarker, renderSummaryTable} from '../../src/job-summary'
const MATRIX_INPUT_ENV = 'INPUT_WORKFLOW-JOB-CONTEXT'
function fakeContext(workflow: string, job: string): typeof github.context {
return {workflow, job} as unknown as typeof github.context
}
const successfulHelpBuild: BuildResult = {
rootProjectName: 'root',
@@ -177,3 +184,27 @@ describe('renderSummaryTable', () => {
})
})
})
describe('jobMarker', () => {
const original = process.env[MATRIX_INPUT_ENV]
afterEach(() => {
if (original === undefined) {
delete process.env[MATRIX_INPUT_ENV]
} else {
process.env[MATRIX_INPUT_ENV] = original
}
})
it('builds a hidden marker from the workflow and job', () => {
process.env[MATRIX_INPUT_ENV] = 'null'
const marker = jobMarker(fakeContext('CI', 'build'))
expect(marker).toBe('<!-- gradle-job-summary: ci-build -->')
})
it('includes the job matrix in the marker', () => {
process.env[MATRIX_INPUT_ENV] = JSON.stringify({os: 'ubuntu', java: '17'})
const marker = jobMarker(fakeContext('CI', 'build'))
expect(marker).toBe('<!-- gradle-job-summary: ci-build-ubuntu-17 -->')
})
})
+29 -2
View File
@@ -11,6 +11,23 @@ export declare interface BuildResult {
get buildScanFailed(): boolean;
}
/** @public */
export declare type CacheCleanupStatus = 'enabled' | 'disabled-param' | 'disabled-failure' | 'disabled-config-cache-hit' | 'disabled-readonly';
/** @public */
export declare interface CacheEntryReport {
entryName: string;
requestedKey?: string;
restoredKey?: string;
restoredSize?: number;
restoredTime?: number;
restoredOutcome: string;
savedKey?: string;
savedSize?: number;
savedTime?: number;
savedOutcome: string;
}
/** @public */
export declare interface CacheOptions {
disabled: boolean;
@@ -18,16 +35,26 @@ export declare interface CacheOptions {
writeOnly: boolean;
overwriteExisting: boolean;
strictMatch: boolean;
cleanup: string;
cleanup: 'always' | 'on-success' | 'never';
encryptionKey?: string;
includes: string[];
excludes: string[];
}
/** @public */
export declare interface CacheReport {
status: CacheStatus;
cleanup?: CacheCleanupStatus;
entries: CacheEntryReport[];
}
/** @public */
export declare type CacheStatus = 'enabled' | 'read-only' | 'write-only' | 'disabled' | 'disabled-existing-home' | 'not-available';
/** @public */
export declare function restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>;
/** @public */
export declare function save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<string>;
export declare function save(gradleUserHome: string, buildResults: BuildResult[], cacheOptions: CacheOptions): Promise<CacheReport>;
export { }
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gradle-actions-caching",
"version": "0.3.0",
"version": "0.7.0",
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",
+1 -1
View File
@@ -5,7 +5,7 @@
"toolPackages": [
{
"packageName": "@microsoft/api-extractor",
"packageVersion": "7.57.6"
"packageVersion": "7.58.8"
}
]
}
+4 -1
View File
@@ -14,4 +14,7 @@ inputs:
outputs:
failed-wrapper:
type: string
type: list
separator: '|'
list-item:
type: string