Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradle V4: New major version of the Gradle task #20596

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ Tasks/GradleV2/ @microsoft/akvelon-build-task-team

Tasks/GradleV3/ @microsoft/akvelon-build-task-team

Tasks/GradleV4/ @microsoft/akvelon-build-task-team

Tasks/GruntV0/ @microsoft/akvelon-build-task-team

Tasks/GulpV0/ @microsoft/akvelon-build-task-team
Expand Down
96 changes: 96 additions & 0 deletions Tasks/GradleV4/Modules/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as tl from 'azure-pipelines-task-lib/task';
import * as javaCommon from 'azure-pipelines-tasks-java-common/java-common';
import { IExecOptions, ToolRunner } from 'azure-pipelines-task-lib/toolrunner';

// Setting the access token env var to both VSTS and AZURE_ARTIFACTS for
// backwards compatibility with repos that already use the older env var.
const accessTokenEnvSettingLegacy: string = 'VSTS_ENV_ACCESS_TOKEN';
const accessTokenEnvSetting: string = 'AZURE_ARTIFACTS_ENV_ACCESS_TOKEN';

/**
* Extract system access token from endpoint
* @returns {string} access token to access account feeds or empty string
*/
function getSystemAccessToken(): string {
tl.debug('Getting credentials for account feeds');

const authorizationData: tl.EndpointAuthorization = tl.getEndpointAuthorization('SYSTEMVSSCONNECTION', false);

if (authorizationData && authorizationData.scheme === 'OAuth') {
tl.debug('Got auth token');
return authorizationData.parameters['AccessToken'];
}

tl.warning(tl.loc('FeedTokenUnavailable'));

return '';
}

/**
* Update JAVA_HOME if user selected specific JDK version or set path manually
* @param {string} javaHomeSelection - value of the `Set JAVA_HOME by` task input
*/
export function setJavaHome(javaHomeSelection: string): void {
let specifiedJavaHome: string;
let javaTelemetryData: any = {};

if (javaHomeSelection === 'JDKVersion') {
tl.debug('Using JDK version to find and set JAVA_HOME');

const jdkVersion: string = tl.getInput('jdkVersion');
const jdkArchitecture: string = tl.getInput('jdkArchitecture');

javaTelemetryData = { 'jdkVersion': jdkVersion };

if (jdkVersion !== 'default') {
specifiedJavaHome = javaCommon.findJavaHome(jdkVersion, jdkArchitecture);
}
} else {
tl.debug('Using path from user input to set JAVA_HOME');

const jdkUserInputPath: string = tl.getPathInput('jdkUserInputPath', true, true);
specifiedJavaHome = jdkUserInputPath;

javaTelemetryData = { 'jdkVersion': 'custom' };
}

javaCommon.publishJavaTelemetry('Gradle', javaTelemetryData);

if (specifiedJavaHome) {
tl.debug(`Set JAVA_HOME to ${specifiedJavaHome}`);
process.env['JAVA_HOME'] = specifiedJavaHome;
}
}

/**
* Get execution options for Gradle.
*
* This function does the following things:
* - Get a snapshot of the process environment variables
* - Update the snapshot to include system access token
* @returns {IExecOptions} object with execution options for Gradle
*/
export function getExecOptions(): IExecOptions {
const env: NodeJS.ProcessEnv = process.env;
env[accessTokenEnvSetting] = env[accessTokenEnvSettingLegacy] = getSystemAccessToken();
return <IExecOptions>{
env: env
};
}

/**
* Configure the JVM associated with this run.
* @param {string} gradleOptions - Gradle options provided by the user
*/
export function setGradleOpts(gradleOptions: string): void {
if (gradleOptions) {
process.env['GRADLE_OPTS'] = gradleOptions;
tl.debug(`GRADLE_OPTS is now set to ${gradleOptions}`);
}
}

export function extractGradleVersion(str: string): string {
const regex = /^Gradle (?<version>\d+\.\d+(?:\.\d+)?.*$)/m;
const match = str.match(regex);
return match?.groups?.version || 'unknown';
}
36 changes: 36 additions & 0 deletions Tasks/GradleV4/Modules/project-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as fs from 'fs';
import * as os from 'os';
import * as tl from 'azure-pipelines-task-lib/task';


