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 # Downloads a 'dist' directory artifact that was uploaded in an earlier 'build-dist' step
- name: Download dist - name: Download dist
if: ${{ !env.ACT }}
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: dist name: dist

View File

@ -183,3 +183,5 @@ jobs:
wrapper-validation: wrapper-validation:
needs: [determine-suite, build-distribution] needs: [determine-suite, build-distribution]
uses: ./.github/workflows/integ-test-wrapper-validation.yml 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: on:
workflow_call: workflow_call:
inputs:
runner-os:
type: string
default: '["ubuntu-latest", "windows-latest", "macos-latest"]'
jobs: 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: test-validation-success:
name: 'Test: Validation success'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
@ -33,9 +61,7 @@ jobs:
exit 1 exit 1
fi fi
# Integration test for failing validation of wrappers
test-validation-error: test-validation-error:
name: 'Test: Validation error'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources

23
build
View File

@ -3,8 +3,21 @@
cd sources cd sources
npm install npm install
if [ "$1" == "all" ]; then case "$1" in
npm run all all)
else nprm run all
npm run build ;;
fi 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`. 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. 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) ## 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: 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". description: Indicate that you agree to the Build Scan® terms of use. This input value must be "yes".
required: false 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 # DEPRECATED ACTION INPUTS
build-scan-terms-of-service-url: 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'. 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 // Internal parameters
export function getJobMatrix(): string { export function getJobMatrix(): string {
return core.getInput('workflow-job-context') 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 * as path from 'path'
import fs from 'fs' import fs from 'fs'
import {PostActionJobFailure} from './errors' import {JobFailure} from './errors'
import {DependencyGraphConfig, DependencyGraphOption, getGithubToken, getWorkspaceDirectory} from './configuration' import {DependencyGraphConfig, DependencyGraphOption, getGithubToken, getWorkspaceDirectory} from './configuration'
const DEPENDENCY_GRAPH_PREFIX = 'dependency-graph_' const DEPENDENCY_GRAPH_PREFIX = 'dependency-graph_'
@ -208,7 +208,7 @@ function markProcessed(dependencyGraphFile: string): void {
function warnOrFail(config: DependencyGraphConfig, option: String, error: unknown): void { function warnOrFail(config: DependencyGraphConfig, option: String, error: unknown): void {
if (!config.getDependencyGraphContinueOnFailure()) { if (!config.getDependencyGraphContinueOnFailure()) {
throw new PostActionJobFailure(error) throw new JobFailure(error)
} }
core.warning(`Failed to ${option} dependency graph. Will continue.\n${String(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 setupGradle from '../setup-gradle'
import * as gradle from '../execution/gradle' import * as gradle from '../execution/gradle'
import * as dependencyGraph from '../dependency-graph' import * as dependencyGraph from '../dependency-graph'
@ -14,6 +12,7 @@ import {
setActionId setActionId
} from '../configuration' } from '../configuration'
import {saveDeprecationState} from '../deprecation-collector' import {saveDeprecationState} from '../deprecation-collector'
import {handleMainActionError} from '../errors'
/** /**
* The main entry point for the action, called by Github Actions for the step. * 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() saveDeprecationState()
} catch (error) { } catch (error) {
core.setFailed(String(error)) handleMainActionError(error)
if (error instanceof Error && error.stack) {
core.info(error.stack)
}
} }
// Explicit process.exit() to prevent waiting for hanging promises. // 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 * as setupGradle from '../setup-gradle'
import {CacheConfig, SummaryConfig} from '../configuration' 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 // 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 // @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. // 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. * 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 { try {
await setupGradle.complete(new CacheConfig(), new SummaryConfig()) await setupGradle.complete(new CacheConfig(), new SummaryConfig())
} catch (error) { } catch (error) {
if (error instanceof PostActionJobFailure) { handlePostActionError(error)
core.setFailed(String(error))
} else {
handleFailure(error)
}
} }
// Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save. // Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save.
process.exit() 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() 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) { constructor(error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
super(error.message) 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 {CacheListener, generateCachingReport} from './caching/cache-reporting'
import {DaemonController} from './daemon-controller' import {DaemonController} from './daemon-controller'
import {BuildScanConfig, CacheConfig, SummaryConfig, getWorkspaceDirectory} from './configuration' 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 GRADLE_SETUP_VAR = 'GRADLE_BUILD_ACTION_SETUP_COMPLETED'
const USER_HOME = 'USER_HOME' const USER_HOME = 'USER_HOME'
@ -96,3 +98,16 @@ async function determineUserHome(): Promise<string> {
core.debug(`Determined user.home from java -version output: '${userHome}'`) core.debug(`Determined user.home from java -version output: '${userHome}'`)
return 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 setupGradle from '../setup-gradle'
import * as gradle from '../execution/gradle' import * as gradle from '../execution/gradle'
import * as dependencyGraph from '../dependency-graph' import * as dependencyGraph from '../dependency-graph'
@ -8,10 +6,12 @@ import {
CacheConfig, CacheConfig,
DependencyGraphConfig, DependencyGraphConfig,
GradleExecutionConfig, GradleExecutionConfig,
doValidateWrappers,
getActionId, getActionId,
setActionId setActionId
} from '../configuration' } from '../configuration'
import {recordDeprecation, saveDeprecationState} from '../deprecation-collector' import {recordDeprecation, saveDeprecationState} from '../deprecation-collector'
import {handleMainActionError} from '../errors'
/** /**
* The main entry point for the action, called by Github Actions for the step. * 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') setActionId('gradle/actions/setup-gradle')
} }
// Check for invalid wrapper JARs if requested
if (doValidateWrappers()) {
await setupGradle.checkNoInvalidWrapperJars()
}
// Configure Gradle environment (Gradle User Home) // Configure Gradle environment (Gradle User Home)
await setupGradle.setup(new CacheConfig(), new BuildScanConfig()) await setupGradle.setup(new CacheConfig(), new BuildScanConfig())
@ -41,10 +46,7 @@ export async function run(): Promise<void> {
saveDeprecationState() saveDeprecationState()
} catch (error) { } catch (error) {
core.setFailed(String(error)) handleMainActionError(error)
if (error instanceof Error && error.stack) {
core.info(error.stack)
}
} }
// Explicit process.exit() to prevent waiting for hanging promises. // 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 setupGradle from '../setup-gradle'
import * as dependencyGraph from '../dependency-graph' import * as dependencyGraph from '../dependency-graph'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../configuration' import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../configuration'
import {PostActionJobFailure} from '../errors' import {handlePostActionError} from '../errors'
import {emitDeprecationWarnings, restoreDeprecationState} from '../deprecation-collector' import {emitDeprecationWarnings, restoreDeprecationState} from '../deprecation-collector'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // 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 // @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. // 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. * 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()) await dependencyGraph.complete(new DependencyGraphConfig())
} }
} catch (error) { } catch (error) {
if (error instanceof PostActionJobFailure) { handlePostActionError(error)
core.setFailed(String(error))
} else {
handleFailure(error)
}
} }
// Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save. // Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save.
process.exit() 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() run()

View File

@ -2,6 +2,7 @@ import * as path from 'path'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as validate from './validate' import * as validate from './validate'
import {handleMainActionError} from '../errors'
export async function run(): Promise<void> { export async function run(): Promise<void> {
try { try {
@ -15,23 +16,14 @@ export async function run(): Promise<void> {
core.info(result.toDisplayString()) core.info(result.toDisplayString())
} else { } else {
core.setFailed( 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) { if (result.invalid.length > 0) {
core.setOutput('failed-wrapper', `${result.invalid.map(w => w.path).join('|')}`) core.setOutput('failed-wrapper', `${result.invalid.map(w => w.path).join('|')}`)
} }
} }
} catch (error) { } catch (error) {
if (error instanceof AggregateError) { handleMainActionError(error)
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}`)
}
} }
} }