Initial impl

This commit is contained in:
Paul Merlin 2019-09-21 16:01:53 +02:00
parent f4a8f7a81b
commit bc921df1ec
11 changed files with 1296 additions and 1055 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
__tests__/runner/* __tests__/runner/*
lib/
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs # Logs

124
README.md
View file

@ -1,9 +1,123 @@
# Create a JavaScript Action using TypeScript # Execute Gradle commands in Github Actions workflows
This template offers an easy way to get started writing a JavaScript action with TypeScript compile time support, unit testing with Jest and using the GitHub Actions Toolkit. This Github Action can be used to run arbitrary Gradle commands on any platform supported by Github Actions.
## Getting Started
See the walkthrough located [here](https://github.com/actions/toolkit/blob/master/docs/typescript-action.md). ## Usage
In addition to walking your through how to create an action, it also provides strategies for versioning, releasing and referencing your actions. The following workflow will run `gradle build` using the wrapper from the repository on ubuntu, macos and windows:
```yaml
// .github/workflows/gradle-build-pr.yml
name: Run Gradle on PRs
on: pull-request
jobs:
gradle:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: eskatos/gradle-command-action@v1
with:
arguments: build
```
## Gradle arguments
The `arguments` input can used to pass arbitrary arguments to the `gradle` command line.
Here are some valid examples:
```yaml
arguments: build
arguments: check --scan
arguments: some arbitrary tasks
arguments: build -PgradleProperty=foo
arguments: build -DsystemProperty=bar
....
```
See `gradle --help` for more information.
If you need to pass environment variables, simply use the Github Actions workflow syntax:
```yaml
- uses: eskatos/gradle-command-action@v1
env:
CI: true
```
## Run a build from a different directory
```yaml
- uses: eskatos/gradle-command-action@v1
with:
build-root-directory: some/subdirectory
```
## Use a Gradle wrapper from a different directory:
```yaml
- uses: eskatos/gradle-command-action@v1
with:
wrapper-directory: path/to/wrapper-directory
```
## Use a specific `gradle` executable
```yaml
- uses: eskatos/gradle-command-action@v1
with:
gradle-executable: path/to/gradle
```
## Setup and use a declared Gradle version:
```yaml
- uses: eskatos/gradle-command-action@v1
with:
gradle-version: 5.6.2
```
`gradle-version` can be set to any valid Gradle version.
Moreover, you can use the following aliases:
| Alias | Selects |
| --- |---|
| `current` | The current [stable release](https://gradle.org/install/) |
| `rc` | The current [release candidate](https://gradle.org/release-candidate/) if any, otherwise fallback to `current` |
| `nightly` | The latest [nightly](https://gradle.org/nightly/), fails if none. |
| `release-nightly` | The latest [release nightly](https://gradle.org/release-nightly/), fails if none. |
This can be handy to automatically test your build with the next Gradle version once a release candidate is out:
```yaml
// .github/workflows/test-gradle-rc.yml
name: Test latest Gradle RC
on:
schedule:
- cron: 0 0 * * * # daily
jobs:
gradle-rc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-java@v1
with:
java-version: 11
- uses: eskatos/gradle-command-action@v1
with:
gradle-version: rc
arguments: build --dry-run # just test build configuration
```
# Build scans
If your build publishes a [build scan](https://gradle.com/build-scans/) the `gradle-command-action` action will emit the link to the published build scan as an output named `build-scan-url`.
You can then use that link in subsequent actions of your workflow.

View file

@ -1,15 +1,34 @@
name: "gradle" name: "Gradle Command"
description: 'Provide a description here' description: 'Execute Gradle Command Line'
author: 'Your name or organization here' author: 'Paul Merlin <paul@nospere.org>'
# TODO build scan link as an output # https://help.github.com/en/articles/metadata-syntax-for-github-actions
# https://help.github.com/en/articles/metadata-syntax-for-github-actions#outputs
inputs: inputs:
args: wrapper-directory:
description: Path to the Gradle Wrapper directory
required: false
gradle-executable:
description: Path to the Gradle executable
required: false
gradle-version:
description: Gradle version to use
required: false
build-root-directory:
description: Path to the root directory of the build
required: false
arguments:
description: Gradle command line arguments, see gradle --help description: Gradle command line arguments, see gradle --help
required: false required: false
outputs:
build-scan-url:
description: Link to the build scan if any
runs: runs:
using: 'node12' using: 'node12'
main: 'lib/main.js' main: 'lib/main.js'
branding:
icon: 'box'
color: 'gray-dark'

View file

@ -1,30 +0,0 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core"));
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
const myInput = core.getInput('myInput');
core.debug(`Hello ${myInput}`);
}
catch (error) {
core.setFailed(error.message);
}
});
}
run();

1808
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
{ {
"name": "javascript-template-action", "name": "gradle-command-action",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"description": "JavaScript template action", "description": "Execute Gradle Command Line",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
@ -20,14 +20,21 @@
"author": "YourNameOrOrganization", "author": "YourNameOrOrganization",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.0.0" "@actions/core": "1.1.1",
"@actions/exec": "1.0.1",
"@actions/io": "1.0.1",
"@actions/tool-cache": "1.1.1",
"string-argv": "0.3.1",
"typed-rest-client": "1.5.0",
"unzipper": "0.10.5"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^24.0.13", "@types/jest": "24.0.18",
"@types/node": "^12.0.4", "@types/node": "12.7.5",
"jest": "^24.8.0", "@types/unzipper": "0.10.0",
"jest-circus": "^24.7.1", "jest": "24.9.0",
"ts-jest": "^24.0.2", "jest-circus": "24.9.0",
"typescript": "^3.5.1" "ts-jest": "24.1.0",
"typescript": "3.6.3"
} }
} }

40
src/execution.ts Normal file
View file

@ -0,0 +1,40 @@
import * as exec from "@actions/exec";
export async function execute(executable: string, root: string, argv: string[]): Promise<BuildResult> {
let publishing = false;
let buildScanLink: any = null;
await exec.exec(executable, argv, {
cwd: root,
listeners: {
stdline: (line: string) => {
if (line.startsWith("Publishing build scan...")) {
publishing = true;
}
if (publishing && line.length == 0) {
publishing = false
}
if (publishing && line.startsWith("http")) {
buildScanLink = line.trim();
publishing = false
}
}
}
});
if (buildScanLink != null) {
return new BuildResultImpl(buildScanLink.toString());
}
return new BuildResultImpl(null as unknown as string);
}
export interface BuildResult {
buildScanUrl: string
}
class BuildResultImpl implements BuildResult {
constructor(readonly buildScanUrl: string) {
}
}

9
src/gradlew.ts Normal file
View file

@ -0,0 +1,9 @@
export function wrapperFilename() {
const isWindows = process.platform === "win32";
return isWindows ? "gradlew.bat" : "gradlew";
}
export function installScriptFilename() {
const isWindows = process.platform === "win32";
return isWindows ? "gradle.bat" : "gradle";
}

View file

@ -1,12 +1,74 @@
import * as core from '@actions/core'; import * as core from "@actions/core";
import * as path from "path";
import {parseArgsStringToArgv} from "string-argv";
import * as execution from "./execution";
import * as gradlew from "./gradlew";
import * as provision from "./provision";
// Invoked by Github Actions
async function run() { async function run() {
try { try {
const myInput = core.getInput('myInput');
core.debug(`Hello ${myInput}`); const baseDirectory = process.env[`GITHUB_WORKSPACE`] || "";
let result = await execution.execute(
await resolveGradleExecutable(baseDirectory),
resolveBuildRootDirectory(baseDirectory),
parseCommandLineArguments()
);
if (result.buildScanUrl != null) {
core.setOutput("build-scan-url", result.buildScanUrl);
}
} catch (error) { } catch (error) {
core.setFailed(error.message); core.setFailed(error.message);
} }
} }
run(); run();
async function resolveGradleExecutable(baseDirectory: string): Promise<string> {
const gradleVersion = inputOrNull("gradle-version");
if (gradleVersion != null) {
return provision.gradleVersion(gradleVersion)
}
const gradleExecutable = inputOrNull("gradle-executable");
if (gradleExecutable != null) {
return path.join(baseDirectory, gradleExecutable)
}
const wrapperDirectory = inputOrNull("wrapper-directory");
const executableDirectory = wrapperDirectory != null
? path.join(baseDirectory, wrapperDirectory)
: baseDirectory;
return path.join(executableDirectory, gradlew.wrapperFilename());
}
function resolveBuildRootDirectory(baseDirectory: string): string {
let buildRootDirectory = inputOrNull("build-root-directory");
return buildRootDirectory == null ? baseDirectory : path.join(baseDirectory, buildRootDirectory);
}
function parseCommandLineArguments(): string[] {
const input = inputOrNull("arguments");
return input == null ? [] : parseArgsStringToArgv(input)
}
function inputOrNull(name: string): string | null {
const inputString = core.getInput(name);
if (inputString.length == 0) {
return null;
}
return inputString
}

163
src/provision.ts Normal file
View file

@ -0,0 +1,163 @@
import * as fs from "fs";
import * as path from "path";
import * as httpm from 'typed-rest-client/HttpClient';
import * as unzip from "unzipper"
import * as core from "@actions/core";
import * as io from '@actions/io';
import * as toolCache from "@actions/tool-cache";
import * as gradlew from "./gradlew";
/**
* @return Gradle executable
*/
export async function gradleVersion(gradleVersion: string): Promise<string> {
switch (gradleVersion) {
case "current":
return gradleCurrent();
case "rc":
return gradleReleaseCandidate();
case "nightly":
return gradleNightly();
case "release-nightly":
return gradleReleaseNightly();
default:
return gradle(gradleVersion);
}
}
const gradleVersionsBaseUrl = "https://services.gradle.org/versions";
async function gradleCurrent(): Promise<string> {
const json = await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/current`);
return provisionGradle(json.version, json.downloadUrl);
}
async function gradleReleaseCandidate(): Promise<string> {
const json = await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/release-candidate`);
if (json != null) {
return provisionGradle(json.version, json.downloadUrl);
}
return gradleCurrent();
}
async function gradleNightly(): Promise<string> {
const json = await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/nightly`);
return provisionGradle(json.version, json.downloadUrl);
}
async function gradleReleaseNightly(): Promise<string> {
const json = await gradleVersionDeclaration(`${gradleVersionsBaseUrl}/release-nightly`);
return provisionGradle(json.version, json.downloadUrl);
}
async function gradle(version: string): Promise<string> {
const declaration = await findGradleVersionDeclaration(version);
if (declaration == null) {
throw new Error(`Gradle version ${version} does not exists`);
}
return provisionGradle(declaration.version, declaration.downloadUrl);
}
async function gradleVersionDeclaration(url: string): Promise<any | null> {
const httpc = new httpm.HttpClient("gradle-github-action");
const response = await httpc.get(url);
const body = await response.readBody();
const json = JSON.parse(body);
return (json == null || json.version == null || json.version.length <= 0)
? null
: json
}
async function findGradleVersionDeclaration(version: string): Promise<any | null> {
const httpc = new httpm.HttpClient("gradle-github-action");
const response = await httpc.get(`${gradleVersionsBaseUrl}/all`);
const body = await response.readBody();
const json = JSON.parse(body);
const found = json.find(entry => {
return entry.version == version;
});
return found != undefined ? found : null
}
async function provisionGradle(version: string, url: string): Promise<string> {
const cachedInstall: string = toolCache.find("gradle", version);
if (cachedInstall != null && cachedInstall.length > 0) {
const cachedExecutable = executableFrom(cachedInstall);
core.info(`Provisioned Gradle executable ${cachedExecutable}`);
return cachedExecutable;
}
const home = process.env["HOME"] || "";
const tmpdir = path.join(home, "gradle-provision-tmpdir");
const downloadsDir = path.join(tmpdir, "downloads");
const installsDir = path.join(tmpdir, "installs");
await io.mkdirP(downloadsDir);
await io.mkdirP(installsDir);
core.info(`Downloading ${url}`);
const downloadPath = path.join(downloadsDir, `gradle-${version}-bin.zip`);
await httpDownload(url, downloadPath);
core.info(`Downloaded at ${downloadPath}, size ${fs.statSync(downloadPath).size}`);
await extractZip(downloadPath, installsDir);
const installDir = path.join(installsDir, `gradle-${version}`);
core.info(`Extracted in ${installDir}`);
const executable = executableFrom(installDir);
fs.chmodSync(executable, "755");
core.info(`Provisioned Gradle executable ${executable}`);
toolCache.cacheDir(installDir, "gradle", version);
return executable;
}
function executableFrom(installDir: string): string {
return path.join(installDir, "bin", `${gradlew.installScriptFilename()}`);
}
async function httpDownload(url: string, path: string): Promise<void> {
return new Promise<void>(function (resolve, reject) {
const httpc = new httpm.HttpClient("gradle-github-action");
const writeStream = fs.createWriteStream(path);
httpc.get(url).then(response => {
response.message.pipe(writeStream)
.on("close", () => {
resolve();
})
.on("error", err => {
reject(err)
});
}).catch(reason => {
reject(reason);
});
});
}
async function extractZip(zip: string, destination: string): Promise<void> {
return new Promise<void>(function (resolve, reject) {
fs.createReadStream(zip)
.pipe(unzip.Extract({"path": destination}))
.on("close", () => {
resolve();
})
.on("error", err => {
reject(err)
});
});
}

View file

@ -24,18 +24,18 @@
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */ "strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */ "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */ /* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */