mirror of
https://github.com/gradle/actions.git
synced 2026-08-18 03:42:51 +00:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
528e626588 | ||
|
|
3711048846 | ||
|
|
4318659b28 | ||
|
|
79b19cd50f | ||
|
|
9d31058114 | ||
|
|
0088877924 | ||
|
|
ff4dbcd5fa | ||
|
|
6550634d3e | ||
|
|
b128da9bf0 | ||
|
|
ee7ea9a078 | ||
|
|
9c445f57df | ||
|
|
bbaaec0da2 | ||
|
|
54d3208a40 | ||
|
|
e993c93d71 |
@@ -3,3 +3,6 @@
|
||||
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
# Marker file written when the 'test' task is configured
|
||||
task-configured.txt
|
||||
|
||||
@@ -14,6 +14,16 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
smoke-tests:
|
||||
uses: ./.github/workflows/suite-smoke-test-caching.yml
|
||||
concurrency:
|
||||
group: CI-smoke-test-caching
|
||||
cancel-in-progress: false
|
||||
with:
|
||||
skip-dist: true
|
||||
runner-os: '["ubuntu-latest", "windows-latest"]'
|
||||
secrets: inherit
|
||||
|
||||
caching-integ-tests:
|
||||
uses: ./.github/workflows/suite-integ-test-caching.yml
|
||||
concurrency:
|
||||
|
||||
@@ -21,9 +21,19 @@ jobs:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Build and upload distribution
|
||||
if: ${{ needs.determine-suite.outputs.suite != 'full' }}
|
||||
uses: ./.github/actions/build-dist
|
||||
|
||||
smoke-tests:
|
||||
needs: build-distribution
|
||||
uses: ./.github/workflows/suite-smoke-test-caching.yml
|
||||
concurrency:
|
||||
group: CI-smoke-test-caching-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
with:
|
||||
skip-dist: false
|
||||
runner-os: '["ubuntu-latest", "windows-latest"]'
|
||||
secrets: inherit
|
||||
|
||||
caching-integ-tests:
|
||||
needs: build-distribution
|
||||
uses: ./.github/workflows/suite-integ-test-caching.yml
|
||||
@@ -64,6 +74,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
needs:
|
||||
- build-distribution
|
||||
- smoke-tests
|
||||
- caching-integ-tests
|
||||
- other-integ-tests
|
||||
- dependency-submission-integ-tests
|
||||
@@ -74,6 +85,7 @@ jobs:
|
||||
run: |
|
||||
echo "One or more integ-test jobs did not succeed:"
|
||||
echo " build-distribution: ${{ needs.build-distribution.result }}"
|
||||
echo " smoke-tests: ${{ needs.smoke-tests.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 }}"
|
||||
|
||||
@@ -43,29 +43,6 @@ jobs:
|
||||
working-directory: .github/workflow-samples/groovy-dsl
|
||||
run: ./gradlew test
|
||||
|
||||
# Test that the gradle-user-home cache will cache dependencies, by running build with --offline
|
||||
restore-gradle-home-dependencies-cache:
|
||||
needs: restore-gradle-home-seed-build
|
||||
strategy:
|
||||
max-parallel: 1
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ${{fromJSON(inputs.runner-os)}}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Initialize integ-test
|
||||
uses: ./.github/actions/init-integ-test
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: ./setup-gradle
|
||||
with:
|
||||
cache-read-only: true
|
||||
- name: Execute Gradle build with --offline
|
||||
working-directory: .github/workflow-samples/groovy-dsl
|
||||
run: ./gradlew test --offline
|
||||
|
||||
# Test that the gradle-user-home cache will cache and restore local build-cache
|
||||
restore-gradle-home-build-cache:
|
||||
needs: restore-gradle-home-seed-build
|
||||
@@ -114,15 +91,16 @@ jobs:
|
||||
working-directory: .github/workflow-samples/groovy-dsl
|
||||
run: ./gradlew test
|
||||
|
||||
# Test that a pre-existing gradle-user-home can be overwritten by the restored cache
|
||||
# Test that a pre-existing gradle-user-home can be overwritten by the restored cache.
|
||||
#
|
||||
# Deliberately not run against the 'runner-os' matrix: creating ~/.gradle up-front is precisely
|
||||
# what stops setup-gradle relocating the Gradle User Home to D:\a\.gradle on Windows, so this job
|
||||
# would look for a cache entry rooted at a different path than the one the seed build saved.
|
||||
# Cache entries are identified by key *and* by a version derived from the cache paths, so that
|
||||
# never matches.
|
||||
restore-gradle-home-pre-existing-gradle-home:
|
||||
needs: restore-gradle-home-seed-build
|
||||
strategy:
|
||||
max-parallel: 1
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ${{fromJSON(inputs.runner-os)}}
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
+4
-1
@@ -39,9 +39,12 @@ jobs:
|
||||
with:
|
||||
cache-provider: basic
|
||||
cache-read-only: false # For testing, allow writing cache entries on non-default branches
|
||||
# Basic caching does no daemon management, so the workflow must ensure no daemon is left
|
||||
# holding locks on the Gradle User Home when the post-action save runs. Without this, `tar`
|
||||
# cannot read the Gradle '*.lock' files on Windows and the cache entry is never saved.
|
||||
- name: Build kotlin-dsl project
|
||||
working-directory: .github/workflow-samples/kotlin-dsl
|
||||
run: ./gradlew build
|
||||
run: ./gradlew build --no-daemon
|
||||
|
||||
basic-cache-verify-build:
|
||||
needs: basic-cache-seed-build
|
||||
@@ -0,0 +1,69 @@
|
||||
name: Smoke test restore Gradle Home
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
cache-key-prefix:
|
||||
type: string
|
||||
default: '0'
|
||||
runner-os:
|
||||
type: string
|
||||
default: '["ubuntu-latest"]'
|
||||
skip-dist:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
SKIP_DIST: ${{ inputs.skip-dist }}
|
||||
# Distinct from the keys used by integ-test-restore-gradle-home.yml, which seeds an equivalent
|
||||
# cache entry. Both suites run concurrently, so they must not write to the same cache key.
|
||||
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: smoke-test-restore-gradle-home-${{ inputs.cache-key-prefix }}
|
||||
GRADLE_BUILD_ACTION_CACHE_KEY_JOB: smoke-test-restore-gradle-home
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
restore-gradle-home-seed-build:
|
||||
strategy:
|
||||
max-parallel: 1
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ${{fromJSON(inputs.runner-os)}}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Initialize integ-test
|
||||
uses: ./.github/actions/init-integ-test
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: ./setup-gradle
|
||||
with:
|
||||
cache-read-only: false # For testing, allow writing cache entries on non-default branches
|
||||
- name: Build using Gradle wrapper
|
||||
working-directory: .github/workflow-samples/groovy-dsl
|
||||
run: ./gradlew test
|
||||
|
||||
# Test that the gradle-user-home cache will cache dependencies, by running build with --offline
|
||||
restore-gradle-home-dependencies-cache:
|
||||
needs: restore-gradle-home-seed-build
|
||||
strategy:
|
||||
max-parallel: 1
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ${{fromJSON(inputs.runner-os)}}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
- name: Initialize integ-test
|
||||
uses: ./.github/actions/init-integ-test
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: ./setup-gradle
|
||||
with:
|
||||
cache-read-only: true
|
||||
- name: Execute Gradle build with --offline
|
||||
working-directory: .github/workflow-samples/groovy-dsl
|
||||
run: ./gradlew test --offline
|
||||
@@ -57,9 +57,3 @@ jobs:
|
||||
with:
|
||||
runner-os: '${{ inputs.runner-os }}'
|
||||
skip-dist: ${{ inputs.skip-dist }}
|
||||
|
||||
basic-cache-provider:
|
||||
uses: ./.github/workflows/integ-test-basic-cache-provider.yml
|
||||
with:
|
||||
runner-os: '${{ inputs.runner-os }}'
|
||||
skip-dist: ${{ inputs.skip-dist }}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: suite-smoke-test-caching
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
runner-os:
|
||||
type: string
|
||||
default: '["ubuntu-latest"]'
|
||||
skip-dist:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
restore-gradle-home:
|
||||
uses: ./.github/workflows/smoke-test-restore-gradle-home.yml
|
||||
with:
|
||||
runner-os: '${{ inputs.runner-os }}'
|
||||
skip-dist: ${{ inputs.skip-dist }}
|
||||
|
||||
basic-cache-provider:
|
||||
uses: ./.github/workflows/smoke-test-basic-cache-provider.yml
|
||||
with:
|
||||
runner-os: '${{ inputs.runner-os }}'
|
||||
skip-dist: ${{ inputs.skip-dist }}
|
||||
+106
-105
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
+126
-126
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+105
-105
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+149
-149
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -47,6 +47,7 @@ export async function run(): Promise<void> {
|
||||
-Dorg.gradle.configureondemand=false
|
||||
-Dorg.gradle.dependency.verification=off
|
||||
-Dorg.gradle.unsafe.isolated-projects=false
|
||||
-Dorg.gradle.isolated-projects=false
|
||||
${taskList}
|
||||
${additionalArgs}
|
||||
`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -80,7 +80,28 @@ export class BasicCacheService implements CacheService {
|
||||
const cachePaths = getCachePaths(gradleUserHome)
|
||||
|
||||
try {
|
||||
await cache.saveCache(cachePaths, primaryKey)
|
||||
// A cacheId of -1 means the save failed: `saveCache` reports the underlying cause and returns
|
||||
// normally, rather than throwing. Warn and continue: caching failures should not fail the build.
|
||||
const cacheId = await cache.saveCache(cachePaths, primaryKey)
|
||||
if (cacheId === -1) {
|
||||
core.warning(
|
||||
`Basic caching failed to save entry with key \`${primaryKey}\`. See preceding log output for the cause.`
|
||||
)
|
||||
return {
|
||||
status: 'enabled',
|
||||
entries: [
|
||||
entryReport({
|
||||
primaryKey,
|
||||
restoredKey,
|
||||
restoredOutcome: restoredKey
|
||||
? '(Entry restored: exact match found)'
|
||||
: '(Entry not restored: no match found)',
|
||||
savedOutcome: '(Entry not saved: save failed)'
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
core.info(`Basic caching saved entry with key: ${primaryKey}`)
|
||||
return {
|
||||
status: 'enabled',
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface CacheOptions {
|
||||
strictMatch: boolean
|
||||
cleanup: string
|
||||
encryptionKey?: string
|
||||
develocityAccessToken?: string
|
||||
develocityServerUrl?: string
|
||||
includes: string[]
|
||||
excludes: string[]
|
||||
}
|
||||
@@ -27,6 +29,13 @@ export type CacheCleanupStatus =
|
||||
| 'disabled-config-cache-hit'
|
||||
| '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 {
|
||||
entryName: string
|
||||
requestedKey?: string
|
||||
@@ -47,6 +56,7 @@ export interface CacheEntryReport {
|
||||
export interface CacheReport {
|
||||
status: CacheStatus
|
||||
cleanup?: CacheCleanupStatus
|
||||
projectCache?: ProjectCacheStatus
|
||||
entries: CacheEntryReport[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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,14 @@ 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> = {
|
||||
'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
|
||||
* 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
|
||||
}
|
||||
|
||||
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 +101,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>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as core from '@actions/core'
|
||||
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_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_AGREE', config.getTermsOfUseAgree())
|
||||
}
|
||||
|
||||
return setupToken(
|
||||
config.getDevelocityAccessKey(),
|
||||
config.getDevelocityAllowUntrustedServer(),
|
||||
config.getDevelocityTokenExpiry()
|
||||
)
|
||||
}
|
||||
|
||||
function maybeExportVariable(variableName: string, value: unknown): void {
|
||||
|
||||
@@ -3,28 +3,41 @@ import * as httpm from '@actions/http-client'
|
||||
import {DevelocityConfig} from '../configuration'
|
||||
import {recordDeprecation} from '../deprecation-collector'
|
||||
|
||||
export async function setupToken(
|
||||
develocityAccessKey: string,
|
||||
develocityAllowUntrustedServer: boolean | undefined,
|
||||
develocityTokenExpiry: string
|
||||
): Promise<void> {
|
||||
if (develocityAccessKey) {
|
||||
try {
|
||||
core.debug('Fetching short-lived token...')
|
||||
const tokens = await getToken(develocityAccessKey, develocityAllowUntrustedServer, develocityTokenExpiry)
|
||||
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)
|
||||
} else {
|
||||
handleMissingAccessToken()
|
||||
}
|
||||
} catch (e) {
|
||||
handleMissingAccessToken()
|
||||
core.warning(`Failed to fetch short-lived token, reason: ${e}`)
|
||||
}
|
||||
/**
|
||||
* Exchange the configured Develocity access key(s) for short-lived tokens, export them as the access
|
||||
* key env vars, and return the short-lived token matching the configured Develocity server URL (for
|
||||
* use as the `develocityAccessToken` cache option). Returns `undefined` when there is no access key,
|
||||
* token fetching fails, or no token matches the configured server.
|
||||
*/
|
||||
export async function setupToken(config: DevelocityConfig): Promise<string | undefined> {
|
||||
const develocityAccessKey = config.getDevelocityAccessKey()
|
||||
if (!develocityAccessKey) {
|
||||
return undefined
|
||||
}
|
||||
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 {
|
||||
@@ -174,3 +187,20 @@ export class DevelocityAccessCredentials {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export async function generateJobSummary(
|
||||
providerNote: ProviderNote | undefined,
|
||||
config: SummaryConfig
|
||||
): Promise<void> {
|
||||
core.startGroup('Generating Job Summary')
|
||||
|
||||
const errors = renderErrors()
|
||||
if (errors) {
|
||||
core.summary.addRaw(errors)
|
||||
@@ -23,19 +25,17 @@ 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')
|
||||
|
||||
core.info(summaryTable)
|
||||
core.info('============================')
|
||||
core.info(cachingReport)
|
||||
|
||||
if (config.shouldGenerateJobSummary(hasFailure)) {
|
||||
core.summary.addRaw(summaryTable)
|
||||
core.summary.addRaw(cachingReport)
|
||||
await core.summary.write()
|
||||
} else {
|
||||
core.info('============================')
|
||||
core.info(summaryTable)
|
||||
core.info('============================')
|
||||
core.info(cachingReport)
|
||||
core.info('============================')
|
||||
}
|
||||
core.endGroup()
|
||||
|
||||
if (config.canAddPRComment()) {
|
||||
await minimizeObsoletePRComments()
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ buildscript {
|
||||
def pluginRepositoryUrl = getInputParam('gradle.plugin-repository.url') ?: 'https://plugins.gradle.org/m2'
|
||||
def pluginRepositoryUsername = getInputParam('gradle.plugin-repository.username')
|
||||
def pluginRepositoryPassword = getInputParam('gradle.plugin-repository.password')
|
||||
def dependencyGraphPluginVersion = getInputParam('dependency-graph-plugin.version') ?: '1.4.1'
|
||||
def dependencyGraphPluginVersion = getInputParam('dependency-graph-plugin.version') ?: '1.4.2'
|
||||
|
||||
logger.lifecycle("Resolving dependency graph plugin ${dependencyGraphPluginVersion} from plugin repository: ${pluginRepositoryUrl}")
|
||||
repositories {
|
||||
|
||||
@@ -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 {setupToken} from './develocity/short-lived-token'
|
||||
|
||||
import {loadBuildResults, markBuildResultsProcessed} from './build-results'
|
||||
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_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(
|
||||
cacheConfig: CacheConfig,
|
||||
@@ -44,17 +47,27 @@ export async function setup(
|
||||
|
||||
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)
|
||||
await cacheService.restore(gradleUserHome, cacheOptionsFrom(cacheConfig))
|
||||
await cacheService.restore(gradleUserHome, cacheOptionsFrom(cacheConfig, develocityServerUrl, cacheToken))
|
||||
|
||||
await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome)
|
||||
|
||||
await buildScan.setup(develocityConfig)
|
||||
buildScan.setup(develocityConfig)
|
||||
|
||||
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
|
||||
@@ -64,8 +77,14 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
|
||||
const buildResults = loadBuildResults()
|
||||
|
||||
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 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)
|
||||
|
||||
markBuildResultsProcessed()
|
||||
@@ -75,7 +94,11 @@ export async function complete(cacheConfig: CacheConfig, summaryConfig: SummaryC
|
||||
return true
|
||||
}
|
||||
|
||||
function cacheOptionsFrom(config: CacheConfig): CacheOptions {
|
||||
function cacheOptionsFrom(
|
||||
config: CacheConfig,
|
||||
develocityServerUrl: string | undefined,
|
||||
develocityAccessToken: string | undefined
|
||||
): CacheOptions {
|
||||
return {
|
||||
disabled: config.isCacheDisabled(),
|
||||
readOnly: config.isCacheReadOnly(),
|
||||
@@ -84,6 +107,8 @@ function cacheOptionsFrom(config: CacheConfig): CacheOptions {
|
||||
strictMatch: config.isCacheStrictMatch(),
|
||||
cleanup: config.getCacheCleanupOption(),
|
||||
encryptionKey: config.getCacheEncryptionKey() || undefined,
|
||||
develocityAccessToken,
|
||||
develocityServerUrl,
|
||||
includes: config.getCacheIncludes(),
|
||||
excludes: config.getCacheExcludes()
|
||||
}
|
||||
|
||||
@@ -79,6 +79,41 @@ 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: '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', () => {
|
||||
const report: CacheReport = {status: 'disabled', entries: []}
|
||||
const md = renderCachingReport(report, undefined)
|
||||
|
||||
@@ -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, resolveTokenForServer} from "../../src/develocity/short-lived-token";
|
||||
|
||||
describe('short lived tokens', () => {
|
||||
it('parse valid access key should return an object', async () => {
|
||||
@@ -134,3 +134,42 @@ describe('short lived tokens with retry', () => {
|
||||
.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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 = 'not-enabled' | 'trial-expired' | 'trial-not-licensed' | 'no-encryption-key' | 'enabled';
|
||||
|
||||
/** @public */
|
||||
export declare function restore(gradleUserHome: string, cacheOptions: CacheOptions): Promise<void>;
|
||||
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradle-actions-caching",
|
||||
"version": "0.7.0",
|
||||
"version": "0.9.0",
|
||||
"type": "module",
|
||||
"main": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user