diff --git a/.github/actions/init-integ-test/action.yml b/.github/actions/init-integ-test/action.yml index 8124f3ea..4ef52d4f 100644 --- a/.github/actions/init-integ-test/action.yml +++ b/.github/actions/init-integ-test/action.yml @@ -11,6 +11,7 @@ runs: # Downloads a 'dist' directory artifact that was uploaded in an earlier 'build-dist' step - name: Download dist + if: ${{ !env.ACT }} uses: actions/download-artifact@v4 with: name: dist diff --git a/.github/workflows/ci-integ-test.yml b/.github/workflows/ci-integ-test.yml index b04db0b7..970c7da2 100644 --- a/.github/workflows/ci-integ-test.yml +++ b/.github/workflows/ci-integ-test.yml @@ -183,3 +183,5 @@ jobs: wrapper-validation: needs: [determine-suite, build-distribution] uses: ./.github/workflows/integ-test-wrapper-validation.yml + with: + runner-os: '["ubuntu-latest"]' diff --git a/.github/workflows/integ-test-wrapper-validation.yml b/.github/workflows/integ-test-wrapper-validation.yml index ce1df329..5dde6db8 100644 --- a/.github/workflows/integ-test-wrapper-validation.yml +++ b/.github/workflows/integ-test-wrapper-validation.yml @@ -1,12 +1,40 @@ -name: Test sample Kotlin DSL project +name: Test wrapper validation on: workflow_call: + inputs: + runner-os: + type: string + default: '["ubuntu-latest", "windows-latest", "macos-latest"]' jobs: - # Integration test for successful validation of wrappers + test-setup-gradle-validation: + strategy: + fail-fast: false + matrix: + os: ${{fromJSON(inputs.runner-os)}} + runs-on: ${{ matrix.os }} + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Initialize integ-test + uses: ./.github/actions/init-integ-test + + - name: Run wrapper-validation-action + id: setup-gradle + uses: ./setup-gradle + with: + validate-wrappers: true + continue-on-error: true + + - name: Check failure + run: | + if [ "${{ steps.setup-gradle.outcome}}" != "failure" ] ; then + echo "Expected validation to fail, but it didn't" + exit 1 + fi + test-validation-success: - name: 'Test: Validation success' runs-on: ubuntu-latest steps: - name: Checkout sources @@ -33,9 +61,7 @@ jobs: exit 1 fi - # Integration test for failing validation of wrappers test-validation-error: - name: 'Test: Validation error' runs-on: ubuntu-latest steps: - name: Checkout sources diff --git a/build b/build index 01c22e17..eb576c62 100755 --- a/build +++ b/build @@ -3,8 +3,21 @@ cd sources npm install -if [ "$1" == "all" ]; then - npm run all -else - npm run build -fi +case "$1" in + all) + nprm run all + ;; + act) + # Build and copy outputs to the dist directory + npm run build + cd .. + cp -r sources/dist . + # Run act + $@ + # Revert the changes to the dist directory + git co -- dist + ;; + *) + npm run build + ;; +esac \ No newline at end of file diff --git a/docs/setup-gradle.md b/docs/setup-gradle.md index 4b3f6221..fa7fcb8d 100644 --- a/docs/setup-gradle.md +++ b/docs/setup-gradle.md @@ -503,6 +503,21 @@ located at `USER_HOME/.gradle/init.d/gradle-actions.build-result-capture.init.gr If you are adding any custom init scripts to the `USER_HOME/.gradle/init.d` directory, it may be necessary to ensure these files are applied before `gradle-actions.build-result-capture.init.gradle`. Since Gradle applies init scripts in alphabetical order, one way to ensure this is via file naming. +## Gradle Wrapper validation + +Instead of using the [wrapper-validation action](./wrapper-validation.md) separately, you can enable +wrapper validation directly in your Setup Gradle step. + +```yaml + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + validate-wrappers: true +``` + +If you need more advanced configuration, then you're advised to continue using a separate workflow step +with `gradle/actions/wrapper-validation`. + ## Support for GitHub Enterprise Server (GHES) You can use the `setup-gradle` action on GitHub Enterprise Server, and benefit from the improved integration with Gradle. Depending on the version of GHES you are running, certain features may be limited: diff --git a/setup-gradle/action.yml b/setup-gradle/action.yml index 6a9ccc40..48ec0f34 100644 --- a/setup-gradle/action.yml +++ b/setup-gradle/action.yml @@ -100,6 +100,14 @@ inputs: description: Indicate that you agree to the Build ScanĀ® terms of use. This input value must be "yes". required: false + # Wrapper validation configuration + validate-wrappers: + description: | + When 'true', the action will perform the 'wrapper-validation' action automatically. + If the wrapper checksums are not valid, the action will fail. + required: false + default: false + # DEPRECATED ACTION INPUTS build-scan-terms-of-service-url: description: The URL to the Build ScanĀ® terms of use. This input must be set to 'https://gradle.com/terms-of-service'. diff --git a/sources/src/configuration.ts b/sources/src/configuration.ts index 0b23c618..f59cfbf7 100644 --- a/sources/src/configuration.ts +++ b/sources/src/configuration.ts @@ -261,6 +261,10 @@ export class GradleExecutionConfig { } } +export function doValidateWrappers(): boolean { + return getBooleanInput('validate-wrappers') +} + // Internal parameters export function getJobMatrix(): string { return core.getInput('workflow-job-context') diff --git a/sources/src/dependency-graph.ts b/sources/src/dependency-graph.ts index 41161914..a12fb130 100644 --- a/sources/src/dependency-graph.ts +++ b/sources/src/dependency-graph.ts @@ -9,7 +9,7 @@ import type {PullRequestEvent} from '@octokit/webhooks-types' import * as path from 'path' import fs from 'fs' -import {PostActionJobFailure} from './errors' +import {JobFailure} from './errors' import {DependencyGraphConfig, DependencyGraphOption, getGithubToken, getWorkspaceDirectory} from './configuration' const DEPENDENCY_GRAPH_PREFIX = 'dependency-graph_' @@ -208,7 +208,7 @@ function markProcessed(dependencyGraphFile: string): void { function warnOrFail(config: DependencyGraphConfig, option: String, error: unknown): void { if (!config.getDependencyGraphContinueOnFailure()) { - throw new PostActionJobFailure(error) + throw new JobFailure(error) } core.warning(`Failed to ${option} dependency graph. Will continue.\n${String(error)}`) diff --git a/sources/src/dependency-submission/main.ts b/sources/src/dependency-submission/main.ts index c9e99a99..0e560db1 100644 --- a/sources/src/dependency-submission/main.ts +++ b/sources/src/dependency-submission/main.ts @@ -1,5 +1,3 @@ -import * as core from '@actions/core' - import * as setupGradle from '../setup-gradle' import * as gradle from '../execution/gradle' import * as dependencyGraph from '../dependency-graph' @@ -14,6 +12,7 @@ import { setActionId } from '../configuration' import {saveDeprecationState} from '../deprecation-collector' +import {handleMainActionError} from '../errors' /** * The main entry point for the action, called by Github Actions for the step. @@ -56,10 +55,7 @@ export async function run(): Promise { saveDeprecationState() } catch (error) { - core.setFailed(String(error)) - if (error instanceof Error && error.stack) { - core.info(error.stack) - } + handleMainActionError(error) } // Explicit process.exit() to prevent waiting for hanging promises. diff --git a/sources/src/dependency-submission/post.ts b/sources/src/dependency-submission/post.ts index 50ab70e9..743478e5 100644 --- a/sources/src/dependency-submission/post.ts +++ b/sources/src/dependency-submission/post.ts @@ -1,13 +1,12 @@ -import * as core from '@actions/core' import * as setupGradle from '../setup-gradle' import {CacheConfig, SummaryConfig} from '../configuration' -import {PostActionJobFailure} from '../errors' +import {handlePostActionError} from '../errors' // Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to // throw an uncaught exception. Instead of failing this action, just warn. -process.on('uncaughtException', e => handleFailure(e)) +process.on('uncaughtException', e => handlePostActionError(e)) /** * The post-execution entry point for the action, called by Github Actions after completing all steps for the Job. @@ -16,22 +15,11 @@ export async function run(): Promise { try { await setupGradle.complete(new CacheConfig(), new SummaryConfig()) } catch (error) { - if (error instanceof PostActionJobFailure) { - core.setFailed(String(error)) - } else { - handleFailure(error) - } + handlePostActionError(error) } // Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save. process.exit() } -function handleFailure(error: unknown): void { - core.warning(`Unhandled error in Gradle post-action - job will continue: ${error}`) - if (error instanceof Error && error.stack) { - core.info(error.stack) - } -} - run() diff --git a/sources/src/errors.ts b/sources/src/errors.ts index 5236ec4e..f3c4cb5d 100644 --- a/sources/src/errors.ts +++ b/sources/src/errors.ts @@ -1,4 +1,6 @@ -export class PostActionJobFailure extends Error { +import * as core from '@actions/core' + +export class JobFailure extends Error { constructor(error: unknown) { if (error instanceof Error) { super(error.message) @@ -9,3 +11,33 @@ export class PostActionJobFailure extends Error { } } } + +export function handleMainActionError(error: unknown): void { + if (error instanceof AggregateError) { + core.setFailed(`Multiple errors returned`) + for (const err of error.errors) { + core.error(`Error ${error.errors.indexOf(err)}: ${err.message}`) + if (err.stack) { + core.info(err.stack) + } + } + } else if (error instanceof JobFailure) { + core.setFailed(String(error)) // No stack trace for JobFailure: these are known errors + } else { + core.setFailed(String(error)) + if (error instanceof Error && error.stack) { + core.info(error.stack) + } + } +} + +export function handlePostActionError(error: unknown): void { + if (error instanceof JobFailure) { + core.setFailed(String(error)) + } else { + core.warning(`Unhandled error in Gradle post-action - job will continue: ${error}`) + if (error instanceof Error && error.stack) { + core.info(error.stack) + } + } +} diff --git a/sources/src/setup-gradle.ts b/sources/src/setup-gradle.ts index d4d3a334..bcf2d399 100644 --- a/sources/src/setup-gradle.ts +++ b/sources/src/setup-gradle.ts @@ -10,6 +10,8 @@ import {loadBuildResults, markBuildResultsProcessed} from './build-results' import {CacheListener, generateCachingReport} from './caching/cache-reporting' import {DaemonController} from './daemon-controller' import {BuildScanConfig, CacheConfig, SummaryConfig, getWorkspaceDirectory} from './configuration' +import {findInvalidWrapperJars} from './wrapper-validation/validate' +import {JobFailure} from './errors' const GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED' const USER_HOME = 'USER_HOME' @@ -96,3 +98,16 @@ async function determineUserHome(): Promise { core.debug(`Determined user.home from java -version output: '${userHome}'`) return userHome } + +export async function checkNoInvalidWrapperJars(rootDir = getWorkspaceDirectory()): Promise { + const allowedChecksums = process.env['ALLOWED_GRADLE_WRAPPER_CHECKSUMS']?.split(',') || [] + const result = await findInvalidWrapperJars(rootDir, 1, false, allowedChecksums) + if (result.isValid()) { + core.info(result.toDisplayString()) + } else { + core.info(result.toDisplayString()) + throw new JobFailure( + `Gradle Wrapper Validation Failed!\n See https://github.com/gradle/actions/blob/main/docs/wrapper-validation.md#reporting-failures\n${result.toDisplayString()}` + ) + } +} diff --git a/sources/src/setup-gradle/main.ts b/sources/src/setup-gradle/main.ts index 1cada07d..f5bc5c45 100644 --- a/sources/src/setup-gradle/main.ts +++ b/sources/src/setup-gradle/main.ts @@ -1,5 +1,3 @@ -import * as core from '@actions/core' - import * as setupGradle from '../setup-gradle' import * as gradle from '../execution/gradle' import * as dependencyGraph from '../dependency-graph' @@ -8,10 +6,12 @@ import { CacheConfig, DependencyGraphConfig, GradleExecutionConfig, + doValidateWrappers, getActionId, setActionId } from '../configuration' import {recordDeprecation, saveDeprecationState} from '../deprecation-collector' +import {handleMainActionError} from '../errors' /** * The main entry point for the action, called by Github Actions for the step. @@ -26,6 +26,11 @@ export async function run(): Promise { setActionId('gradle/actions/setup-gradle') } + // Check for invalid wrapper JARs if requested + if (doValidateWrappers()) { + await setupGradle.checkNoInvalidWrapperJars() + } + // Configure Gradle environment (Gradle User Home) await setupGradle.setup(new CacheConfig(), new BuildScanConfig()) @@ -41,10 +46,7 @@ export async function run(): Promise { saveDeprecationState() } catch (error) { - core.setFailed(String(error)) - if (error instanceof Error && error.stack) { - core.info(error.stack) - } + handleMainActionError(error) } // Explicit process.exit() to prevent waiting for hanging promises. diff --git a/sources/src/setup-gradle/post.ts b/sources/src/setup-gradle/post.ts index 4f7b9d4e..b39f77d1 100644 --- a/sources/src/setup-gradle/post.ts +++ b/sources/src/setup-gradle/post.ts @@ -1,15 +1,14 @@ -import * as core from '@actions/core' import * as setupGradle from '../setup-gradle' import * as dependencyGraph from '../dependency-graph' import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../configuration' -import {PostActionJobFailure} from '../errors' +import {handlePostActionError} from '../errors' import {emitDeprecationWarnings, restoreDeprecationState} from '../deprecation-collector' // Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to // throw an uncaught exception. Instead of failing this action, just warn. -process.on('uncaughtException', e => handleFailure(e)) +process.on('uncaughtException', e => handlePostActionError(e)) /** * The post-execution entry point for the action, called by Github Actions after completing all steps for the Job. @@ -24,22 +23,11 @@ export async function run(): Promise { await dependencyGraph.complete(new DependencyGraphConfig()) } } catch (error) { - if (error instanceof PostActionJobFailure) { - core.setFailed(String(error)) - } else { - handleFailure(error) - } + handlePostActionError(error) } // Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save. process.exit() } -function handleFailure(error: unknown): void { - core.warning(`Unhandled error in Gradle post-action - job will continue: ${error}`) - if (error instanceof Error && error.stack) { - core.info(error.stack) - } -} - run() diff --git a/sources/src/wrapper-validation/main.ts b/sources/src/wrapper-validation/main.ts index 9fb30705..34b13871 100644 --- a/sources/src/wrapper-validation/main.ts +++ b/sources/src/wrapper-validation/main.ts @@ -2,6 +2,7 @@ import * as path from 'path' import * as core from '@actions/core' import * as validate from './validate' +import {handleMainActionError} from '../errors' export async function run(): Promise { try { @@ -15,23 +16,14 @@ export async function run(): Promise { core.info(result.toDisplayString()) } else { core.setFailed( - `Gradle Wrapper Validation Failed!\n See https://github.com/gradle/wrapper-validation-action#reporting-failures\n${result.toDisplayString()}` + `Gradle Wrapper Validation Failed!\n See https://github.com/gradle/actions/blob/main/docs/wrapper-validation.md#reporting-failures\n${result.toDisplayString()}` ) if (result.invalid.length > 0) { core.setOutput('failed-wrapper', `${result.invalid.map(w => w.path).join('|')}`) } } } catch (error) { - if (error instanceof AggregateError) { - core.setFailed(`Multiple errors returned`) - for (const err of error.errors) { - core.error(`Error ${error.errors.indexOf(err)}: ${err.message}`) - } - } else if (error instanceof Error) { - core.setFailed(error.message) - } else { - core.setFailed(`Unknown object was thrown: ${error}`) - } + handleMainActionError(error) } }