mirror of
https://github.com/gradle/actions.git
synced 2026-10-16 16:41:34 +00:00
This PR reports the support status of every Gradle version used in a workflow: - **End-of-life** — two or more major versions behind the latest release. - **Out of date** — one major version behind the latest release, or more than two minor versions behind on the current major. The information is surfaced as job annotations and in the Job Summary: an icon beside the Gradle version in the build results table, and a message below it. | version kind | job annotation | version table | below the table | | ------ | ----- | ---- | --------- | | EOL | warning | ⚠️ | expandable section, pointing to the Gradle Security Subscription | | Out of date | notice | ℹ️ | one-line legend, pointing to the Gradle release lifecycle docs | | Current | none | — | — | A single expandable section covers every end-of-life version, naming them in its summary line and listing the affected release lines in its body. The upgrade legend likewise appears once per job, however many versions carry the info icon. Notes on what is deliberately *not* reported: - **Patch releases.** Only the major and minor version are considered, so being on `9.7.0` when `9.7.1` exists is not flagged. - **Pre-releases.** Release candidates, milestones and snapshots are never reported, so testing against a nightly or an RC produces no annotations. The latest Gradle release is determined from the wrapper checksum data already bundled with the action, so no network access is required. That data is refreshed weekly, and the two-minor grace band absorbs the lag. Note that the annotations are emitted independently of `add-job-summary`: setting it to `never` suppresses the Job Summary itself, but the warning and notice annotations remain. ### Examples The `demo-job-summary` workflow has a `support-status-eol-and-outdated` job that builds with three end-of-life versions (`6.9.4`, `7.4`, `7.6.6`), two out-of-date versions (`8.0.2`, `8.14.5`) and the current release, so all three statuses appear in one summary: * **Job Summary**: https://github.com/gradle/actions/actions/runs/34166688755#summary-101879071306 * **Annotations**: https://github.com/gradle/actions/actions/runs/34166688755/job/101879071306 (expand annotations) That workflow is never triggered automatically; run it manually against a branch to review the rendering. ### Implementation * Adds `GradleVersion`, parsing and ordering versions the same way Gradle's own `org.gradle.util.GradleVersion` does. This replaces the previous `versionIsAtLeast` helper and removes the `semver` dependency. * Adds `gradle-support-status.ts`, which owns the classification policy and both of its presentations (annotations and Job Summary section) behind a three-function API, so `job-summary.ts` only asks for the icon and the rendered block. ### Testing * Unit tests for version ordering, classification, the annotations and the rendered summary. * `integ-test-gradle-support-status.yml`, which derives its expected versions from the bundled release data so the assertions cannot drift from the action's own view, then asserts against the real job log. * The demo workflow job linked above, for reviewing the rendering by eye. Documented under [Build reporting](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#gradle-version-support-status) in `docs/setup-gradle.md`. --------- Co-authored-by: Louis Jacomet <[email protected]> Co-authored-by: Daz DeBoer <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
162 lines
6.5 KiB
TypeScript
162 lines
6.5 KiB
TypeScript
import * as core from '@actions/core'
|
|
import * as exec from '@actions/exec'
|
|
import * as fs from 'fs'
|
|
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'
|
|
import {CacheOptions} from './cache-service'
|
|
import {
|
|
DevelocityConfig,
|
|
CacheConfig,
|
|
SummaryConfig,
|
|
WrapperValidationConfig,
|
|
getWorkspaceDirectory
|
|
} from './configuration'
|
|
import * as wrapperValidator from './wrapper-validation/wrapper-validator'
|
|
import {initializeGradleUserHome} from './gradle-user-home'
|
|
import {reportSupportStatus} from './gradle-support-status'
|
|
|
|
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,
|
|
develocityConfig: DevelocityConfig,
|
|
wrapperValidationConfig: WrapperValidationConfig
|
|
): Promise<boolean> {
|
|
const userHome = await determineUserHome()
|
|
const gradleUserHome = await determineGradleUserHome()
|
|
|
|
// Bypass setup on all but first action step in workflow.
|
|
if (process.env[GRADLE_SETUP_VAR]) {
|
|
core.info('Gradle setup only performed on first gradle/actions step in workflow.')
|
|
return false
|
|
}
|
|
// Record setup complete: visible to subsequent actions and prevents duplicate setup
|
|
core.exportVariable(GRADLE_SETUP_VAR, true)
|
|
// Record setup complete: visible in post-action, to control action completion
|
|
core.saveState(GRADLE_SETUP_VAR, true)
|
|
// Save the Gradle User Home for use in the post-action step.
|
|
core.saveState(GRADLE_USER_HOME, gradleUserHome)
|
|
|
|
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, develocityServerUrl, cacheToken))
|
|
|
|
await wrapperValidator.validateWrappers(wrapperValidationConfig, getWorkspaceDirectory(), gradleUserHome)
|
|
|
|
buildScan.setup(develocityConfig)
|
|
|
|
return true
|
|
}
|
|
|
|
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
|
|
}
|
|
core.info('In post-action step')
|
|
|
|
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, develocityServerUrl, cacheToken)
|
|
)
|
|
await jobSummary.generateJobSummary(buildResults, cacheReport, getProviderNote(cacheConfig), summaryConfig)
|
|
reportSupportStatus(buildResults.map(result => result.gradleVersion))
|
|
|
|
markBuildResultsProcessed()
|
|
|
|
core.info('Completed post-action step')
|
|
|
|
return true
|
|
}
|
|
|
|
function cacheOptionsFrom(
|
|
config: CacheConfig,
|
|
develocityServerUrl: string | undefined,
|
|
develocityAccessToken: string | undefined
|
|
): CacheOptions {
|
|
return {
|
|
disabled: config.isCacheDisabled(),
|
|
readOnly: config.isCacheReadOnly(),
|
|
writeOnly: config.isCacheWriteOnly(),
|
|
overwriteExisting: config.isCacheOverwriteExisting(),
|
|
strictMatch: config.isCacheStrictMatch(),
|
|
cleanup: config.getCacheCleanupOption(),
|
|
encryptionKey: config.getCacheEncryptionKey() || undefined,
|
|
develocityAccessToken,
|
|
develocityServerUrl,
|
|
includes: config.getCacheIncludes(),
|
|
excludes: config.getCacheExcludes()
|
|
}
|
|
}
|
|
|
|
async function determineGradleUserHome(): Promise<string> {
|
|
const customGradleUserHome = process.env['GRADLE_USER_HOME']
|
|
if (customGradleUserHome) {
|
|
const rootDir = getWorkspaceDirectory()
|
|
return path.resolve(rootDir, customGradleUserHome)
|
|
}
|
|
|
|
const defaultGradleUserHome = path.resolve(await determineUserHome(), '.gradle')
|
|
// Use the default Gradle User Home if it already exists
|
|
if (fs.existsSync(defaultGradleUserHome)) {
|
|
core.info(`Gradle User Home already exists at ${defaultGradleUserHome}`)
|
|
core.exportVariable('GRADLE_USER_HOME', defaultGradleUserHome)
|
|
return defaultGradleUserHome
|
|
}
|
|
|
|
// Switch Gradle User Home to faster 'D:' drive if possible
|
|
if (os.platform() === 'win32' && defaultGradleUserHome.startsWith('C:\\') && fs.existsSync('D:\\a\\')) {
|
|
const fasterGradleUserHome = 'D:\\a\\.gradle'
|
|
core.info(`Setting GRADLE_USER_HOME to ${fasterGradleUserHome} to leverage (potentially) faster drive.`)
|
|
core.exportVariable('GRADLE_USER_HOME', fasterGradleUserHome)
|
|
return fasterGradleUserHome
|
|
}
|
|
|
|
core.exportVariable('GRADLE_USER_HOME', defaultGradleUserHome)
|
|
return defaultGradleUserHome
|
|
}
|
|
|
|
/**
|
|
* Different values can be returned by os.homedir() in Javascript and System.getProperty('user.home') in Java.
|
|
* In order to determine the correct Gradle User Home, we ask Java for the user home instead of using os.homedir().
|
|
*/
|
|
async function determineUserHome(): Promise<string> {
|
|
const output = await exec.getExecOutput('java', ['-XshowSettings:properties', '-version'], {silent: true})
|
|
const regex = /user\.home = (\S*)/i
|
|
const found = output.stderr.match(regex)
|
|
if (found == null || found.length <= 1) {
|
|
core.info('Could not determine user.home from java -version output. Using os.homedir().')
|
|
return os.homedir()
|
|
}
|
|
const userHome = found[1]
|
|
core.debug(`Determined user.home from java -version output: '${userHome}'`)
|
|
return userHome
|
|
}
|