Compare commits

...

16 Commits

Author SHA1 Message Date
bot-githubaction f88b678606 [bot] Update dist directory 2026-06-15 05:09:41 +00:00
Daz DeBoer c484532a21 Update vendored gradle-actions-caching library
Refresh the vendored build to pick up the refined configuration-cache
trial check ordering and log messages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:05:44 -06:00
Daz DeBoer 59a129ea73 Update vendored gradle-actions-caching library
Refresh the vendored build from gradle/actions-caching to pick up the
Develocity access-token JWT validation in the configuration-cache trial
check (issuer matching with distinct failure messages).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:31:26 -06:00
Daz DeBoer f4925085d0 Consolidate short-lived token handling and resolve cache token via state
Have setupToken take the DevelocityConfig and return the short-lived JWT
matching the configured Develocity server, replacing resolveAccessKeyForServer
with resolveTokenForServer. Call setupToken directly from setup-gradle's
setup() (split out of buildScan.setup), persist the resolved token via
core.saveState, and read it back in complete(). cacheOptionsFrom now takes
the server URL and access token as parameters.

This ensures CacheOptions.develocityAccessToken is always the resolved
short-lived JWT on both restore and save, regardless of whether the access
key was supplied as an action input or env var.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:24:47 -06:00
Daz DeBoer 9173e33285 Log Job Summary to make it AI accessible 2026-06-14 21:32:53 -06:00
Daz DeBoer 2f5a111e6f Pass DV credentials on restore 2026-06-13 23:16:08 -06:00
Daz DeBoer 4ecda0ee35 Add missing cache status 2026-06-13 23:16:08 -06:00
Daz DeBoer 79c65f4225 Prettier and fix tests 2026-06-13 23:16:08 -06:00
Daz DeBoer cb8f8c9bc3 Mirron status changes in gradle-actions-caching 2026-06-13 23:16:08 -06:00
Daz DeBoer 88020b1ce1 Make resolveAccessKeyForServer tolerate short-lived tokens
At save time the Develocity access key has been swapped for a short-lived token
(host=<JWT>), whose value the strict DevelocityAccessCredentials parser rejects
(it requires host=\w+). Parse the host=value pairs leniently instead, so the
per-host token resolves for both long-lived keys and short-lived tokens.

