Replace static config methods with config types

This will allow different entry points to have different inputs.
This commit is contained in:
daz 2024-04-06 20:37:46 -06:00
parent ed4d086d37
commit cfd20ecc0a
No known key found for this signature in database
16 changed files with 337 additions and 323 deletions

View file

@ -1,30 +1,18 @@
import * as core from '@actions/core'
import {getBuildScanPublishEnabled, getBuildScanTermsOfUseUrl, getBuildScanTermsOfUseAgree} from './input-params'
import {BuildScanConfig} from './input-params'
export function setup(): void {
export function setup(config: BuildScanConfig): void {
maybeExportVariable('DEVELOCITY_INJECTION_INIT_SCRIPT_NAME', 'gradle-actions.inject-develocity.init.gradle')
maybeExportVariable('DEVELOCITY_AUTO_INJECTION_CUSTOM_VALUE', 'gradle-actions')
if (getBuildScanPublishEnabled() && verifyTermsOfUseAgreement()) {
if (config.getBuildScanPublishEnabled()) {
maybeExportVariable('DEVELOCITY_INJECTION_ENABLED', 'true')
maybeExportVariable('DEVELOCITY_PLUGIN_VERSION', '3.17')
maybeExportVariable('DEVELOCITY_CCUD_PLUGIN_VERSION', '1.13')
maybeExportVariable('DEVELOCITY_TERMS_OF_USE_URL', getBuildScanTermsOfUseUrl())
maybeExportVariable('DEVELOCITY_TERMS_OF_USE_AGREE', getBuildScanTermsOfUseAgree())
maybeExportVariable('DEVELOCITY_TERMS_OF_USE_URL', config.getBuildScanTermsOfUseUrl())
maybeExportVariable('DEVELOCITY_TERMS_OF_USE_AGREE', config.getBuildScanTermsOfUseAgree())
}
}
function verifyTermsOfUseAgreement(): boolean {
if (
(getBuildScanTermsOfUseUrl() !== 'https://gradle.com/terms-of-service' &&
getBuildScanTermsOfUseUrl() !== 'https://gradle.com/help/legal-terms-of-use') ||
getBuildScanTermsOfUseAgree() !== 'yes'
) {
core.warning(`Terms of use must be agreed in order to publish build scans.`)
return false
}
return true
}
function maybeExportVariable(variableName: string, value: unknown): void {
if (!process.env[variableName]) {
core.exportVariable(variableName, value)

View file

@ -4,7 +4,7 @@ import * as glob from '@actions/glob'
import path from 'path'
import fs from 'fs'
import * as params from './input-params'
import {CacheConfig} from './input-params'
import {CacheListener} from './cache-reporting'
import {saveCache, restoreCache, cacheDebug, isCacheDebuggingEnabled, tryDelete, generateCacheKey} from './cache-utils'
import {GradleHomeEntryExtractor, ConfigurationCacheEntryExtractor} from './cache-extract-entries'
@ -14,15 +14,17 @@ const RESTORED_CACHE_KEY_KEY = 'restored-cache-key'
export const META_FILE_DIR = '.setup-gradle'
export class GradleStateCache {
private cacheConfig: CacheConfig
private cacheName: string
private cacheDescription: string
protected readonly userHome: string
protected readonly gradleUserHome: string
constructor(userHome: string, gradleUserHome: string) {
constructor(userHome: string, gradleUserHome: string, cacheConfig: CacheConfig) {
this.userHome = userHome
this.gradleUserHome = gradleUserHome
this.cacheConfig = cacheConfig
this.cacheName = 'gradle'
this.cacheDescription = 'Gradle User Home'
}
@ -31,7 +33,7 @@ export class GradleStateCache {
this.initializeGradleUserHome()
// Export the GRADLE_ENCRYPTION_KEY variable if provided
const encryptionKey = params.getCacheEncryptionKey()
const encryptionKey = this.cacheConfig.getCacheEncryptionKey()
if (encryptionKey) {
core.exportVariable('GRADLE_ENCRYPTION_KEY', encryptionKey)
}
@ -52,7 +54,7 @@ export class GradleStateCache {
async restore(listener: CacheListener): Promise<void> {
const entryListener = listener.entry(this.cacheDescription)
const cacheKey = generateCacheKey(this.cacheName)
const cacheKey = generateCacheKey(this.cacheName, this.cacheConfig)
cacheDebug(
`Requesting ${this.cacheDescription} with
@ -82,8 +84,8 @@ export class GradleStateCache {
*/
async afterRestore(listener: CacheListener): Promise<void> {
await this.debugReportGradleUserHomeSize('as restored from cache')
await new GradleHomeEntryExtractor(this.gradleUserHome).restore(listener)
await new ConfigurationCacheEntryExtractor(this.gradleUserHome).restore(listener)
await new GradleHomeEntryExtractor(this.gradleUserHome, this.cacheConfig).restore(listener)
await new ConfigurationCacheEntryExtractor(this.gradleUserHome, this.cacheConfig).restore(listener)
await this.debugReportGradleUserHomeSize('after restoring common artifacts')
}
@ -95,7 +97,7 @@ export class GradleStateCache {
* it is saved with the exact key.
*/
async save(listener: CacheListener): Promise<void> {
const cacheKey = generateCacheKey(this.cacheName).key
const cacheKey = generateCacheKey(this.cacheName, this.cacheConfig).key
const restoredCacheKey = core.getState(RESTORED_CACHE_KEY_KEY)
const gradleHomeEntryListener = listener.entry(this.cacheDescription)
@ -133,8 +135,8 @@ export class GradleStateCache {
await this.debugReportGradleUserHomeSize('before saving common artifacts')
await this.deleteExcludedPaths()
await Promise.all([
new GradleHomeEntryExtractor(this.gradleUserHome).extract(listener),
new ConfigurationCacheEntryExtractor(this.gradleUserHome).extract(listener)
new GradleHomeEntryExtractor(this.gradleUserHome, this.cacheConfig).extract(listener),
new ConfigurationCacheEntryExtractor(this.gradleUserHome, this.cacheConfig).extract(listener)
])
await this.debugReportGradleUserHomeSize(
"after extracting common artifacts (only 'caches' and 'notifications' will be stored)"
@ -145,7 +147,7 @@ export class GradleStateCache {
* Delete any file paths that are excluded by the `gradle-home-cache-excludes` parameter.
*/
private async deleteExcludedPaths(): Promise<void> {
const rawPaths: string[] = params.getCacheExcludes()
const rawPaths: string[] = this.cacheConfig.getCacheExcludes()
rawPaths.push('caches/*/cc-keystore')
const resolvedPaths = rawPaths.map(x => path.resolve(this.gradleUserHome, x))
@ -168,7 +170,7 @@ export class GradleStateCache {
* but this can be overridden by the `gradle-home-cache-includes` parameter.
*/
protected getCachePath(): string[] {
const rawPaths: string[] = params.getCacheIncludes()
const rawPaths: string[] = this.cacheConfig.getCacheIncludes()
rawPaths.push(META_FILE_DIR)
const resolvedPaths = rawPaths.map(x => this.resolveCachePath(x))
cacheDebug(`Using cache paths: ${resolvedPaths}`)

View file

@ -4,12 +4,19 @@ import * as core from '@actions/core'
import * as glob from '@actions/glob'
import * as semver from 'semver'
import * as params from './input-params'
import {META_FILE_DIR} from './cache-base'
import {CacheEntryListener, CacheListener} from './cache-reporting'
import {cacheDebug, getCacheKeyPrefix, hashFileNames, restoreCache, saveCache, tryDelete} from './cache-utils'
import {
cacheDebug,
getCacheKeyPrefix,
hashFileNames,
isCacheDebuggingEnabled,
restoreCache,
saveCache,
tryDelete
} from './cache-utils'
import {BuildResult, loadBuildResults} from './build-results'
import {CacheConfig} from './input-params'
const SKIP_RESTORE_VAR = 'GRADLE_BUILD_ACTION_SKIP_RESTORE'
@ -80,12 +87,14 @@ class ExtractedCacheEntryDefinition {
* for more efficient storage.
*/
abstract class AbstractEntryExtractor {
protected readonly cacheConfig: CacheConfig
protected readonly gradleUserHome: string
private extractorName: string
constructor(gradleUserHome: string, extractorName: string) {
constructor(gradleUserHome: string, extractorName: string, cacheConfig: CacheConfig) {
this.gradleUserHome = gradleUserHome
this.extractorName = extractorName
this.cacheConfig = cacheConfig
}
/**
@ -261,7 +270,7 @@ abstract class AbstractEntryExtractor {
// Run actions sequentially if debugging is enabled
private async awaitForDebugging(p: Promise<ExtractedCacheEntry>): Promise<ExtractedCacheEntry> {
if (params.isCacheDebuggingEnabled()) {
if (isCacheDebuggingEnabled()) {
await p
}
return p
@ -306,8 +315,8 @@ abstract class AbstractEntryExtractor {
}
export class GradleHomeEntryExtractor extends AbstractEntryExtractor {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'gradle-home')
constructor(gradleUserHome: string, cacheConfig: CacheConfig) {
super(gradleUserHome, 'gradle-home', cacheConfig)
}
async extract(listener: CacheListener): Promise<void> {
@ -363,8 +372,8 @@ export class GradleHomeEntryExtractor extends AbstractEntryExtractor {
}
export class ConfigurationCacheEntryExtractor extends AbstractEntryExtractor {
constructor(gradleUserHome: string) {
super(gradleUserHome, 'configuration-cache')
constructor(gradleUserHome: string, cacheConfig: CacheConfig) {
super(gradleUserHome, 'configuration-cache', cacheConfig)
}
/**
@ -377,7 +386,7 @@ export class ConfigurationCacheEntryExtractor extends AbstractEntryExtractor {
return
}
if (!params.getCacheEncryptionKey()) {
if (!this.cacheConfig.getCacheEncryptionKey()) {
this.markNotRestored(listener, 'Encryption Key was not provided')
return
}
@ -399,7 +408,7 @@ export class ConfigurationCacheEntryExtractor extends AbstractEntryExtractor {
}
async extract(listener: CacheListener): Promise<void> {
if (!params.getCacheEncryptionKey()) {
if (!this.cacheConfig.getCacheEncryptionKey()) {
const cacheEntryDefinitions = this.getExtractedCacheEntryDefinitions()
if (cacheEntryDefinitions.length > 0) {
core.info('Not saving configuration-cache state, as no encryption key was provided')

View file

@ -7,9 +7,8 @@ import * as crypto from 'crypto'
import * as path from 'path'
import * as fs from 'fs'
import * as params from './input-params'
import {CacheEntryListener} from './cache-reporting'
import {CacheConfig, getJobMatrix} from './input-params'
const CACHE_PROTOCOL_VERSION = 'v9-'
@ -22,31 +21,11 @@ const CACHE_KEY_JOB_EXECUTION_VAR = 'GRADLE_BUILD_ACTION_CACHE_KEY_JOB_EXECUTION
const SEGMENT_DOWNLOAD_TIMEOUT_VAR = 'SEGMENT_DOWNLOAD_TIMEOUT_MINS'
const SEGMENT_DOWNLOAD_TIMEOUT_DEFAULT = 10 * 60 * 1000 // 10 minutes
export function isCacheDisabled(): boolean {
if (!cache.isFeatureAvailable()) {
export function isCacheDebuggingEnabled(): boolean {
if (core.isDebug()) {
return true
}
return params.isCacheDisabled()
}
export function isCacheReadOnly(): boolean {
return !isCacheWriteOnly() && params.isCacheReadOnly()
}
export function isCacheWriteOnly(): boolean {
return params.isCacheWriteOnly()
}
export function isCacheOverwriteExisting(): boolean {
return params.isCacheOverwriteExisting()
}
export function isCacheDebuggingEnabled(): boolean {
return params.isCacheDebuggingEnabled()
}
export function isCacheCleanupEnabled(): boolean {
return params.isCacheCleanupEnabled()
return process.env['GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED'] ? true : false
}
/**
@ -80,7 +59,7 @@ export class CacheKey {
* - Any previous key for this Job (any matrix)
* - Any previous key for this cache on the current OS
*/
export function generateCacheKey(cacheName: string): CacheKey {
export function generateCacheKey(cacheName: string, config: CacheConfig): CacheKey {
const cacheKeyBase = `${getCacheKeyPrefix()}${CACHE_PROTOCOL_VERSION}${cacheName}`
// At the most general level, share caches for all executions on the same OS
@ -95,7 +74,7 @@ export function generateCacheKey(cacheName: string): CacheKey {
// Exact match on Git SHA
const cacheKey = `${cacheKeyForJobContext}-${getCacheKeyJobExecution()}`
if (params.isCacheStrictMatch()) {
if (config.isCacheStrictMatch()) {
return new CacheKey(cacheKey, [cacheKeyForJobContext])
}
@ -125,7 +104,7 @@ function getCacheKeyJobInstance(): string {
// By default, we hash the workflow name and the full `matrix` data for the run, to uniquely identify this job invocation
// The only way we can obtain the `matrix` data is via the `workflow-job-context` parameter in action.yml.
const workflowName = github.context.workflow
const workflowJobContext = params.getJobMatrix()
const workflowJobContext = getJobMatrix()
return hashStrings([workflowName, workflowJobContext])
}

View file

@ -1,19 +1,18 @@
import * as core from '@actions/core'
import {
isCacheCleanupEnabled,
isCacheDisabled,
isCacheReadOnly,
isCacheWriteOnly,
isCacheOverwriteExisting
} from './cache-utils'
import {CacheListener} from './cache-reporting'
import {DaemonController} from './daemon-controller'
import {GradleStateCache} from './cache-base'
import {CacheCleaner} from './cache-cleaner'
import {CacheConfig} from './input-params'
const CACHE_RESTORED_VAR = 'GRADLE_BUILD_ACTION_CACHE_RESTORED'
export async function restore(userHome: string, gradleUserHome: string, cacheListener: CacheListener): Promise<void> {
export async function restore(
userHome: string,
gradleUserHome: string,
cacheListener: CacheListener,
cacheConfig: CacheConfig
): Promise<void> {
// Bypass restore cache on all but first action step in workflow.
if (process.env[CACHE_RESTORED_VAR]) {
core.info('Cache only restored on first action step.')
@ -21,9 +20,9 @@ export async function restore(userHome: string, gradleUserHome: string, cacheLis
}
core.exportVariable(CACHE_RESTORED_VAR, true)
const gradleStateCache = new GradleStateCache(userHome, gradleUserHome)
const gradleStateCache = new GradleStateCache(userHome, gradleUserHome, cacheConfig)
if (isCacheDisabled()) {
if (cacheConfig.isCacheDisabled()) {
core.info('Cache is disabled: will not restore state from previous builds.')
// Initialize the Gradle User Home even when caching is disabled.
gradleStateCache.init()
@ -32,7 +31,7 @@ export async function restore(userHome: string, gradleUserHome: string, cacheLis
}
if (gradleStateCache.cacheOutputExists()) {
if (!isCacheOverwriteExisting()) {
if (!cacheConfig.isCacheOverwriteExisting()) {
core.info('Gradle User Home already exists: will not restore from cache.')
// Initialize pre-existing Gradle User Home.
gradleStateCache.init()
@ -47,7 +46,7 @@ export async function restore(userHome: string, gradleUserHome: string, cacheLis
// Mark the state as restored so that post-action will perform save.
core.saveState(CACHE_RESTORED_VAR, true)
if (isCacheWriteOnly()) {
if (cacheConfig.isCacheWriteOnly()) {
core.info('Cache is write-only: will not restore from cache.')
cacheListener.cacheWriteOnly = true
return
@ -57,7 +56,7 @@ export async function restore(userHome: string, gradleUserHome: string, cacheLis
await gradleStateCache.restore(cacheListener)
})
if (isCacheCleanupEnabled() && !isCacheReadOnly()) {
if (cacheConfig.isCacheCleanupEnabled()) {
core.info('Preparing cache for cleanup.')
const cacheCleaner = new CacheCleaner(gradleUserHome, process.env['RUNNER_TEMP']!)
await cacheCleaner.prepare()
@ -68,9 +67,10 @@ export async function save(
userHome: string,
gradleUserHome: string,
cacheListener: CacheListener,
daemonController: DaemonController
daemonController: DaemonController,
cacheConfig: CacheConfig
): Promise<void> {
if (isCacheDisabled()) {
if (cacheConfig.isCacheDisabled()) {
core.info('Cache is disabled: will not save state for later builds.')
return
}
@ -80,7 +80,7 @@ export async function save(
return
}
if (isCacheReadOnly()) {
if (cacheConfig.isCacheReadOnly()) {
core.info('Cache is read-only: will not save state for use in subsequent builds.')
cacheListener.cacheReadOnly = true
return
@ -88,7 +88,7 @@ export async function save(
await daemonController.stopAllDaemons()
if (isCacheCleanupEnabled()) {
if (cacheConfig.isCacheCleanupEnabled()) {
core.info('Forcing cache cleanup.')
const cacheCleaner = new CacheCleaner(gradleUserHome, process.env['RUNNER_TEMP']!)
try {
@ -99,6 +99,6 @@ export async function save(
}
await core.group('Caching Gradle state', async () => {
return new GradleStateCache(userHome, gradleUserHome).save(cacheListener)
return new GradleStateCache(userHome, gradleUserHome, cacheConfig).save(cacheListener)
})
}

View file

@ -11,31 +11,26 @@ import fs from 'fs'
import * as layout from './repository-layout'
import {PostActionJobFailure} from './errors'
import {
DependencyGraphOption,
getDependencyGraphContinueOnFailure,
getGithubToken,
getJobMatrix,
getArtifactRetentionDays
} from './input-params'
import {DependencyGraphConfig, DependencyGraphOption, getGithubToken} from './input-params'
const DEPENDENCY_GRAPH_PREFIX = 'dependency-graph_'
export async function setup(option: DependencyGraphOption): Promise<void> {
export async function setup(config: DependencyGraphConfig): Promise<void> {
const option = config.getDependencyGraphOption()
if (option === DependencyGraphOption.Disabled) {
core.exportVariable('GITHUB_DEPENDENCY_GRAPH_ENABLED', 'false')
return
}
// Download and submit early, for compatability with dependency review.
if (option === DependencyGraphOption.DownloadAndSubmit) {
await downloadAndSubmitDependencyGraphs()
await downloadAndSubmitDependencyGraphs(config)
return
}
core.info('Enabling dependency graph generation')
core.exportVariable('GITHUB_DEPENDENCY_GRAPH_ENABLED', 'true')
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_CONTINUE_ON_FAILURE', getDependencyGraphContinueOnFailure())
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR', getJobCorrelator())
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_CONTINUE_ON_FAILURE', config.getDependencyGraphContinueOnFailure())
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR', config.getJobCorrelator())
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_ID', github.context.runId)
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_REF', github.context.ref)
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_SHA', getShaFromContext())
@ -58,12 +53,13 @@ function maybeExportVariable(variableName: string, value: unknown): void {
}
}
export async function complete(option: DependencyGraphOption): Promise<void> {
export async function complete(config: DependencyGraphConfig): Promise<void> {
if (isRunningInActEnvironment()) {
core.info('Dependency graph upload and submit not supported in the ACT environment.')
return
}
const option = config.getDependencyGraphOption()
try {
switch (option) {
case DependencyGraphOption.Disabled:
@ -75,10 +71,10 @@ export async function complete(option: DependencyGraphOption): Promise<void> {
await submitDependencyGraphs(await findGeneratedDependencyGraphFiles())
return
case DependencyGraphOption.GenerateAndUpload:
await uploadDependencyGraphs(await findGeneratedDependencyGraphFiles())
await uploadDependencyGraphs(await findGeneratedDependencyGraphFiles(), config)
}
} catch (e) {
warnOrFail(option, e)
warnOrFail(config, option, e)
}
}
@ -87,7 +83,7 @@ async function findGeneratedDependencyGraphFiles(): Promise<string[]> {
return await findDependencyGraphFiles(workspaceDirectory)
}
async function uploadDependencyGraphs(dependencyGraphFiles: string[]): Promise<void> {
async function uploadDependencyGraphs(dependencyGraphFiles: string[], config: DependencyGraphConfig): Promise<void> {
const workspaceDirectory = layout.workspaceDirectory()
const artifactClient = new DefaultArtifactClient()
@ -96,12 +92,12 @@ async function uploadDependencyGraphs(dependencyGraphFiles: string[]): Promise<v
core.info(`Uploading dependency graph file: ${relativePath}`)
const artifactName = `${DEPENDENCY_GRAPH_PREFIX}${path.basename(dependencyGraphFile)}`
await artifactClient.uploadArtifact(artifactName, [dependencyGraphFile], workspaceDirectory, {
retentionDays: getArtifactRetentionDays()
retentionDays: config.getArtifactRetentionDays()
})
}
}
async function downloadAndSubmitDependencyGraphs(): Promise<void> {
async function downloadAndSubmitDependencyGraphs(config: DependencyGraphConfig): Promise<void> {
if (isRunningInActEnvironment()) {
core.info('Dependency graph download and submit not supported in the ACT environment.')
return
@ -110,7 +106,7 @@ async function downloadAndSubmitDependencyGraphs(): Promise<void> {
try {
await submitDependencyGraphs(await downloadDependencyGraphs())
} catch (e) {
warnOrFail(DependencyGraphOption.DownloadAndSubmit, e)
warnOrFail(config, DependencyGraphOption.DownloadAndSubmit, e)
}
}
@ -192,8 +188,8 @@ async function findDependencyGraphFiles(dir: string): Promise<string[]> {
return graphFiles
}
function warnOrFail(option: String, error: unknown): void {
if (!getDependencyGraphContinueOnFailure()) {
function warnOrFail(config: DependencyGraphConfig, option: String, error: unknown): void {
if (!config.getDependencyGraphContinueOnFailure()) {
throw new PostActionJobFailure(error)
}
@ -228,32 +224,6 @@ function getShaFromContext(): string {
}
}
function getJobCorrelator(): string {
return constructJobCorrelator(github.context.workflow, github.context.job, getJobMatrix())
}
export function constructJobCorrelator(workflow: string, jobId: string, matrixJson: string): string {
const matrixString = describeMatrix(matrixJson)
const label = matrixString ? `${workflow}-${jobId}-${matrixString}` : `${workflow}-${jobId}`
return sanitize(label)
}
function describeMatrix(matrixJson: string): string {
core.debug(`Got matrix json: ${matrixJson}`)
const matrix = JSON.parse(matrixJson)
if (matrix) {
return Object.values(matrix).join('-')
}
return ''
}
function sanitize(value: string): string {
return value
.replace(/[^a-zA-Z0-9_-\s]/g, '')
.replace(/\s+/g, '_')
.toLowerCase()
}
function isRunningInActEnvironment(): boolean {
return process.env.ACT !== undefined
}

View file

@ -7,7 +7,7 @@ import * as layout from '../repository-layout'
import * as dependencyGraph from '../dependency-graph'
import {parseArgsStringToArgv} from 'string-argv'
import {DependencyGraphOption, getDependencyGraphOption} from '../input-params'
import {BuildScanConfig, CacheConfig, DependencyGraphConfig, DependencyGraphOption} from '../input-params'
/**
* The main entry point for the action, called by Github Actions for the step.
@ -15,12 +15,13 @@ import {DependencyGraphOption, getDependencyGraphOption} from '../input-params'
export async function run(): Promise<void> {
try {
// Configure Gradle environment (Gradle User Home)
await setupGradle.setup()
await setupGradle.setup(new CacheConfig(), new BuildScanConfig())
// Configure the dependency graph submission
await dependencyGraph.setup(getDependencyGraphOption())
const config = new DependencyGraphConfig()
await dependencyGraph.setup(config)
if (getDependencyGraphOption() === DependencyGraphOption.DownloadAndSubmit) {
if (config.getDependencyGraphOption() === DependencyGraphOption.DownloadAndSubmit) {
// No execution to perform
return
}

View file

@ -2,7 +2,7 @@ import * as core from '@actions/core'
import * as setupGradle from '../setup-gradle'
import * as dependencyGraph from '../dependency-graph'
import {getDependencyGraphOption} from '../input-params'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../input-params'
import {PostActionJobFailure} from '../errors'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
@ -15,9 +15,9 @@ process.on('uncaughtException', e => handleFailure(e))
*/
export async function run(): Promise<void> {
try {
if (await setupGradle.complete()) {
if (await setupGradle.complete(new CacheConfig(), new SummaryConfig())) {
// Only submit the dependency graphs once per job
await dependencyGraph.complete(getDependencyGraphOption())
await dependencyGraph.complete(new DependencyGraphConfig())
}
} catch (error) {
if (error instanceof PostActionJobFailure) {

View file

@ -1,54 +1,234 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as cache from '@actions/cache'
import {SUMMARY_ENV_VAR} from '@actions/core/lib/summary'
import {parseArgsStringToArgv} from 'string-argv'
export function isCacheDisabled(): boolean {
return getBooleanInput('cache-disabled')
export class DependencyGraphConfig {
getDependencyGraphOption(): DependencyGraphOption {
const val = core.getInput('dependency-graph')
switch (val.toLowerCase().trim()) {
case 'disabled':
return DependencyGraphOption.Disabled
case 'generate':
return DependencyGraphOption.Generate
case 'generate-and-submit':
return DependencyGraphOption.GenerateAndSubmit
case 'generate-and-upload':
return DependencyGraphOption.GenerateAndUpload
case 'download-and-submit':
return DependencyGraphOption.DownloadAndSubmit
case 'clear':
return DependencyGraphOption.Clear
}
throw TypeError(
`The value '${val}' is not valid for 'dependency-graph'. Valid values are: [disabled, generate, generate-and-submit, generate-and-upload, download-and-submit, clear]. The default value is 'disabled'.`
)
}
getDependencyGraphContinueOnFailure(): boolean {
return getBooleanInput('dependency-graph-continue-on-failure', true)
}
getArtifactRetentionDays(): number {
const val = core.getInput('artifact-retention-days')
return parseNumericInput('artifact-retention-days', val, 0)
// Zero indicates that the default repository settings should be used
}
getJobCorrelator(): string {
return DependencyGraphConfig.constructJobCorrelator(github.context.workflow, github.context.job, getJobMatrix())
}
static constructJobCorrelator(workflow: string, jobId: string, matrixJson: string): string {
const matrixString = this.describeMatrix(matrixJson)
const label = matrixString ? `${workflow}-${jobId}-${matrixString}` : `${workflow}-${jobId}`
return this.sanitize(label)
}
private static describeMatrix(matrixJson: string): string {
core.debug(`Got matrix json: ${matrixJson}`)
const matrix = JSON.parse(matrixJson)
if (matrix) {
return Object.values(matrix).join('-')
}
return ''
}
private static sanitize(value: string): string {
return value
.replace(/[^a-zA-Z0-9_-\s]/g, '')
.replace(/\s+/g, '_')
.toLowerCase()
}
}
export function isCacheReadOnly(): boolean {
return getBooleanInput('cache-read-only')
export enum DependencyGraphOption {
Disabled = 'disabled',
Generate = 'generate',
GenerateAndSubmit = 'generate-and-submit',
GenerateAndUpload = 'generate-and-upload',
DownloadAndSubmit = 'download-and-submit',
Clear = 'clear'
}
export function isCacheWriteOnly(): boolean {
return getBooleanInput('cache-write-only')
export class CacheConfig {
isCacheDisabled(): boolean {
if (!cache.isFeatureAvailable()) {
return true
}
return getBooleanInput('cache-disabled')
}
isCacheReadOnly(): boolean {
return !this.isCacheWriteOnly() && getBooleanInput('cache-read-only')
}
isCacheWriteOnly(): boolean {
return getBooleanInput('cache-write-only')
}
isCacheOverwriteExisting(): boolean {
return getBooleanInput('cache-overwrite-existing')
}
isCacheStrictMatch(): boolean {
return getBooleanInput('gradle-home-cache-strict-match')
}
isCacheCleanupEnabled(): boolean {
return getBooleanInput('gradle-home-cache-cleanup') && !this.isCacheReadOnly()
}
getCacheEncryptionKey(): string {
return core.getInput('cache-encryption-key')
}
getCacheIncludes(): string[] {
return core.getMultilineInput('gradle-home-cache-includes')
}
getCacheExcludes(): string[] {
return core.getMultilineInput('gradle-home-cache-excludes')
}
}
export function isCacheOverwriteExisting(): boolean {
return getBooleanInput('cache-overwrite-existing')
export class SummaryConfig {
shouldGenerateJobSummary(hasFailure: boolean): boolean {
// Check if Job Summary is supported on this platform
if (!process.env[SUMMARY_ENV_VAR]) {
return false
}
// Check if Job Summary is disabled using the deprecated input
if (!this.isJobSummaryEnabled()) {
return false
}
return this.shouldAddJobSummary(this.getJobSummaryOption(), hasFailure)
}
shouldAddPRComment(hasFailure: boolean): boolean {
return this.shouldAddJobSummary(this.getPRCommentOption(), hasFailure)
}
private shouldAddJobSummary(option: JobSummaryOption, hasFailure: boolean): boolean {
switch (option) {
case JobSummaryOption.Always:
return true
case JobSummaryOption.Never:
return false
case JobSummaryOption.OnFailure:
return hasFailure
}
}
private isJobSummaryEnabled(): boolean {
return getBooleanInput('generate-job-summary', true)
}
private getJobSummaryOption(): JobSummaryOption {
return this.parseJobSummaryOption('add-job-summary')
}
private getPRCommentOption(): JobSummaryOption {
return this.parseJobSummaryOption('add-job-summary-as-pr-comment')
}
private parseJobSummaryOption(paramName: string): JobSummaryOption {
const val = core.getInput(paramName)
switch (val.toLowerCase().trim()) {
case 'never':
return JobSummaryOption.Never
case 'always':
return JobSummaryOption.Always
case 'on-failure':
return JobSummaryOption.OnFailure
}
throw TypeError(
`The value '${val}' is not valid for ${paramName}. Valid values are: [never, always, on-failure].`
)
}
}
export function isCacheStrictMatch(): boolean {
return getBooleanInput('gradle-home-cache-strict-match')
export enum JobSummaryOption {
Never = 'never',
Always = 'always',
OnFailure = 'on-failure'
}
export function isCacheDebuggingEnabled(): boolean {
return process.env['GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED'] ? true : false
}
export class BuildScanConfig {
getBuildScanPublishEnabled(): boolean {
if (!this.verifyTermsOfUseAgreement()) {
return false
}
return getBooleanInput('build-scan-publish') && this.verifyTermsOfUseAgreement()
}
export function isCacheCleanupEnabled(): boolean {
return getBooleanInput('gradle-home-cache-cleanup')
}
getBuildScanTermsOfUseUrl(): string {
return this.getTermsOfUseProp('build-scan-terms-of-use-url', 'build-scan-terms-of-service-url')
}
export function getCacheEncryptionKey(): string {
return core.getInput('cache-encryption-key')
}
getBuildScanTermsOfUseAgree(): string {
return this.getTermsOfUseProp('build-scan-terms-of-use-agree', 'build-scan-terms-of-service-agree')
}
export function getCacheIncludes(): string[] {
return core.getMultilineInput('gradle-home-cache-includes')
}
private verifyTermsOfUseAgreement(): boolean {
if (
(this.getBuildScanTermsOfUseUrl() !== 'https://gradle.com/terms-of-service' &&
this.getBuildScanTermsOfUseUrl() !== 'https://gradle.com/help/legal-terms-of-use') ||
this.getBuildScanTermsOfUseAgree() !== 'yes'
) {
core.warning(
`Terms of use at 'https://gradle.com/help/legal-terms-of-use' must be agreed in order to publish build scans.`
)
return false
}
return true
}
export function getCacheExcludes(): string[] {
return core.getMultilineInput('gradle-home-cache-excludes')
}
export function getBuildRootDirectory(): string {
return core.getInput('build-root-directory')
/**
* TODO @bigdaz: remove support for the deprecated input property in the next major release of the action
*/
private getTermsOfUseProp(newPropName: string, oldPropName: string): string {
const newProp = core.getInput(newPropName)
if (newProp !== '') {
return newProp
}
return core.getInput(oldPropName)
}
}
export function getGradleVersion(): string {
return core.getInput('gradle-version')
}
export function getBuildRootDirectory(): string {
return core.getInput('build-root-directory')
}
export function getArguments(): string[] {
const input = core.getInput('arguments')
return parseArgsStringToArgv(input)
@ -63,85 +243,6 @@ export function getGithubToken(): string {
return core.getInput('github-token', {required: true})
}
export function isJobSummaryEnabled(): boolean {
return getBooleanInput('generate-job-summary', true)
}
export function getJobSummaryOption(): JobSummaryOption {
return parseJobSummaryOption('add-job-summary')
}
export function getPRCommentOption(): JobSummaryOption {
return parseJobSummaryOption('add-job-summary-as-pr-comment')
}
export function getBuildScanPublishEnabled(): boolean {
return getBooleanInput('build-scan-publish')
}
export function getBuildScanTermsOfUseUrl(): string {
return getTermsOfUseProp('build-scan-terms-of-use-url', 'build-scan-terms-of-service-url')
}
export function getBuildScanTermsOfUseAgree(): string {
return getTermsOfUseProp('build-scan-terms-of-use-agree', 'build-scan-terms-of-service-agree')
}
/**
* TODO @bigdaz: remove the deprecated input property in the next major release of the action
*/
function getTermsOfUseProp(newPropName: string, oldPropName: string): string {
const newProp = core.getInput(newPropName)
if (newProp !== '') {
return newProp
}
return core.getInput(oldPropName)
}
function parseJobSummaryOption(paramName: string): JobSummaryOption {
const val = core.getInput(paramName)
switch (val.toLowerCase().trim()) {
case 'never':
return JobSummaryOption.Never
case 'always':
return JobSummaryOption.Always
case 'on-failure':
return JobSummaryOption.OnFailure
}
throw TypeError(`The value '${val}' is not valid for ${paramName}. Valid values are: [never, always, on-failure].`)
}
export function getDependencyGraphOption(): DependencyGraphOption {
const val = core.getInput('dependency-graph')
switch (val.toLowerCase().trim()) {
case 'disabled':
return DependencyGraphOption.Disabled
case 'generate':
return DependencyGraphOption.Generate
case 'generate-and-submit':
return DependencyGraphOption.GenerateAndSubmit
case 'generate-and-upload':
return DependencyGraphOption.GenerateAndUpload
case 'download-and-submit':
return DependencyGraphOption.DownloadAndSubmit
case 'clear':
return DependencyGraphOption.Clear
}
throw TypeError(
`The value '${val}' is not valid for 'dependency-graph'. Valid values are: [disabled, generate, generate-and-submit, generate-and-upload, download-and-submit, clear]. The default value is 'disabled'.`
)
}
export function getDependencyGraphContinueOnFailure(): boolean {
return getBooleanInput('dependency-graph-continue-on-failure', true)
}
export function getArtifactRetentionDays(): number {
const val = core.getInput('artifact-retention-days')
return parseNumericInput('artifact-retention-days', val, 0)
// Zero indicates that the default repository settings should be used
}
export function parseNumericInput(paramName: string, paramValue: string, paramDefault: number): number {
if (paramValue.length === 0) {
return paramDefault
@ -165,18 +266,3 @@ function getBooleanInput(paramName: string, paramDefault = false): boolean {
}
throw TypeError(`The value '${paramValue} is not valid for '${paramName}. Valid values are: [true, false]`)
}
export enum DependencyGraphOption {
Disabled = 'disabled',
Generate = 'generate',
GenerateAndSubmit = 'generate-and-submit',
GenerateAndUpload = 'generate-and-upload',
DownloadAndSubmit = 'download-and-submit',
Clear = 'clear'
}
export enum JobSummaryOption {
Never = 'never',
Always = 'always',
OnFailure = 'on-failure'
}

View file

@ -1,17 +1,21 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import {SUMMARY_ENV_VAR} from '@actions/core/lib/summary'
import {RequestError} from '@octokit/request-error'
import * as params from './input-params'
import {BuildResult} from './build-results'
import {CacheListener, generateCachingReport} from './cache-reporting'
import {SummaryConfig, getGithubToken} from './input-params'
export async function generateJobSummary(buildResults: BuildResult[], cacheListener: CacheListener): Promise<void> {
export async function generateJobSummary(
buildResults: BuildResult[],
cacheListener: CacheListener,
config: SummaryConfig
): Promise<void> {
const summaryTable = renderSummaryTable(buildResults)
const cachingReport = generateCachingReport(cacheListener)
if (shouldGenerateJobSummary(buildResults)) {
const hasFailure = buildResults.some(result => result.buildFailed)
if (config.shouldGenerateJobSummary(hasFailure)) {
core.info('Generating Job Summary')
core.summary.addRaw(summaryTable)
@ -25,7 +29,7 @@ export async function generateJobSummary(buildResults: BuildResult[], cacheListe
core.info('============================')
}
if (shouldAddPRComment(buildResults)) {
if (config.shouldAddPRComment(hasFailure)) {
await addPRComment(summaryTable)
}
}
@ -47,7 +51,7 @@ async function addPRComment(jobSummary: string): Promise<void> {
${jobSummary}`
const github_token = params.getGithubToken()
const github_token = getGithubToken()
const octokit = github.getOctokit(github_token)
try {
await octokit.rest.issues.createComment({
@ -128,36 +132,6 @@ function renderBuildScanBadge(outcomeText: string, outcomeColor: string, targetU
return `<a href="${targetUrl}" rel="nofollow" target="_blank">${badgeHtml}</a>`
}
function shouldGenerateJobSummary(buildResults: BuildResult[]): boolean {
// Check if Job Summary is supported on this platform
if (!process.env[SUMMARY_ENV_VAR]) {
return false
}
// Check if Job Summary is disabled using the deprecated input
if (!params.isJobSummaryEnabled()) {
return false
}
return shouldAddJobSummary(params.getJobSummaryOption(), buildResults)
}
function shouldAddPRComment(buildResults: BuildResult[]): boolean {
return shouldAddJobSummary(params.getPRCommentOption(), buildResults)
}
function shouldAddJobSummary(option: params.JobSummaryOption, buildResults: BuildResult[]): boolean {
switch (option) {
case params.JobSummaryOption.Always:
return true
case params.JobSummaryOption.Never:
return false
case params.JobSummaryOption.OnFailure:
core.info(`Got these build results: ${JSON.stringify(buildResults)}`)
return buildResults.some(result => result.buildFailed)
}
}
function truncateString(str: string, maxLength: number): string {
if (str.length > maxLength) {
return `<div title='${str}'>${str.slice(0, maxLength - 1)}…</div>`

View file

@ -8,7 +8,8 @@ import * as toolCache from '@actions/tool-cache'
import * as gradlew from './gradlew'
import * as params from './input-params'
import {handleCacheFailure, isCacheDisabled, isCacheReadOnly} from './cache-utils'
import {handleCacheFailure} from './cache-utils'
import {CacheConfig} from './input-params'
const gradleVersionsBaseUrl = 'https://services.gradle.org/versions'
@ -122,7 +123,9 @@ async function locateGradleAndDownloadIfRequired(versionInfo: GradleVersionInfo)
async function downloadAndCacheGradleDistribution(versionInfo: GradleVersionInfo): Promise<string> {
const downloadPath = path.join(os.homedir(), `gradle-installations/downloads/gradle-${versionInfo.version}-bin.zip`)
if (isCacheDisabled()) {
// TODO: Convert this to a class and inject config
const cacheConfig = new CacheConfig()
if (cacheConfig.isCacheDisabled()) {
await downloadGradleDistribution(versionInfo, downloadPath)
return downloadPath
}
@ -141,7 +144,7 @@ async function downloadAndCacheGradleDistribution(versionInfo: GradleVersionInfo
core.info(`Gradle distribution ${versionInfo.version} not found in cache. Will download.`)
await downloadGradleDistribution(versionInfo, downloadPath)
if (!isCacheReadOnly()) {
if (!cacheConfig.isCacheReadOnly()) {
try {
await cache.saveCache([downloadPath], cacheKey)
} catch (error) {

View file

@ -10,13 +10,14 @@ import * as buildScan from './build-scan'
import {loadBuildResults} from './build-results'
import {CacheListener} from './cache-reporting'
import {DaemonController} from './daemon-controller'
import {BuildScanConfig, CacheConfig, SummaryConfig} from './input-params'
const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED'
const USER_HOME = 'USER_HOME'
const GRADLE_USER_HOME = 'GRADLE_USER_HOME'
const CACHE_LISTENER = 'CACHE_LISTENER'
export async function setup(): Promise<boolean> {
export async function setup(cacheConfig: CacheConfig, buildScanConfig: BuildScanConfig): Promise<boolean> {
const userHome = await determineUserHome()
const gradleUserHome = await determineGradleUserHome()
@ -35,16 +36,16 @@ export async function setup(): Promise<boolean> {
core.saveState(GRADLE_USER_HOME, gradleUserHome)
const cacheListener = new CacheListener()
await caches.restore(userHome, gradleUserHome, cacheListener)
await caches.restore(userHome, gradleUserHome, cacheListener, cacheConfig)
core.saveState(CACHE_LISTENER, cacheListener.stringify())
buildScan.setup()
buildScan.setup(buildScanConfig)
return true
}
export async function complete(): Promise<boolean> {
export async function complete(cacheConfig: CacheConfig, 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
@ -58,9 +59,9 @@ export async function complete(): Promise<boolean> {
const cacheListener: CacheListener = CacheListener.rehydrate(core.getState(CACHE_LISTENER))
const daemonController = new DaemonController(buildResults)
await caches.save(userHome, gradleUserHome, cacheListener, daemonController)
await caches.save(userHome, gradleUserHome, cacheListener, daemonController, cacheConfig)
await jobSummary.generateJobSummary(buildResults, cacheListener)
await jobSummary.generateJobSummary(buildResults, cacheListener, summaryConfig)
core.info('Completed post-action step')

View file

@ -4,8 +4,8 @@ import * as setupGradle from '../setup-gradle'
import * as execution from '../execution'
import * as provisioner from '../provision'
import * as layout from '../repository-layout'
import * as params from '../input-params'
import * as dependencyGraph from '../dependency-graph'
import {BuildScanConfig, CacheConfig, DependencyGraphConfig, getArguments} from '../input-params'
/**
* The main entry point for the action, called by Github Actions for the step.
@ -13,16 +13,16 @@ import * as dependencyGraph from '../dependency-graph'
export async function run(): Promise<void> {
try {
// Configure Gradle environment (Gradle User Home)
await setupGradle.setup()
await setupGradle.setup(new CacheConfig(), new BuildScanConfig())
// Configure the dependency graph submission
await dependencyGraph.setup(params.getDependencyGraphOption())
await dependencyGraph.setup(new DependencyGraphConfig())
// Download and install Gradle if required
const executable = await provisioner.provisionGradle()
// Only execute if arguments have been provided
const args: string[] = params.getArguments()
const args: string[] = getArguments()
if (args.length > 0) {
const buildRootDirectory = layout.buildRootDirectory()
await execution.executeGradleBuild(executable, buildRootDirectory, args)

View file

@ -2,7 +2,7 @@ import * as core from '@actions/core'
import * as setupGradle from '../setup-gradle'
import * as dependencyGraph from '../dependency-graph'
import {getDependencyGraphOption} from '../input-params'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../input-params'
import {PostActionJobFailure} from '../errors'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
@ -15,9 +15,9 @@ process.on('uncaughtException', e => handleFailure(e))
*/
export async function run(): Promise<void> {
try {
if (await setupGradle.complete()) {
if (await setupGradle.complete(new CacheConfig(), new SummaryConfig())) {
// Only submit the dependency graphs once per job
await dependencyGraph.complete(getDependencyGraphOption())
await dependencyGraph.complete(new DependencyGraphConfig())
}
} catch (error) {
if (error instanceof PostActionJobFailure) {

View file

@ -1,6 +1,7 @@
import {GradleStateCache} from "../../src/cache-base"
import * as path from 'path'
import * as fs from 'fs'
import {GradleStateCache} from "../../src/cache-base"
import {CacheConfig} from "../../src/input-params"
const testTmp = 'test/jest/tmp'
fs.rmSync(testTmp, {recursive: true, force: true})
@ -11,7 +12,7 @@ describe("--info and --stacktrace", () => {
const emptyGradleHome = `${testTmp}/empty-gradle-home`
fs.mkdirSync(emptyGradleHome, {recursive: true})
const stateCache = new GradleStateCache("ignored", emptyGradleHome)
const stateCache = new GradleStateCache("ignored", emptyGradleHome, new CacheConfig())
stateCache.configureInfoLogLevel()
expect(fs.readFileSync(path.resolve(emptyGradleHome, "gradle.properties"), 'utf-8'))
@ -24,7 +25,7 @@ describe("--info and --stacktrace", () => {
fs.mkdirSync(existingGradleHome, {recursive: true})
fs.writeFileSync(path.resolve(existingGradleHome, "gradle.properties"), "org.gradle.logging.level=debug\n")
const stateCache = new GradleStateCache("ignored", existingGradleHome)
const stateCache = new GradleStateCache("ignored", existingGradleHome, new CacheConfig())
stateCache.configureInfoLogLevel()
expect(fs.readFileSync(path.resolve(existingGradleHome, "gradle.properties"), 'utf-8'))

View file

@ -1,33 +1,33 @@
import * as dependencyGraph from '../../src/dependency-graph'
import { DependencyGraphConfig } from "../../src/input-params"
describe('dependency-graph', () => {
describe('constructs job correlator', () => {
it('removes commas from workflow name', () => {
const id = dependencyGraph.constructJobCorrelator('Workflow, with,commas', 'jobid', '{}')
const id = DependencyGraphConfig.constructJobCorrelator('Workflow, with,commas', 'jobid', '{}')
expect(id).toBe('workflow_withcommas-jobid')
})
it('removes non word characters', () => {
const id = dependencyGraph.constructJobCorrelator('Workflow!_with()characters', 'job-*id', '{"foo": "bar!@#$%^&*("}')
const id = DependencyGraphConfig.constructJobCorrelator('Workflow!_with()characters', 'job-*id', '{"foo": "bar!@#$%^&*("}')
expect(id).toBe('workflow_withcharacters-job-id-bar')
})
it('replaces spaces', () => {
const id = dependencyGraph.constructJobCorrelator('Workflow !_ with () characters, and spaces', 'job-*id', '{"foo": "bar!@#$%^&*("}')
const id = DependencyGraphConfig.constructJobCorrelator('Workflow !_ with () characters, and spaces', 'job-*id', '{"foo": "bar!@#$%^&*("}')
expect(id).toBe('workflow___with_characters_and_spaces-job-id-bar')
})
it('without matrix', () => {
const id = dependencyGraph.constructJobCorrelator('workflow', 'jobid', 'null')
const id = DependencyGraphConfig.constructJobCorrelator('workflow', 'jobid', 'null')
expect(id).toBe('workflow-jobid')
})
it('with dashes in values', () => {
const id = dependencyGraph.constructJobCorrelator('workflow-name', 'job-id', '{"os": "ubuntu-latest"}')
const id = DependencyGraphConfig.constructJobCorrelator('workflow-name', 'job-id', '{"os": "ubuntu-latest"}')
expect(id).toBe('workflow-name-job-id-ubuntu-latest')
})
it('with single matrix value', () => {
const id = dependencyGraph.constructJobCorrelator('workflow', 'jobid', '{"os": "windows"}')
const id = DependencyGraphConfig.constructJobCorrelator('workflow', 'jobid', '{"os": "windows"}')
expect(id).toBe('workflow-jobid-windows')
})
it('with composite matrix value', () => {
const id = dependencyGraph.constructJobCorrelator('workflow', 'jobid', '{"os": "windows", "java-version": "21.1", "other": "Value, with COMMA"}')
const id = DependencyGraphConfig.constructJobCorrelator('workflow', 'jobid', '{"os": "windows", "java-version": "21.1", "other": "Value, with COMMA"}')
expect(id).toBe('workflow-jobid-windows-211-value_with_comma')
})
})