/**
* Configure wrapper script:
* - For Windows - set `*.bat` extension
* - For Linux/macOS - set script as executable
* @param {string} wrapperScript - Relative path from the repository root to the Gradle Wrapper script.
* @returns {string} path to the wrapper script
*/
export function configureWrapperScript(wrapperScript: string): string {
let script: string = wrapperScript;
const isWindows: RegExpMatchArray = os.type().match(/^Win/);

if (isWindows) {
// append .bat extension name on Windows platform
if (!script.endsWith('bat')) {
tl.debug('Append .bat extension name to gradlew script.');
script += '.bat';
}
}

if (fs.existsSync(script)) {
try {
// Make sure the wrapper script is executable
fs.accessSync(script, fs.constants.X_OK)
} catch (err) {
// If not, show warning and chmodding the gradlew file to make it executable
tl.warning(tl.loc('chmodGradlew'));
fs.chmodSync(script, '755');
}
}
return script;
}
46 changes: 46 additions & 0 deletions Tasks/GradleV4/Modules/publish-test-results.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as tl from 'azure-pipelines-task-lib/task';

const TESTRUN_SYSTEM = 'VSTS - gradle';


/**
* Publish unit tests results to Azure DevOps
* @param {boolean} publishJUnitResults - if set to `true`, the result of the unit tests will be published otherwise publishing will be skipped
* @param {string} testResultsFiles - pattern for test results files
*/
export function publishTestResults(publishJUnitResults: boolean, testResultsFiles: string): number {
if (publishJUnitResults) {
let matchingTestResultsFiles: string[] = [];

// check for pattern in testResultsFiles
if (testResultsFiles.indexOf('*') >= 0 || testResultsFiles.indexOf('?') >= 0) {
tl.debug('Pattern found in testResultsFiles parameter');

const buildFolder: string = tl.getVariable('System.DefaultWorkingDirectory');

// The find options are as default, except the `skipMissingFiles` option is set to `true`
// so there will be a warning instead of an error if an item will not be found
const findOptions: tl.FindOptions = {
allowBrokenSymbolicLinks: false,
followSpecifiedSymbolicLink: true,
followSymbolicLinks: true,
skipMissingFiles: true
};

matchingTestResultsFiles = tl.findMatch(buildFolder, testResultsFiles, findOptions, { matchBase: true });
} else {
tl.debug('No pattern found in testResultsFiles parameter');
matchingTestResultsFiles = [testResultsFiles];
}

if (!matchingTestResultsFiles || matchingTestResultsFiles.length === 0) {
console.log(tl.loc('NoTestResults', testResultsFiles));
return 0;
}

const tp: tl.TestPublisher = new tl.TestPublisher('JUnit');
const testRunTitle = tl.getInput('testRunTitle');

tp.publish(matchingTestResultsFiles, 'true', '', '', testRunTitle, 'true', TESTRUN_SYSTEM);
}
}
32 changes: 32 additions & 0 deletions Tasks/GradleV4/Modules/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ITaskResult, ICodeAnalysisResult } from '../interfaces';
import { TaskResult } from 'azure-pipelines-task-lib';

/**
* Resolve task status based on code analysis run results
* @param {ICodeAnalysisResult} codeAnalysisResult - Code analysis run data
* @returns {ITaskResult} task status and message
*/
export function resolveTaskResult(codeAnalysisResult: ICodeAnalysisResult): ITaskResult {
let status: TaskResult;
let message: string = '';

if (codeAnalysisResult.gradleResult === 0) {
status = TaskResult.Succeeded;
message = 'Build succeeded.';
} else if (codeAnalysisResult.gradleResult === -1) {
status = TaskResult.Failed;

if (codeAnalysisResult.statusFailed) {
message = `Code analysis failed. Gradle exit code: ${codeAnalysisResult.gradleResult}. Error: ${codeAnalysisResult.analysisError}`;
} else {
message = `Build failed. Gradle exit code: ${codeAnalysisResult.gradleResult}`;
}
}

const taskResult: ITaskResult = {
status: status,
message: message
};

return taskResult;
}
71 changes: 71 additions & 0 deletions Tasks/GradleV4/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Build your code using Gradle in Azure Pipelines