Without this, threading credentials into the project-cache trial check fails
with trial-not-licensed on every real save (caught by the configuration-cache
integ-test). Adds a unit case for a JWT-shaped token value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 23:16:08 -06:00
Daz DeBoer d885df93e3 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 23:16:08 -06:00
Daz DeBoer fccb7323bf 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 23:16:07 -06:00
Daz DeBoer ab6eefcd4a 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 23:16:07 -06:00
Bot Githubaction ce13e2edb5 Test with UNRELEASED gradle-actions-caching library v0.8.0-cc 2026-06-14 05:06:55 +00: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
21 changed files with 684 additions and 526 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+125 -125
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+93 -93
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+148 -148
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 * as setupGradle from '../../setup-gradle'
import {CacheConfig, SummaryConfig} from '../../configuration' import {CacheConfig, DevelocityConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors' import {handlePostActionError} from '../../errors'
import {forceExit} from '../../force-exit' import {forceExit} from '../../force-exit'
@@ -14,7 +14,7 @@ process.on('uncaughtException', e => handlePostActionError(e))
*/ */
export async function run(): Promise<void> { export async function run(): Promise<void> {
try { try {
await setupGradle.complete(new CacheConfig(), new SummaryConfig()) await setupGradle.complete(new CacheConfig(), new DevelocityConfig(), new SummaryConfig())
} catch (error) { } catch (error) {
handlePostActionError(error) handlePostActionError(error)
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import * as setupGradle from '../../setup-gradle' import * as setupGradle from '../../setup-gradle'
import * as dependencyGraph from '../../dependency-graph' import * as dependencyGraph from '../../dependency-graph'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../../configuration' import {CacheConfig, DependencyGraphConfig, DevelocityConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors' import {handlePostActionError} from '../../errors'
import {emitDeprecationWarnings, restoreDeprecationState} from '../../deprecation-collector' import {emitDeprecationWarnings, restoreDeprecationState} from '../../deprecation-collector'
import {forceExit} from '../../force-exit' import {forceExit} from '../../force-exit'
@@ -19,7 +19,7 @@ export async function run(): Promise<void> {
restoreDeprecationState() restoreDeprecationState()
emitDeprecationWarnings() 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 // Only submit the dependency graphs once per job
await dependencyGraph.complete(new DependencyGraphConfig()) await dependencyGraph.complete(new DependencyGraphConfig())
} }
+10
View File
@@ -8,6 +8,8 @@ export interface CacheOptions {
strictMatch: boolean strictMatch: boolean
cleanup: string cleanup: string
encryptionKey?: string encryptionKey?: string
develocityAccessToken?: string
develocityServerUrl?: string
includes: string[] includes: string[]
excludes: string[] excludes: string[]
} }
@@ -27,6 +29,13 @@ export type CacheCleanupStatus =
| 'disabled-config-cache-hit' | 'disabled-config-cache-hit'
| 'disabled-readonly' | 'disabled-readonly'
export type ProjectCacheStatus =
| 'not-enabled' // the hidden opt-in env var was not set (rendered as nothing)
| 'trial-expired' // past the hard trial expiry
| 'trial-not-licensed' // Develocity trial token missing or invalid
| 'no-encryption-key' // Cannot store due to missing encryption key
| 'enabled' // Trial in effect: will attempt to save project state
export interface CacheEntryReport { export interface CacheEntryReport {
entryName: string entryName: string
requestedKey?: string requestedKey?: string
@@ -47,6 +56,7 @@ export interface CacheEntryReport {
export interface CacheReport { export interface CacheReport {
status: CacheStatus status: CacheStatus
cleanup?: CacheCleanupStatus cleanup?: CacheCleanupStatus
projectCache?: ProjectCacheStatus
entries: CacheEntryReport[] entries: CacheEntryReport[]
} }
+16 -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 DOCS = 'https://github.com/gradle/actions/blob/main/docs/setup-gradle.md'
const DISTRIBUTION = 'https://github.com/gradle/actions/blob/main/DISTRIBUTION.md' const DISTRIBUTION = 'https://github.com/gradle/actions/blob/main/DISTRIBUTION.md'
@@ -28,6 +28,14 @@ const CLEANUP_COPY: Record<CacheCleanupStatus, string> = {
'disabled-readonly': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) is always disabled when the cache is read-only.` 'disabled-readonly': `[Cache cleanup](${DOCS}#configuring-cache-cleanup) is always disabled when the cache is read-only.`
} }
const PROJECT_CACHE_COPY: Record<ProjectCacheStatus, string> = {
'not-enabled': ``,
'trial-expired': `Project state (build-logic and configuration cache) was not cached - the Develocity caching trial has expired.`,
'trial-not-licensed': `Project state (build-logic and configuration cache) was not cached - a develocity-access-key and develocity-server-url is required.`,
'no-encryption-key': `Project state (build-logic and configuration cache) was not cached - a [cache-encryption-key](${DOCS}#cache-encryption-key) is required.`,
enabled: `Caching of project state (build-logic and configuration cache) was enabled.`
}
/** /**
* Renders a cache report into the unified Job Summary markdown, with a consistent * 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 * skeleton across every variant: a section heading, a status line, an integrated
@@ -69,6 +77,11 @@ function renderCleanupLine(cleanup?: CacheCleanupStatus): string | undefined {
return cleanup ? CLEANUP_COPY[cleanup] : 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 { function renderProviderNote(providerNote?: ProviderNote): string | undefined {
if (!providerNote) { if (!providerNote) {
return undefined return undefined
@@ -88,9 +101,10 @@ function renderDetails(report: CacheReport): string {
: `Entries: ${restored} restored, ${saved} saved - Expand for more details` : `Entries: ${restored} restored, ${saved} saved - Expand for more details`
const cleanup = report.status === 'enabled' ? renderCleanupLine(report.cleanup) : undefined const cleanup = report.status === 'enabled' ? renderCleanupLine(report.cleanup) : undefined
const projectCache = renderProjectCacheLine(report.projectCache)
const table = renderEntryTable(report.entries) const table = renderEntryTable(report.entries)
const pre = `<pre>\n${renderEntryDetails(report.entries)}</pre>` 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> return `<details>
<summary>${summary}</summary> <summary>${summary}</summary>
+1 -8
View File
@@ -1,8 +1,7 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {DevelocityConfig} from '../configuration' import {DevelocityConfig} from '../configuration'
import {setupToken} from './short-lived-token'
export async function setup(config: DevelocityConfig): Promise<void> { export function setup(config: DevelocityConfig): void {
maybeExportVariable('DEVELOCITY_INJECTION_INIT_SCRIPT_NAME', 'gradle-actions.inject-develocity.init.gradle') maybeExportVariable('DEVELOCITY_INJECTION_INIT_SCRIPT_NAME', 'gradle-actions.inject-develocity.init.gradle')
maybeExportVariable('DEVELOCITY_INJECTION_CUSTOM_VALUE', 'gradle-actions') maybeExportVariable('DEVELOCITY_INJECTION_CUSTOM_VALUE', 'gradle-actions')
@@ -39,12 +38,6 @@ export async function setup(config: DevelocityConfig): Promise<void> {
maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_URL', config.getTermsOfUseUrl()) maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_URL', config.getTermsOfUseUrl())
maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_AGREE', config.getTermsOfUseAgree()) maybeExportVariable('DEVELOCITY_INJECTION_TERMS_OF_USE_AGREE', config.getTermsOfUseAgree())
} }
return setupToken(
config.getDevelocityAccessKey(),
config.getDevelocityAllowUntrustedServer(),
config.getDevelocityTokenExpiry()
)
} }
function maybeExportVariable(variableName: string, value: unknown): void { function maybeExportVariable(variableName: string, value: unknown): void {
+51 -21
View File
@@ -3,28 +3,41 @@ import * as httpm from '@actions/http-client'
import {DevelocityConfig} from '../configuration' import {DevelocityConfig} from '../configuration'
import {recordDeprecation} from '../deprecation-collector' import {recordDeprecation} from '../deprecation-collector'
export async function setupToken( /**
develocityAccessKey: string, * Exchange the configured Develocity access key(s) for short-lived tokens, export them as the access
develocityAllowUntrustedServer: boolean | undefined, * key env vars, and return the short-lived token matching the configured Develocity server URL (for
develocityTokenExpiry: string * use as the `develocityAccessToken` cache option). Returns `undefined` when there is no access key,
): Promise<void> { * token fetching fails, or no token matches the configured server.
if (develocityAccessKey) { */
try { export async function setupToken(config: DevelocityConfig): Promise<string | undefined> {
core.debug('Fetching short-lived token...') const develocityAccessKey = config.getDevelocityAccessKey()
const tokens = await getToken(develocityAccessKey, develocityAllowUntrustedServer, develocityTokenExpiry) if (!develocityAccessKey) {
if (tokens != null && !tokens.isEmpty()) { return undefined
core.debug(`Got token(s), setting the access key env vars`)
const token = tokens.raw()
core.setSecret(token)
exportAccessKeyEnvVars(token)
} else {
handleMissingAccessToken()
}
} catch (e) {
handleMissingAccessToken()
core.warning(`Failed to fetch short-lived token, reason: ${e}`)
}
} }
try {
core.debug('Fetching short-lived token...')
const tokens = await getToken(
develocityAccessKey,
config.getDevelocityAllowUntrustedServer(),
config.getDevelocityTokenExpiry()
)
if (tokens != null && !tokens.isEmpty()) {
core.debug(`Got token(s), setting the access key env vars`)
const token = tokens.raw()
core.setSecret(token)
exportAccessKeyEnvVars(token)
for (const k of tokens.keys) {
core.setSecret(k.key)
}
const serverUrl = config.getDevelocityUrl()
return serverUrl ? resolveTokenForServer(tokens, serverUrl) : undefined
}
handleMissingAccessToken()
} catch (e) {
handleMissingAccessToken()
core.warning(`Failed to fetch short-lived token, reason: ${e}`)
}
return undefined
} }
function exportAccessKeyEnvVars(value: string): void { function exportAccessKeyEnvVars(value: string): void {
@@ -174,3 +187,20 @@ export class DevelocityAccessCredentials {
return this.accessKeyRegexp.test(allKeys) return this.accessKeyRegexp.test(allKeys)
} }
} }
/**
* Resolve the token whose hostname matches a given Develocity server URL. Returns `undefined`
* (fail-closed) when the server URL is empty or no token matches the server's host.
*/
export function resolveTokenForServer(tokens: DevelocityAccessCredentials, serverUrl: string): string | undefined {
if (!serverUrl) {
return undefined
}
let host: string
try {
host = new URL(serverUrl).hostname
} catch {
host = serverUrl // tolerate a bare hostname (no scheme)
}
return tokens.keys.find(k => k.hostname === host)?.key
}
+8 -8
View File
@@ -13,6 +13,8 @@ export async function generateJobSummary(
providerNote: ProviderNote | undefined, providerNote: ProviderNote | undefined,
config: SummaryConfig config: SummaryConfig
): Promise<void> { ): Promise<void> {
core.startGroup('Generating Job Summary')
const errors = renderErrors() const errors = renderErrors()
if (errors) { if (errors) {
core.summary.addRaw(errors) core.summary.addRaw(errors)
@@ -23,19 +25,17 @@ export async function generateJobSummary(
const summaryTable = renderSummaryTable(buildResults) const summaryTable = renderSummaryTable(buildResults)
const cachingReport = renderCachingReport(cacheReport, providerNote) const cachingReport = renderCachingReport(cacheReport, providerNote)
const hasFailure = anyFailed(buildResults) const hasFailure = anyFailed(buildResults)
if (config.shouldGenerateJobSummary(hasFailure)) {
core.info('Generating Job Summary')
core.info(summaryTable)
core.info('============================')
core.info(cachingReport)
if (config.shouldGenerateJobSummary(hasFailure)) {
core.summary.addRaw(summaryTable) core.summary.addRaw(summaryTable)
core.summary.addRaw(cachingReport) core.summary.addRaw(cachingReport)
await core.summary.write() await core.summary.write()
} else {
core.info('============================')
core.info(summaryTable)
core.info('============================')
core.info(cachingReport)
core.info('============================')
} }
core.endGroup()
if (config.canAddPRComment()) { if (config.canAddPRComment()) {
await minimizeObsoletePRComments() await minimizeObsoletePRComments()
+30 -5
View File
@@ -5,6 +5,7 @@ import * as path from 'path'
import * as os from 'os' import * as os from 'os'
import * as jobSummary from './job-summary' import * as jobSummary from './job-summary'
import * as buildScan from './develocity/build-scan' import * as buildScan from './develocity/build-scan'
import {setupToken} from './develocity/short-lived-token'
import {loadBuildResults, markBuildResultsProcessed} from './build-results' import {loadBuildResults, markBuildResultsProcessed} from './build-results'
import {getCacheService, getProviderNote} from './cache-service-loader' import {getCacheService, getProviderNote} from './cache-service-loader'
@@ -21,6 +22,8 @@ import {initializeGradleUserHome} from './gradle-user-home'
const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED' const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED'
const GRADLE_USER_HOME = 'GRADLE_USER_HOME' const GRADLE_USER_HOME = 'GRADLE_USER_HOME'
// Short-lived Develocity token for the configured server, resolved during setup and reused on save.
const DEVELOCITY_CACHE_TOKEN = 'DEVELOCITY_CACHE_TOKEN'
export async function setup( export async function setup(
cacheConfig: CacheConfig, cacheConfig: CacheConfig,
@@ -44,17 +47,27 @@ export async function setup(
initializeGradleUserHome(userHome, gradleUserHome, cacheConfig.getCacheEncryptionKey()) initializeGradleUserHome(userHome, gradleUserHome, cacheConfig.getCacheEncryptionKey())
// Exchange the long-lived access key(s) for short-lived tokens, resolving the token for the
// configured Develocity server and retaining it for the post-action (save) step.
const develocityServerUrl = develocityConfig.getDevelocityUrl() || undefined
const cacheToken = await setupToken(develocityConfig)
core.saveState(DEVELOCITY_CACHE_TOKEN, cacheToken ?? '')
const cacheService = await getCacheService(cacheConfig) const cacheService = await getCacheService(cacheConfig)
await cacheService.restore(gradleUserHome, cacheOptionsFrom(cacheConfig)) await cacheService.restore(gradleUserHome, cacheOptionsFrom(cacheConfig, develocityServerUrl, cacheToken))
await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome) await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome)
await buildScan.setup(develocityConfig) buildScan.setup(develocityConfig)
return true 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)) { if (!core.getState(GRADLE_SETUP_VAR)) {
core.info('Gradle setup post-action only performed for first gradle/actions step in workflow.') core.info('Gradle setup post-action only performed for first gradle/actions step in workflow.')
return false return false
@@ -64,8 +77,14 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
const buildResults = loadBuildResults() const buildResults = loadBuildResults()
const gradleUserHome = core.getState(GRADLE_USER_HOME) const gradleUserHome = core.getState(GRADLE_USER_HOME)
const develocityServerUrl = develocityConfig.getDevelocityUrl() || undefined
const cacheToken = core.getState(DEVELOCITY_CACHE_TOKEN) || undefined
const cacheService = await getCacheService(cacheConfig) const cacheService = await getCacheService(cacheConfig)
const cacheReport = await cacheService.save(gradleUserHome, buildResults, cacheOptionsFrom(cacheConfig)) const cacheReport = await cacheService.save(
gradleUserHome,
buildResults,
cacheOptionsFrom(cacheConfig, develocityServerUrl, cacheToken)
)
await jobSummary.generateJobSummary(buildResults, cacheReport, getProviderNote(cacheConfig), summaryConfig) await jobSummary.generateJobSummary(buildResults, cacheReport, getProviderNote(cacheConfig), summaryConfig)
markBuildResultsProcessed() markBuildResultsProcessed()
@@ -75,7 +94,11 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
return true return true
} }
function cacheOptionsFrom(config: CacheConfig): CacheOptions { function cacheOptionsFrom(
config: CacheConfig,
develocityServerUrl: string | undefined,
develocityAccessToken: string | undefined
): CacheOptions {
return { return {
disabled: config.isCacheDisabled(), disabled: config.isCacheDisabled(),
readOnly: config.isCacheReadOnly(), readOnly: config.isCacheReadOnly(),
@@ -84,6 +107,8 @@ function cacheOptionsFrom(config: CacheConfig): CacheOptions {
strictMatch: config.isCacheStrictMatch(), strictMatch: config.isCacheStrictMatch(),
cleanup: config.getCacheCleanupOption(), cleanup: config.getCacheCleanupOption(),
encryptionKey: config.getCacheEncryptionKey() || undefined, encryptionKey: config.getCacheEncryptionKey() || undefined,
develocityAccessToken,
develocityServerUrl,
includes: config.getCacheIncludes(), includes: config.getCacheIncludes(),
excludes: config.getCacheExcludes() excludes: config.getCacheExcludes()
} }
+35
View File
@@ -79,6 +79,41 @@ describe('renderCachingReport', () => {
expect(md).toContain('<summary>Entries: 1 restored, 0 saved - Expand for more details</summary>') 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: 'enabled',
entries: [entry()]
}
const md = renderCachingReport(report, ENHANCED)
const detailsBody = md.slice(md.indexOf('</summary>'))
expect(detailsBody).toContain(
'Caching of project state (build-logic and configuration cache) was enabled.'
)
})
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')
})
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', () => { it('renders a compact disabled report with no note and no details', () => {
const report: CacheReport = {status: 'disabled', entries: []} const report: CacheReport = {status: 'disabled', entries: []}
const md = renderCachingReport(report, undefined) const md = renderCachingReport(report, undefined)
+40 -1
View File
@@ -1,7 +1,7 @@
import nock from "nock"; import nock from "nock";
import {describe, expect, it} from '@jest/globals' import {describe, expect, it} from '@jest/globals'
import {DevelocityAccessCredentials, getToken} from "../../src/develocity/short-lived-token"; import {DevelocityAccessCredentials, getToken, resolveTokenForServer} from "../../src/develocity/short-lived-token";
describe('short lived tokens', () => { describe('short lived tokens', () => {
it('parse valid access key should return an object', async () => { it('parse valid access key should return an object', async () => {
@@ -134,3 +134,42 @@ describe('short lived tokens with retry', () => {
.toBeNull() .toBeNull()
}) })
}) })
describe('resolveTokenForServer', () => {
const credentials = (...pairs: [string, string][]): DevelocityAccessCredentials =>
DevelocityAccessCredentials.of(pairs.map(([hostname, key]) => ({hostname, key})))
it('returns the token matching the server host from a full URL', () => {
const tokens = credentials(['ge.example.com', 'key1'], ['other', 'key2'])
expect(resolveTokenForServer(tokens, 'https://ge.example.com')).toBe('key1')
})
it('matches on hostname, ignoring scheme, port and path', () => {
const tokens = credentials(['ge.example.com', 'key1'])
expect(resolveTokenForServer(tokens, 'https://ge.example.com:8443/path')).toBe('key1')
})
it('tolerates a bare hostname with no scheme', () => {
const tokens = credentials(['ge.example.com', 'key1'])
expect(resolveTokenForServer(tokens, 'ge.example.com')).toBe('key1')
})
it('selects the matching token when multiple are present', () => {
const tokens = credentials(['dev', 'key1'], ['ge.example.com', 'key2'])
expect(resolveTokenForServer(tokens, 'https://ge.example.com')).toBe('key2')
})
it('returns undefined when no token matches the server host', () => {
const tokens = credentials(['ge.example.com', 'key1'])
expect(resolveTokenForServer(tokens, 'https://other.example.com')).toBeUndefined()
})
it('returns undefined for an empty server URL', () => {
const tokens = credentials(['ge.example.com', 'key1'])
expect(resolveTokenForServer(tokens, '')).toBeUndefined()
})
it('returns undefined when there are no tokens', () => {
expect(resolveTokenForServer(credentials(), 'https://ge.example.com')).toBeUndefined()
})
})
+12
View File
@@ -37,6 +37,8 @@ export declare interface CacheOptions {
strictMatch: boolean; strictMatch: boolean;
cleanup: 'always' | 'on-success' | 'never'; cleanup: 'always' | 'on-success' | 'never';
encryptionKey?: string; encryptionKey?: string;
develocityAccessToken?: string;
develocityServerUrl?: string;
includes: string[]; includes: string[];
excludes: string[]; excludes: string[];
} }
@@ -45,12 +47,22 @@ export declare interface CacheOptions {
export declare interface CacheReport { export declare interface CacheReport {
status: CacheStatus; status: CacheStatus;
cleanup?: CacheCleanupStatus; cleanup?: CacheCleanupStatus;
projectCache?: ProjectCacheStatus;
entries: CacheEntryReport[]; entries: CacheEntryReport[];
} }
/** @public */ /** @public */
export declare type CacheStatus = 'enabled' | 'read-only' | 'write-only' | 'disabled' | 'disabled-existing-home' | 'not-available'; 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 = 'not-enabled' | 'trial-expired' | 'trial-not-licensed' | 'no-encryption-key' | 'enabled';
/** @public */ /** @public */
export declare function restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>; 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", "name": "gradle-actions-caching",
"version": "0.7.0", "version": "0.8.1-cc",
"type": "module", "type": "module",
"main": "./index.js", "main": "./index.js",
"types": "./index.d.ts", "types": "./index.d.ts",