Compare commits

..

6 Commits

Author SHA1 Message Date
Bot Githubaction e0e2e10af8 Test with UNRELEASED gradle-actions-caching library v0.8.0-cc 2026-06-13 16:58:37 -06:00
Daz DeBoer 3c7c4f09eb Thread Develocity trial credentials into the save-path cache options
Supplies develocityAccessToken / develocityServerUrl to the gated
project-entry caching feature, on the save path only. Restore stays ungated,
so it keeps passing no DevelocityConfig and the credentials remain undefined.

- cacheOptionsFrom gains an optional develocityConfig; when present it sets
  develocityServerUrl from getDevelocityUrl() and resolves develocityAccessToken
  via resolveAccessKeyForServer (fail-closed when the URL or matching key is
  absent).
- complete() takes a DevelocityConfig and passes it through to cacheOptionsFrom
  at the save call; setup()/restore continues to call cacheOptionsFrom with no
  DevelocityConfig.
- Both post actions (setup-gradle, dependency-submission) pass
  new DevelocityConfig() to complete().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:38:49 -06:00
Daz DeBoer e0e493dd02 Add resolveAccessKeyForServer for the project-cache trial
Resolves the Develocity access key matching a given server URL, for threading
into the new develocityAccessToken cache option (next commit). Reuses
DevelocityAccessCredentials.parse and fails closed (returns undefined) when the
access key is empty/malformed, the server URL is empty/unparseable, or no key
matches the server host. Tolerates a bare hostname with no scheme.

Extends short-lived-token.test.ts with coverage for full URLs, bare hostnames,
host-only matching (ignoring scheme/port/path), multi-key selection, and the
fail-closed cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:34:44 -06:00
Daz DeBoer 3283118ee4 Mirror project-cache type and report changes from the caching library
Prepares the consumer for the gated project-entry caching feature in the
gradle-actions-caching library. Source-only; the vendored bundle is refreshed
in a later commit.

- cache-service.ts: add develocityAccessToken / develocityServerUrl to the
  local CacheOptions mirror; replace ConfigurationCacheStatus (4 values) with
  the 9-value ProjectCacheStatus (not-active retired); rename
  CacheReport.configurationCache to projectCache.
- caching-report.ts: PROJECT_CACHE_COPY is an exhaustive
  Record<ProjectCacheStatus, string> so a missed status fails compilation;
  not-enabled maps to '' (dropped by the existing .filter(Boolean)).
  renderConfigCacheLine becomes renderProjectCacheLine, reading
  report.projectCache.
- Tests updated for the renamed field and new copy, including a not-enabled
  case that renders nothing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:42:39 -06:00
