Allow wrapper validation via the 'setup-gradle' action (#162)

Adds a 'validate-wrappers' option to `gradle/actions/setup-gradle`,
which defaults to 'false'.
When 'true', the action will first validate all Gradle wrappers in the
repository before proceeding.

Fixes #161
This commit is contained in:
Daz DeBoer 2024-04-11 12:40:45 -06:00 committed by GitHub
commit 38e549269f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 148 additions and 66 deletions

View file

@ -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

View file

@ -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"]'

View file

@ -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

23
build
View file

@ -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

View file

@ -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:

View file

@ -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'.

View file

@ -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')

View file

@ -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)}`)

View file

@ -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<void> {
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.

View file

@ -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<void> {
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()

View file

@ -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)
}
}
}

View file

@ -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<string> {
core.debug(`Determined user.home from java -version output: '${userHome}'`)
return userHome
}
export async function checkNoInvalidWrapperJars(rootDir = getWorkspaceDirectory()): Promise<void> {
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()}`
)
}
}

View file

@ -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<void> {
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<void> {
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.

View file

@ -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<void> {
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()

View file

@ -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<void> {
try {
@ -15,23 +16,14 @@ export async function run(): Promise<void> {
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)
}
}