### Parameters for Gradle build task are explained below

- **Gradle Wrapper :** This is a Required field. The location in the repository of the gradlew wrapper used for the build. Note that on Windows build agents (including the hosted pool), you must use the `gradlew.bat` wrapper. Xplat build agents use the `gradlew` shell script. To Know more [click here](https://docs.gradle.org/current/userguide/gradle_wrapper.html)

- **Options :** Specify any command line options you want to pass to the Gradle wrapper. To know more [click here](https://docs.gradle.org/current/userguide/gradle_command_line.html)

- **Goal(s) :** The task(s) for Gradle to execute. A list of tasks can be taken from `gradlew tasks` issued from a command prompt. To know more [click here](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html)

#### JUnit Test Results
Use the next three options to manage your JUnit test results in Azure Pipelines

- **Publish to Azure Pipelines :** Select this option to publish JUnit Test results produced by the Gradle build to Azure Pipelines/TFS. Each test result file matching `Test Results Files` will be published as a test run in Azure Pipelines.

- **Test Results Files :** This option will appear if you select the above option. Here, provide Test results files path. Wildcards can be used. For example, `**/TEST-*.xml` for all xml files whose name starts with `TEST-."`

- **Test Run Title :** This option will appear if you select the `Publish to Azure Pipelines/TFS` option. Here provide a name for the Test Run

#### Advanced
Use the next options to manage your `JAVA_HOME` attribute by JDK Version and Path

- **Working Directory :** Directory on the build agent where the Gradle wrapper will be invoked from. Defaults to the repository root.

- **Set JAVA_HOME by :** Select to set `JAVA_HOME` either by providing a path or let Azure Pipelines set the `JAVA_HOME` based on JDK version choosen. By default it is set to `JDK Version`

- **JDK Version :** Here provide the PATH to `JAVA_HOME` if you want to set it by path or select the appropriate JDK version.

- **JDK Architecture :** Select the approriate JDK Architecture. By default it is set to `x86`

#### Code Analysis

- **Run SonarQube Analysis :** You can choose to run SonarQube analysis after executing the current goals. 'install' or 'package' goals should be executed first. To know more about this option [click here](https://blogs.msdn.com/b/visualstudioalm/archive/2015/10/08/the-maven-build-task-now-simplifies-sonarqube-analysis.aspx)

- **Run Checkstyle :** You can choose to run the Checkstyle static code analysis tool, which checks the compliance of your source code with coding rules. You will receive a code analysis report with the number of violations detected, as well as the original report files if there were any violations.

- **Run PMD :** You can choose to run the PMD static code analysis tool, which examines your source code for possible bugs. You will receive a code analysis report with the number of violations detected, as well as the original report files if there were any violations.

- **Run FindBugs :** You can choose to run the FindBugs static code analysis tool, which examines the bytecode of your program for possible bugs. You will receive a code analysis report with the number of violations detected, as well as the original report files if there were any violations.

### Q&A

#### How do I generate a wrapper from my Gradle project?

The Gradle wrapper allows the build agent to download and configure the exact Gradle environment that is checked into the repository without having any software configuration on the build agent itself other than the JVM.

- **1.** Create the Gradle wrapper by issuing the following command from the root project directory where your build.gradle resides:
`jamal@fabrikam> gradle wrapper`


- **2.** Upload your Gradle wrapper to your remote repository.

There is a binary artifact that is generated by the gradle wrapper (located at `gradle/wrapper/gradle-wrapper.jar`). This binary file is small and doesn't require updating. If you need to change the Gradle configuration run on the build agent, you update the `gradle-wrapper.properties`.

The repository should look something like this:

```ssh
|-- gradle/
`-- wrapper/
`-- gradle-wrapper.jar
`-- gradle-wrapper.properties
|-- src/
|-- .gitignore
|-- build.gradle
|-- gradlew
|-- gradlew.bat
```




Loading
Loading