bot-githubaction 54d3208a40 [bot] Update dist directory 2026-06-13 01:52:50 +00:00
Daz DeBoer e993c93d71 Render configuration-cache status in the caching Job Summary (#989)
Render the configuration-cache restore-state in the caching Job Summary,
driven by the new `CacheReport.configurationCache` field produced by the
`gradle-actions-caching` provider.

## What's here

- `cache-service.ts`: add a `ConfigurationCacheStatus` type
(`not-active` / `restored` / `not-restored` / `restore-incomplete`) and
an optional `configurationCache` field on `CacheReport`.
- `caching-report.ts`: a `CONFIG_CACHE_COPY` map and a prominent status
line in `renderCachingReport`, beside the cleanup line. The `not-active`
case links to the `#cache-encryption-key` docs.

## Cross-repo dependency

The field is populated by gradle/actions-caching PR #75 ("Restore
configuration-cache support for simple builds"). This rendering compiles
independently (it uses this repo's own `CacheReport` type) and renders
nothing until the vendored `gradle-actions-caching` bundle is refreshed
from that branch — so this should land with/after the vendor refresh.

## Verification

`npm run check` clean; full Jest suite (366 tests) passes, including 3
new rendering tests.

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:51:57 -06:00
19 changed files with 494 additions and 326 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+54 -54
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+101 -101
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
import * as setupGradle from '../../setup-gradle'
import {CacheConfig, SummaryConfig} from '../../configuration'
import {CacheConfig, DevelocityConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors'
import {forceExit} from '../../force-exit'
@@ -14,7 +14,7 @@ process.on('uncaughtException', e => handlePostActionError(e))
*/
export async function run(): Promise<void> {
try {
await setupGradle.complete(new CacheConfig(), new SummaryConfig())
await setupGradle.complete(new CacheConfig(), new DevelocityConfig(), new SummaryConfig())
} catch (error) {
handlePostActionError(error)
}
+2 -2
View File
@@ -1,7 +1,7 @@
import * as setupGradle from '../../setup-gradle'
import * as dependencyGraph from '../../dependency-graph'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../../configuration'
import {CacheConfig, DependencyGraphConfig, DevelocityConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors'
import {emitDeprecationWarnings, restoreDeprecationState} from '../../deprecation-collector'
import {forceExit} from '../../force-exit'
@@ -19,7 +19,7 @@ export async function run(): Promise<void> {
restoreDeprecationState()
emitDeprecationWarnings()
if (await setupGradle.complete(new CacheConfig(), new SummaryConfig())) {
if (await setupGradle.complete(new CacheConfig(), new DevelocityConfig(), new SummaryConfig())) {
// Only submit the dependency graphs once per job
await dependencyGraph.complete(new DependencyGraphConfig())
}
+16
View File
@@ -8,6 +8,8 @@ export interface CacheOptions {
strictMatch: boolean
cleanup: string
encryptionKey?: string
develocityAccessToken?: string
develocityServerUrl?: string
includes: string[]
excludes: string[]
}
@@ -27,6 +29,19 @@ export type CacheCleanupStatus =
| 'disabled-config-cache-hit'
| 'disabled-readonly'
// Mirrors ProjectCacheStatus in the gradle-actions-caching library. The first three are set on
// restore (ungated); the rest on save, reflecting the opt-in + Develocity trial gate.
export type ProjectCacheStatus =
| 'restore-incomplete'
| 'restored'
| 'not-restored'
| 'not-enabled'
| 'trial-expired'
| 'trial-not-licensed'
| 'not-stored-no-develocity-plugin'
| 'stored'
| 'stored-no-configuration-cache'
export interface CacheEntryReport {
entryName: string
requestedKey?: string
@@ -47,6 +62,7 @@ export interface CacheEntryReport {
export interface CacheReport {
status: CacheStatus
cleanup?: CacheCleanupStatus
projectCache?: ProjectCacheStatus
entries: CacheEntryReport[]
}
+23 -2
View File
@@ -1,4 +1,4 @@
import {CacheCleanupStatus, CacheEntryReport, CacheReport, CacheStatus} from './cache-service'
import {CacheCleanupStatus, CacheEntryReport, CacheReport, CacheStatus, ProjectCacheStatus} 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'
@@ -28,6 +28,21 @@ const CLEANUP_COPY: Record<CacheCleanupStatus, string> = {
'disabled-readonly': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) is always disabled when the cache is read-only.`
}
const PROJECT_CACHE_COPY: Record<ProjectCacheStatus, string> = {
// Restore (ungated).
'restore-incomplete': `Project state was not restored — the Gradle User Home was not fully restored.`,
restored: `Project state (build-logic and configuration cache) was restored from the cache.`,
'not-restored': `Project state was not restored — no cached data was available (e.g. the first run for this cache key).`,
// Save, Tier A gate. 'not-enabled' renders nothing (dropped by the .filter(Boolean) below).
'not-enabled': ``,
'trial-expired': `Project state was not cached — the Develocity caching trial has expired.`,
'trial-not-licensed': `Project state was not cached — a valid Develocity trial token is required.`,
// Save, post-gate outcomes.
'not-stored-no-develocity-plugin': `Project state was not cached — applying the Develocity plugin is required to cache build-logic and configuration-cache state.`,
stored: `Project state (build-logic and configuration cache) was saved to the cache.`,
'stored-no-configuration-cache': `Build-logic state was cached. Storing configuration-cache data requires a build running Gradle >= 8.6 and a [valid encryption key](${DOCS}#cache-encryption-key).`
}
/**
* 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
@@ -69,6 +84,11 @@ function renderCleanupLine(cleanup?: CacheCleanupStatus): string | undefined {
return cleanup ? CLEANUP_COPY[cleanup] : undefined
}
function renderProjectCacheLine(projectCache?: ProjectCacheStatus): string | undefined {
// PROJECT_CACHE_COPY['not-enabled'] is '', which the .filter(Boolean) at the call site drops.
return projectCache ? PROJECT_CACHE_COPY[projectCache] : undefined
}
function renderProviderNote(providerNote?: ProviderNote): string | undefined {
if (!providerNote) {
return undefined
@@ -88,9 +108,10 @@ function renderDetails(report: CacheReport): string {
: `Entries: ${restored} restored, ${saved} saved - Expand for more details`
const cleanup = report.status === 'enabled' ? renderCleanupLine(report.cleanup) : undefined
const projectCache = renderProjectCacheLine(report.projectCache)
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')
const body = [STATUS_COPY[report.status], cleanup, projectCache, table, pre].filter(Boolean).join('\n\n')
return `<details>
<summary>${summary}</summary>
@@ -174,3 +174,22 @@ export class DevelocityAccessCredentials {
return this.accessKeyRegexp.test(allKeys)
}
}
/**
* Resolve the access key that matches a given Develocity server, for use as the
* `develocityAccessToken` cache option. Returns `undefined` (fail-closed) when the access key is
* empty/malformed, the server URL is empty/unparseable, or no key matches the server's host.
*/
export function resolveAccessKeyForServer(accessKey: string, serverUrl: string): string | undefined {
const creds = DevelocityAccessCredentials.parse(accessKey)
if (!creds || !serverUrl) {
return undefined
}
let host: string
try {
host = new URL(serverUrl).hostname
} catch {
host = serverUrl // tolerate a bare hostname (no scheme)
}
return creds.keys.find(k => k.hostname === host)?.key
}
+21 -3
View File
@@ -5,6 +5,7 @@ import * as path from 'path'
import * as os from 'os'
import * as jobSummary from './job-summary'
import * as buildScan from './develocity/build-scan'
import {resolveAccessKeyForServer} from './develocity/short-lived-token'
import {loadBuildResults, markBuildResultsProcessed} from './build-results'
import {getCacheService, getProviderNote} from './cache-service-loader'
@@ -54,7 +55,11 @@ export async function setup(
return true
}
export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryConfig): Promise<boolean> {
export async function complete(
cacheConfig: CacheConfig,
develocityConfig: DevelocityConfig,
summaryConfig: SummaryConfig
): Promise<boolean> {
if (!core.getState(GRADLE_SETUP_VAR)) {
core.info('Gradle setup post-action only performed for first gradle/actions step in workflow.')
return false
@@ -65,7 +70,11 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
const gradleUserHome = core.getState(GRADLE_USER_HOME)
const cacheService = await getCacheService(cacheConfig)
const cacheReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig))
const cacheReport = await cacheService.save(
gradleUserHome,
buildResults,
cacheOptionsFrom(cacheConfig, develocityConfig)
)
await jobSummary.generateJobSummary(buildResults, cacheReport, getProviderNote(cacheConfig), summaryConfig)
markBuildResultsProcessed()
@@ -75,7 +84,14 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
return true
}
function cacheOptionsFrom(config: CacheConfig): CacheOptions {
function cacheOptionsFrom(config: CacheConfig, develocityConfig?: DevelocityConfig): CacheOptions {
// Trial credentials are threaded only on the save path (when develocityConfig is provided).
// On restore they stay undefined, which is fine: project-entry restore is ungated.
const develocityServerUrl = develocityConfig?.getDevelocityUrl() || undefined
const develocityAccessToken =
develocityConfig && develocityServerUrl
? resolveAccessKeyForServer(develocityConfig.getDevelocityAccessKey(), develocityServerUrl)
: undefined
return {
disabled: config.isCacheDisabled(),
readOnly: config.isCacheReadOnly(),
@@ -84,6 +100,8 @@ function cacheOptionsFrom(config: CacheConfig): CacheOptions {
strictMatch: config.isCacheStrictMatch(),
cleanup: config.getCacheCleanupOption(),
encryptionKey: config.getCacheEncryptionKey() || undefined,
develocityAccessToken,
develocityServerUrl,
includes: config.getCacheIncludes(),
excludes: config.getCacheExcludes()
}
+48
View File
@@ -79,6 +79,54 @@ describe('renderCachingReport', () => {
expect(md).toContain('<summary>Entries: 1 restored, 0 saved - Expand for more details</summary>')
})
it('renders the project-cache status line inside the details', () => {
const report: CacheReport = {
status: 'enabled',
cleanup: 'enabled',
projectCache: 'restored',
entries: [entry()]
}
const md = renderCachingReport(report, ENHANCED)
const detailsBody = md.slice(md.indexOf('</summary>'))
expect(detailsBody).toContain(
'Project state (build-logic and configuration cache) was restored from the cache.'
)
})
it('explains an omitted configuration cache with a link to the encryption key docs', () => {
const report: CacheReport = {
status: 'enabled',
cleanup: 'enabled',
projectCache: 'stored-no-configuration-cache',
entries: [entry()]
}
const md = renderCachingReport(report, ENHANCED)
expect(md).toContain('Build-logic state was cached.')
expect(md).toContain('#cache-encryption-key')
})
it('renders nothing for the not-enabled project-cache status', () => {
const report: CacheReport = {
status: 'enabled',
cleanup: 'enabled',
projectCache: 'not-enabled',
entries: [entry()]
}
const md = renderCachingReport(report, ENHANCED)
expect(md).not.toContain('Project state')
expect(md).not.toContain('Build-logic state')
})
it('omits the project-cache line when the status is absent', () => {
const report: CacheReport = {status: 'enabled', cleanup: 'enabled', entries: [entry()]}
const md = renderCachingReport(report, ENHANCED)
expect(md).not.toContain('Project state')
})
it('renders a compact disabled report with no note and no details', () => {
const report: CacheReport = {status: 'disabled', entries: []}
const md = renderCachingReport(report, undefined)
+35 -1
View File
@@ -1,7 +1,7 @@
import nock from "nock";
import {describe, expect, it} from '@jest/globals'
import {DevelocityAccessCredentials, getToken} from "../../src/develocity/short-lived-token";
import {DevelocityAccessCredentials, getToken, resolveAccessKeyForServer} from "../../src/develocity/short-lived-token";
describe('short lived tokens', () => {
it('parse valid access key should return an object', async () => {
@@ -134,3 +134,37 @@ describe('short lived tokens with retry', () => {
.toBeNull()
})
})
describe('resolveAccessKeyForServer', () => {
it('returns the key matching the server host from a full URL', () => {
expect(resolveAccessKeyForServer('ge.example.com=key1;other=key2', 'https://ge.example.com')).toBe('key1')
})
it('matches on hostname, ignoring scheme, port and path', () => {
expect(resolveAccessKeyForServer('ge.example.com=key1', 'https://ge.example.com:8443/path')).toBe('key1')
})
it('tolerates a bare hostname with no scheme', () => {
expect(resolveAccessKeyForServer('ge.example.com=key1', 'ge.example.com')).toBe('key1')
})
it('selects the matching key when multiple are present', () => {
expect(resolveAccessKeyForServer('dev=key1;ge.example.com=key2', 'https://ge.example.com')).toBe('key2')
})
it('returns undefined when no key matches the server host', () => {
expect(resolveAccessKeyForServer('ge.example.com=key1', 'https://other.example.com')).toBeUndefined()
})
it('returns undefined for an empty server URL', () => {
expect(resolveAccessKeyForServer('ge.example.com=key1', '')).toBeUndefined()
})
it('returns undefined for an empty access key', () => {
expect(resolveAccessKeyForServer('', 'https://ge.example.com')).toBeUndefined()
})
it('returns undefined for a malformed access key', () => {
expect(resolveAccessKeyForServer('not-a-valid-access-key', 'https://ge.example.com')).toBeUndefined()
})
})
+12
View File
@@ -37,6 +37,8 @@ export declare interface CacheOptions {
strictMatch: boolean;
cleanup: 'always' | 'on-success' | 'never';
encryptionKey?: string;
develocityAccessToken?: string;
develocityServerUrl?: string;
includes: string[];
excludes: string[];
}
@@ -45,12 +47,22 @@ export declare interface CacheOptions {
export declare interface CacheReport {
status: CacheStatus;
cleanup?: CacheCleanupStatus;
projectCache?: ProjectCacheStatus;
entries: CacheEntryReport[];
}
/** @public */
export declare type CacheStatus = 'enabled' | 'read-only' | 'write-only' | 'disabled' | 'disabled-existing-home' | 'not-available';
/**
* Status of project-entry caching (build-logic artifacts + configuration-cache data) for a run.
* The first three are set on restore (always ungated); the rest are set on save and reflect the
* two-tier gate (opt-in + Develocity trial, then encryption key + Gradle version). Still beta.
*
* @public
*/
declare type ProjectCacheStatus = 'restore-incomplete' | 'restored' | 'not-restored' | 'not-enabled' | 'trial-expired' | 'trial-not-licensed' | 'not-stored-no-develocity-plugin' | 'stored' | 'stored-no-configuration-cache';
/** @public */
export declare function restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>;
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.7.0",
"version": "0.8.0-cc",
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",