From 7b8939ae834d17c4100faabb72e991958dcb5cd0 Mon Sep 17 00:00:00 2001 From: Janne Valkealahti Date: Fri, 17 Jan 2020 12:58:57 +0000 Subject: [PATCH] Add support for maven/gradle versions - Essentially use same concepts as jdk install to switch maven and gradle versions if those are defined with config. - Bump to tool-cache 1.3.0 as 1.0.0 tries to use node_modules/@actions/tool-cache/scripts/externals/unzip which doesn't have execute flag so you'd get `Permission denied` - `extractZipNix` in toolkit no longer support relative path so needs to resolve absolute path before passing file to these methods. - Commit has my local build for `index.js` which probably should get recreated but makes it easier to test this from a PR branch. - Fixes #40 --- README.md | 22 ++- __tests__/gradle-installer.test.ts | 127 +++++++++++++++++ __tests__/maven-installer.test.ts | 134 ++++++++++++++++++ action.yml | 20 +++ dist/index.js | Bin 159050 -> 222663 bytes package-lock.json | 82 ++++++++--- package.json | 6 +- src/gradle-installer.ts | 204 +++++++++++++++++++++++++++ src/installer.ts | 6 +- src/maven-installer.ts | 218 +++++++++++++++++++++++++++++ src/setup-java.ts | 15 ++ 11 files changed, 801 insertions(+), 33 deletions(-) create mode 100644 __tests__/gradle-installer.test.ts create mode 100644 __tests__/maven-installer.test.ts create mode 100644 src/gradle-installer.ts create mode 100644 src/maven-installer.ts diff --git a/README.md b/README.md index 05b42909..7a663f49 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,25 @@ steps: - uses: actions/setup-java@v1 with: java-version: '4.0.0' - architecture: x64 jdkFile: # Optional - jdkFile to install java from. Useful for versions not found on Zulu Community CDN - run: java -cp java HelloWorldApp ``` +## Maven/Gradle Versions +```yaml +steps: +- uses: actions/checkout@v1 +- uses: actions/setup-java@v1 + with: + java-version: '9.0.4' + maven-version: '3.6.2' + maven-mirror: # Optional - defaults to https://archive.apache.org/dist/maven/maven-3/ + maven-file: # Optional - to install maven from. + gradle-version: '5.6.2' + gradle-file: # Optional - to install gradle from. +- run: java -cp java HelloWorldApp +``` + ## Matrix Testing ```yaml jobs: @@ -87,7 +101,7 @@ jobs: server-password: MAVEN_CENTRAL_TOKEN # env variable for token in deploy - name: Publish to Apache Maven Central - run: mvn deploy + run: mvn deploy env: MAVEN_USERNAME: maven_username123 MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} @@ -117,7 +131,7 @@ The two `settings.xml` files created from the above example look like the follow ``` -***NOTE: The `settings.xml` file is created in the Actions $HOME directory. If you have an existing `settings.xml` file at that location, it will be overwritten. See below for using the `settings-path` to change your `settings.xml` file location.*** +***NOTE: The `settings.xml` file is created in the Actions $HOME directory. If you have an existing `settings.xml` file at that location, it will be overwritten. See below for using the `settings-path` to change your `settings.xml` file location.*** See the help docs on [Publishing a Package](https://help.github.com/en/github/managing-packages-with-github-packages/configuring-apache-maven-for-use-with-github-packages#publishing-a-package) for more information on the `pom.xml` file. @@ -144,7 +158,7 @@ jobs: PASSWORD: ${{ secrets.GITHUB_TOKEN }} ``` -***NOTE: The `USERNAME` and `PASSWORD` need to correspond to the credentials environment variables used in the publishing section of your `build.gradle`.*** +***NOTE: The `USERNAME` and `PASSWORD` need to correspond to the credentials environment variables used in the publishing section of your `build.gradle`.*** See the help docs on [Publishing a Package with Gradle](https://help.github.com/en/github/managing-packages-with-github-packages/configuring-gradle-for-use-with-github-packages#example-using-gradle-groovy-for-a-single-package-in-a-repository) for more information on the `build.gradle` configuration file. diff --git a/__tests__/gradle-installer.test.ts b/__tests__/gradle-installer.test.ts new file mode 100644 index 00000000..b06555cb --- /dev/null +++ b/__tests__/gradle-installer.test.ts @@ -0,0 +1,127 @@ +import io = require('@actions/io'); +import fs = require('fs'); +import path = require('path'); +import child_process = require('child_process'); + +const toolDir = path.join(__dirname, 'runnerg', 'tools'); +const tempDir = path.join(__dirname, 'runnerg', 'temp'); +const gradleDir = path.join(__dirname, 'runnerg', 'gradle'); + +process.env['RUNNER_TOOL_CACHE'] = toolDir; +process.env['RUNNER_TEMP'] = tempDir; +import * as installer from '../src/gradle-installer'; + +let gradleFilePath = ''; +let gradleUrl = ''; +if (process.platform === 'win32') { + gradleFilePath = path.join(gradleDir, 'gradle_win.zip'); + gradleUrl = 'https://services.gradle.org/distributions/gradle-6.0.1-bin.zip'; +} else if (process.platform === 'darwin') { + gradleFilePath = path.join(gradleDir, 'gradle_mac.zip'); + gradleUrl = 'https://services.gradle.org/distributions/gradle-6.0.1-bin.zip'; +} else { + gradleFilePath = path.join(gradleDir, 'gradle_linux.zip'); + gradleUrl = 'https://services.gradle.org/distributions/gradle-6.0.1-bin.zip'; +} + +describe('gradle installer tests', () => { + beforeAll(async () => { + await io.rmRF(toolDir); + await io.rmRF(tempDir); + await io.rmRF(gradleDir); + if (!fs.existsSync(`${gradleFilePath}.complete`)) { + // Download gradle + await io.mkdirP(gradleDir); + + console.log('Downloading gradle'); + child_process.execSync(`curl -L "${gradleUrl}" > "${gradleFilePath}"`); + // Write complete file so we know it was successful + fs.writeFileSync(`${gradleFilePath}.complete`, 'content'); + } + }, 300000); + + afterAll(async () => { + try { + await io.rmRF(toolDir); + await io.rmRF(tempDir); + await io.rmRF(gradleDir); + } catch { + console.log('Failed to remove test directories'); + } + }, 100000); + + it('Installs version of Gradle from gradleFile if no matching version is installed', async () => { + await installer.getGradle('6.0.1', gradleFilePath); + const gradleDir = path.join(toolDir, 'gradle', '6.0.1', 'x64'); + + expect(fs.existsSync(`${gradleDir}.complete`)).toBe(true); + expect(fs.existsSync(path.join(gradleDir, 'bin'))).toBe(true); + }, 100000); + + it('Throws if invalid directory to gradle', async () => { + let thrown = false; + try { + await installer.getGradle('1000', 'bad path'); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + }); + + it('Downloads gradle if no file given', async () => { + await installer.getGradle('5.6.3', ''); + const gradleDir = path.join(toolDir, 'gradle', '5.6.3', 'x64'); + + expect(fs.existsSync(`${gradleDir}.complete`)).toBe(true); + expect(fs.existsSync(path.join(gradleDir, 'bin'))).toBe(true); + }, 100000); + + it('Downloads gradle with 1.x syntax', async () => { + await installer.getGradle('4.10', ''); + const gradleDir = path.join(toolDir, 'gradle', '4.10.3', 'x64'); + + expect(fs.existsSync(`${gradleDir}.complete`)).toBe(true); + expect(fs.existsSync(path.join(gradleDir, 'bin'))).toBe(true); + }, 100000); + + it('Downloads gradle with normal semver syntax', async () => { + await installer.getGradle('4.8.x', ''); + const gradleDir = path.join(toolDir, 'gradle', '4.8.1', 'x64'); + + expect(fs.existsSync(`${gradleDir}.complete`)).toBe(true); + expect(fs.existsSync(path.join(gradleDir, 'bin'))).toBe(true); + }, 100000); + + it('Throws if invalid directory to gradle', async () => { + let thrown = false; + try { + await installer.getGradle('1000', 'bad path'); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + }); + + it('Uses version of gradle installed in cache', async () => { + const gradleDir: string = path.join(toolDir, 'gradle', '250.0.0', 'x64'); + await io.mkdirP(gradleDir); + fs.writeFileSync(`${gradleDir}.complete`, 'hello'); + // This will throw if it doesn't find it in the cache (because no such version exists) + await installer.getGradle('250', 'path shouldnt matter, found in cache'); + return; + }); + + it('Doesnt use version of gradle that was only partially installed in cache', async () => { + const gradleDir: string = path.join(toolDir, 'gradle', '251.0.0', 'x64'); + await io.mkdirP(gradleDir); + let thrown = false; + try { + // This will throw if it doesn't find it in the cache (because no such version exists) + await installer.getGradle('251', 'bad path'); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + return; + }); +}); diff --git a/__tests__/maven-installer.test.ts b/__tests__/maven-installer.test.ts new file mode 100644 index 00000000..ed2ed438 --- /dev/null +++ b/__tests__/maven-installer.test.ts @@ -0,0 +1,134 @@ +import io = require('@actions/io'); +import fs = require('fs'); +import path = require('path'); +import child_process = require('child_process'); + +const toolDir = path.join(__dirname, 'runnerm', 'tools'); +const tempDir = path.join(__dirname, 'runnerm', 'temp'); +const mavenDir = path.join(__dirname, 'runnerm', 'maven'); + +process.env['RUNNER_TOOL_CACHE'] = toolDir; +process.env['RUNNER_TEMP'] = tempDir; +import * as installer from '../src/maven-installer'; + +let mavenFilePath = ''; +let mavenUrl = ''; +if (process.platform === 'win32') { + mavenFilePath = path.join(mavenDir, 'maven_win.zip'); + mavenUrl = + 'https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.zip'; +} else if (process.platform === 'darwin') { + mavenFilePath = path.join(mavenDir, 'maven_mac.tar.gz'); + mavenUrl = + 'https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz'; +} else { + mavenFilePath = path.join(mavenDir, 'maven_linux.tar.gz'); + mavenUrl = + 'https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz'; +} + +describe('maven installer tests', () => { + beforeAll(async () => { + await io.rmRF(toolDir); + await io.rmRF(tempDir); + await io.rmRF(mavenDir); + if (!fs.existsSync(`${mavenFilePath}.complete`)) { + // Download maven + await io.mkdirP(mavenDir); + + console.log('Downloading maven'); + child_process.execSync(`curl "${mavenUrl}" > "${mavenFilePath}"`); + // Write complete file so we know it was successful + fs.writeFileSync(`${mavenFilePath}.complete`, 'content'); + } + }, 300000); + + afterAll(async () => { + try { + await io.rmRF(toolDir); + await io.rmRF(tempDir); + await io.rmRF(mavenDir); + } catch { + console.log('Failed to remove test directories'); + } + }, 100000); + + it('Installs version of Maven from maven-file if no matching version is installed', async () => { + await installer.getMaven( + '3.6.3', + mavenFilePath, + 'https://archive.apache.org/dist/maven/maven-3/' + ); + const mavenDir = path.join(toolDir, 'maven', '3.6.3', 'x64'); + + expect(fs.existsSync(`${mavenDir}.complete`)).toBe(true); + expect(fs.existsSync(path.join(mavenDir, 'bin'))).toBe(true); + }, 100000); + + it('Throws if invalid directory to maven', async () => { + let thrown = false; + try { + await installer.getMaven('1000', 'bad path'); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + }); + + it('Downloads maven if no file given', async () => { + await installer.getMaven('3.6.2', ''); + const mavenDir = path.join(toolDir, 'maven', '3.6.2', 'x64'); + + expect(fs.existsSync(`${mavenDir}.complete`)).toBe(true); + expect(fs.existsSync(path.join(mavenDir, 'bin'))).toBe(true); + }, 100000); + + it('Downloads maven with 1.x syntax', async () => { + await installer.getMaven('3.1', ''); + const mavenDir = path.join(toolDir, 'maven', '3.1.1', 'x64'); + + expect(fs.existsSync(`${mavenDir}.complete`)).toBe(true); + expect(fs.existsSync(path.join(mavenDir, 'bin'))).toBe(true); + }, 100000); + + it('Downloads maven with normal semver syntax', async () => { + await installer.getMaven('3.5.x', ''); + const mavenDir = path.join(toolDir, 'maven', '3.5.4', 'x64'); + + expect(fs.existsSync(`${mavenDir}.complete`)).toBe(true); + expect(fs.existsSync(path.join(mavenDir, 'bin'))).toBe(true); + }, 100000); + + it('Throws if invalid directory to maven', async () => { + let thrown = false; + try { + await installer.getMaven('1000', 'bad path'); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + }); + + it('Uses version of Maven installed in cache', async () => { + const mavenDir: string = path.join(toolDir, 'maven', '250.0.0', 'x64'); + await io.mkdirP(mavenDir); + fs.writeFileSync(`${mavenDir}.complete`, 'hello'); + // This will throw if it doesn't find it in the cache (because no such version exists) + await installer.getMaven('250', 'path shouldnt matter, found in cache'); + return; + }); + + it('Doesnt use version of Maven that was only partially installed in cache', async () => { + const mavenDir: string = path.join(toolDir, 'maven', '251.0.0', 'x64'); + await io.mkdirP(mavenDir); + let thrown = false; + try { + // This will throw if it doesn't find it in the cache (because no such version exists) + await installer.getMaven('251', 'bad path'); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + return; + }); +}); diff --git a/action.yml b/action.yml index 6337613f..1995940a 100644 --- a/action.yml +++ b/action.yml @@ -34,6 +34,26 @@ inputs: settings-path: description: 'Path to where the settings.xml file will be written. Default is ~/.m2.' required: false + maven-version: + description: 'The Maven version to make available on the path. Takes a whole + or semver Maven version, or 3.x syntax (e.g. 3.6 => Maven 3.x)' + required: false + maven-file: + description: 'Path to where the compressed Maven is located. The path could + be in your source repository or a local path on the agent.' + required: false + maven-mirror: + description: 'Uri hosting Maven3 mirror packages.' + required: false + default: 'https://archive.apache.org/dist/maven/maven-3/' + gradle-version: + description: 'The Gradle version to make available on the path. Takes a whole + or semver Gradle version, or 6.x syntax (e.g. 6.0 => Gradle 6.x)' + required: false + gradle-file: + description: 'Path to where the compressed Gradle is located. The path could + be in your source repository or a local path on the agent.' + required: false runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 9e0ab8c4a683198bf62b21e336ddaff5c7e23913..1db34ab1d2f98e3119a532c0efb237d0ce8e5ced 100644 GIT binary patch delta 30449 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 UGovh@b)Add!a8DP?;=nAR~cB!cmMzZ delta 13333 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 diff --git a/package-lock.json b/package-lock.json index d0a41b10..0564ca19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,31 +5,48 @@ "requires": true, "dependencies": { "@actions/core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.0.0.tgz", - "integrity": "sha512-aMIlkx96XH4E/2YZtEOeyrYQfhlas9jIRkfGPqMwXD095Rdkzo4lB6ZmbxPQSzD+e1M+Xsm98ZhuSMYGv/AlqA==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.1.tgz", + "integrity": "sha512-xD+CQd9p4lU7ZfRqmUcbJpqR+Ss51rJRVeXMyOLrZQImN9/8Sy/BEUBnHO/UKD3z03R686PVTLfEPmkropGuLw==" }, "@actions/exec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.0.tgz", "integrity": "sha512-nquH0+XKng+Ll7rZfCojN7NWSbnGh+ltwUJhzfbLkmOJgxocGX2/yXcZLMyT9fa7+tByEow/NSTrBExNlEj9fw==" }, + "@actions/http-client": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.1.tgz", + "integrity": "sha512-vy5DhqTJ1gtEkpRrD/6BHhUlkeyccrOX0BT9KmtO5TWxe5KSSwVHFE+J15Z0dG+tJwZJ/nHC4slUIyqpkahoMg==" + }, "@actions/io": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.0.tgz", - "integrity": "sha512-ezrJSRdqtXtdx1WXlfYL85+40F7gB39jCK9P0jZVODW3W6xUYmu6ZOEc/UmmElUwhRyDRm1R4yNZu1Joq2kuQg==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", + "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==" }, "@actions/tool-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-1.0.0.tgz", - "integrity": "sha512-l3zT0IfDfi5Ik5aMpnXqGHGATxN8xa9ls4ue+X/CBXpPhRMRZS4vcuh5Q9T98WAGbkysRCfhpbksTPHIcKnNwQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-1.3.0.tgz", + "integrity": "sha512-pbv32I89niDShw1YTDo0OFQmWPkZPElE7e3So1jfEzjIyzGRfYIzshwOVhemJZLcDtzo3kxO3GFDAmuVvub/6w==", "requires": { - "@actions/core": "^1.0.0", + "@actions/core": "^1.2.0", "@actions/exec": "^1.0.0", - "@actions/io": "^1.0.0", + "@actions/http-client": "^1.0.1", + "@actions/io": "^1.0.1", "semver": "^6.1.0", - "typed-rest-client": "^1.4.0", "uuid": "^3.3.2" + }, + "dependencies": { + "@actions/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.1.tgz", + "integrity": "sha512-xD+CQd9p4lU7ZfRqmUcbJpqR+Ss51rJRVeXMyOLrZQImN9/8Sy/BEUBnHO/UKD3z03R686PVTLfEPmkropGuLw==" + }, + "@actions/io": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", + "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==" + } } }, "@babel/code-frame": { @@ -1711,7 +1728,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -1732,12 +1750,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1752,17 +1772,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -1879,7 +1902,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -1891,6 +1915,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1905,6 +1930,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1912,12 +1938,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -1936,6 +1964,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2016,7 +2045,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2028,6 +2058,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2113,7 +2144,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2149,6 +2181,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2168,6 +2201,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2211,12 +2245,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index 00922b49..28aa2624 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,10 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@actions/core": "^1.0.0", + "@actions/core": "^1.2.1", "@actions/exec": "^1.0.0", - "@actions/io": "^1.0.0", - "@actions/tool-cache": "^1.0.0", + "@actions/io": "^1.0.2", + "@actions/tool-cache": "^1.3.0", "semver": "^6.1.1", "typed-rest-client": "1.5.0" }, diff --git a/src/gradle-installer.ts b/src/gradle-installer.ts new file mode 100644 index 00000000..e6739898 --- /dev/null +++ b/src/gradle-installer.ts @@ -0,0 +1,204 @@ +let tempDirectory = process.env['RUNNER_TEMP'] || ''; + +import * as core from '@actions/core'; +import * as io from '@actions/io'; +import * as tc from '@actions/tool-cache'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as semver from 'semver'; +import * as httpm from 'typed-rest-client/HttpClient'; + +const IS_WINDOWS = process.platform === 'win32'; + +if (!tempDirectory) { + let baseLocation; + if (IS_WINDOWS) { + // On windows use the USERPROFILE env variable + baseLocation = process.env['USERPROFILE'] || 'C:\\'; + } else { + if (process.platform === 'darwin') { + baseLocation = '/Users'; + } else { + baseLocation = '/home'; + } + } + tempDirectory = path.join(baseLocation, 'actions', 'temp'); +} + +export async function getGradle( + version: string, + gradleFile: string, + gradleMirror: string = 'https://services.gradle.org/distributions/' +): Promise { + const toolName = 'gradle'; + let toolPath = tc.find(toolName, version); + + if (toolPath) { + core.debug(`Tool found in cache ${toolPath}`); + } else { + let compressedFileExtension = ''; + if (!gradleFile) { + core.debug('Downloading Gradle from gradle.org'); + let http: httpm.HttpClient = new httpm.HttpClient('spring-build-action'); + let contents = await (await http.get(gradleMirror)).readBody(); + let refs: string[] = []; + const regex = /gradle-([\d\.]+)-bin\.zip<\/span>/g; + let match = regex.exec(contents); + while (match != null) { + refs.push(match[1]); + match = regex.exec(contents); + } + core.debug(`Found refs ${refs}`); + + const downloadInfo = getDownloadInfo(refs, version, gradleMirror); + + gradleFile = await tc.downloadTool(downloadInfo.url); + version = downloadInfo.version; + compressedFileExtension = '.zip'; + } else { + core.debug('Retrieving Gradle from local path'); + } + compressedFileExtension = + compressedFileExtension || getFileEnding(gradleFile); + let tempDir: string = path.join( + tempDirectory, + 'temp_' + Math.floor(Math.random() * 2000000000) + ); + const gradleDir = await unzipGradleDownload( + gradleFile, + compressedFileExtension, + tempDir + ); + core.debug(`gradle extracted to ${gradleDir}`); + toolPath = await tc.cacheDir( + gradleDir, + toolName, + getCacheVersionString(version) + ); + } + + core.exportVariable('GRADLE_HOME', toolPath); + core.addPath(path.join(toolPath, 'bin')); +} + +function getCacheVersionString(version: string) { + const versionArray = version.split('.'); + const major = versionArray[0]; + const minor = versionArray.length > 1 ? versionArray[1] : '0'; + const patch = versionArray.length > 2 ? versionArray[2] : '0'; + return `${major}.${minor}.${patch}`; +} + +function getFileEnding(file: string): string { + let fileEnding = ''; + + if (file.endsWith('.zip')) { + fileEnding = '.zip'; + } else { + throw new Error(`${file} has an unsupported file extension`); + } + + return fileEnding; +} + +async function extractFiles( + file: string, + fileEnding: string, + destinationFolder: string +): Promise { + const stats = fs.statSync(file); + if (!stats) { + throw new Error(`Failed to extract ${file} - it doesn't exist`); + } else if (stats.isDirectory()) { + throw new Error(`Failed to extract ${file} - it is a directory`); + } + + if ('.zip' === fileEnding) { + await tc.extractZip(file, destinationFolder); + } else { + throw new Error(`Failed to extract ${file} - only .zip supported`); + } +} + +async function unzipGradleDownload( + repoRoot: string, + fileEnding: string, + destinationFolder: string +): Promise { + // Create the destination folder if it doesn't exist + await io.mkdirP(destinationFolder); + + const gradleFile = path.normalize(repoRoot); + const stats = fs.statSync(gradleFile); + if (stats.isFile()) { + await extractFiles(path.resolve(gradleFile), fileEnding, destinationFolder); + const gradleDirectory = path.join( + destinationFolder, + fs.readdirSync(destinationFolder)[0] + ); + return gradleDirectory; + } else { + throw new Error(`Gradle argument ${gradleFile} is not a file`); + } +} + +function getDownloadInfo( + refs: string[], + version: string, + gradleMirror: string +): {version: string; url: string} { + version = normalizeVersion(version); + const extension = '.zip'; + + // Maps version to url + let versionMap = new Map(); + + // Filter by platform + refs.forEach(ref => { + if (semver.satisfies(ref, version)) { + core.debug(`VersionMap add ${ref} ${version}`); + versionMap.set(ref, `${gradleMirror}gradle-${ref}-bin${extension}`); + } + }); + + // Choose the most recent satisfying version + let curVersion = '0.0.0'; + let curUrl = ''; + for (const entry of versionMap.entries()) { + const entryVersion = entry[0]; + const entryUrl = entry[1]; + core.debug(`VersionMap Entry ${entryVersion} ${entryUrl}`); + if (semver.gt(entryVersion, curVersion)) { + core.debug(`VersionMap semver gt ${entryVersion} ${entryUrl}`); + curUrl = entryUrl; + curVersion = entryVersion; + } + } + + if (curUrl == '') { + throw new Error( + `No valid download found for version ${version}. Check ${gradleMirror} for a list of valid versions or download your own gradle file and add the gradleFile argument` + ); + } + + return {version: curVersion, url: curUrl}; +} + +function normalizeVersion(version: string): string { + if (version.slice(0, 2) === '1.') { + // Trim leading 1. for versions like 1.8 + version = version.slice(2); + if (!version) { + throw new Error('1. is not a valid version'); + } + } + + if (version.split('.').length < 3) { + // Add trailing .x if it is missing + if (version[version.length - 1] != 'x') { + version = version + '.x'; + } + } + + return version; +} diff --git a/src/installer.ts b/src/installer.ts index ab4f466b..9b97c0ad 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -118,12 +118,12 @@ async function extractFiles( } if ('.tar' === fileEnding || '.tar.gz' === fileEnding) { - await tc.extractTar(file, destinationFolder); + await tc.extractTar(path.resolve(file), destinationFolder); } else if ('.zip' === fileEnding) { - await tc.extractZip(file, destinationFolder); + await tc.extractZip(path.resolve(file), destinationFolder); } else { // fall through and use sevenZip - await tc.extract7z(file, destinationFolder); + await tc.extract7z(path.resolve(file), destinationFolder); } } diff --git a/src/maven-installer.ts b/src/maven-installer.ts new file mode 100644 index 00000000..b0a60fac --- /dev/null +++ b/src/maven-installer.ts @@ -0,0 +1,218 @@ +let tempDirectory = process.env['RUNNER_TEMP'] || ''; + +import * as core from '@actions/core'; +import * as io from '@actions/io'; +import * as tc from '@actions/tool-cache'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as semver from 'semver'; +import * as httpm from 'typed-rest-client/HttpClient'; + +const IS_WINDOWS = process.platform === 'win32'; + +if (!tempDirectory) { + let baseLocation; + if (IS_WINDOWS) { + // On windows use the USERPROFILE env variable + baseLocation = process.env['USERPROFILE'] || 'C:\\'; + } else { + if (process.platform === 'darwin') { + baseLocation = '/Users'; + } else { + baseLocation = '/home'; + } + } + tempDirectory = path.join(baseLocation, 'actions', 'temp'); +} + +export async function getMaven( + version: string, + mavenFile: string, + mavenMirror: string = 'https://archive.apache.org/dist/maven/maven-3/' +): Promise { + const toolName = 'maven'; + let toolPath = tc.find(toolName, version); + + if (toolPath) { + core.debug(`Tool found in cache ${toolPath}`); + } else { + let compressedFileExtension = ''; + if (!mavenFile) { + core.debug('Downloading Maven from Apache Mirror'); + let http: httpm.HttpClient = new httpm.HttpClient('spring-build-action'); + let contents = await (await http.get(mavenMirror)).readBody(); + let refs: string[] = []; + const regex = /([\d\.]+)\/<\/a>/g; + let match = regex.exec(contents); + while (match != null) { + refs.push(match[1]); + match = regex.exec(contents); + } + core.debug(`Found refs ${refs}`); + + const downloadInfo = getDownloadInfo(refs, version, mavenMirror); + + mavenFile = await tc.downloadTool(downloadInfo.url); + version = downloadInfo.version; + compressedFileExtension = IS_WINDOWS ? '.zip' : '.tar.gz'; + } else { + core.debug('Retrieving Maven from local path'); + } + compressedFileExtension = + compressedFileExtension || getFileEnding(mavenFile); + let tempDir: string = path.join( + tempDirectory, + 'temp_' + Math.floor(Math.random() * 2000000000) + ); + const mavenDir = await unzipMavenDownload( + mavenFile, + compressedFileExtension, + tempDir + ); + core.debug(`maven extracted to ${mavenDir}`); + toolPath = await tc.cacheDir( + mavenDir, + toolName, + getCacheVersionString(version) + ); + } + + core.exportVariable('M2_HOME', toolPath); + core.addPath(path.join(toolPath, 'bin')); +} + +function getCacheVersionString(version: string) { + const versionArray = version.split('.'); + const major = versionArray[0]; + const minor = versionArray.length > 1 ? versionArray[1] : '0'; + const patch = versionArray.length > 2 ? versionArray[2] : '0'; + return `${major}.${minor}.${patch}`; +} + +function getFileEnding(file: string): string { + let fileEnding = ''; + + if (file.endsWith('.tar.gz')) { + fileEnding = '.tar.gz'; + } else if (file.endsWith('.zip')) { + fileEnding = '.zip'; + } else { + throw new Error(`${file} has an unsupported file extension`); + } + + return fileEnding; +} + +async function extractFiles( + file: string, + fileEnding: string, + destinationFolder: string +): Promise { + const stats = fs.statSync(file); + if (!stats) { + throw new Error(`Failed to extract ${file} - it doesn't exist`); + } else if (stats.isDirectory()) { + throw new Error(`Failed to extract ${file} - it is a directory`); + } + + if ('.tar.gz' === fileEnding) { + await tc.extractTar(file, destinationFolder); + } else if ('.zip' === fileEnding) { + await tc.extractZip(file, destinationFolder); + } else { + throw new Error( + `Failed to extract ${file} - only .zip or .tar.gz supported` + ); + } +} + +async function unzipMavenDownload( + repoRoot: string, + fileEnding: string, + destinationFolder: string +): Promise { + // Create the destination folder if it doesn't exist + await io.mkdirP(destinationFolder); + + const mavenFile = path.normalize(repoRoot); + const stats = fs.statSync(mavenFile); + if (stats.isFile()) { + await extractFiles(path.resolve(mavenFile), fileEnding, destinationFolder); + const mavenDirectory = path.join( + destinationFolder, + fs.readdirSync(destinationFolder)[0] + ); + return mavenDirectory; + } else { + throw new Error(`Maven argument ${mavenFile} is not a file`); + } +} + +function getDownloadInfo( + refs: string[], + version: string, + mavenMirror: string +): {version: string; url: string} { + version = normalizeVersion(version); + let extension = ''; + if (IS_WINDOWS) { + extension = `.zip`; + } else { + extension = `.tar.gz`; + } + + // Maps version to url + let versionMap = new Map(); + + // Filter by platform + refs.forEach(ref => { + if (semver.satisfies(ref, version)) { + core.debug(`VersionMap add ${ref} ${version}`); + versionMap.set( + ref, + `${mavenMirror}${ref}/binaries/apache-maven-${ref}-bin${extension}` + ); + } + }); + + // Choose the most recent satisfying version + let curVersion = '0.0.0'; + let curUrl = ''; + for (const entry of versionMap.entries()) { + const entryVersion = entry[0]; + const entryUrl = entry[1]; + core.debug(`VersionMap Entry ${entryVersion} ${entryUrl}`); + if (semver.gt(entryVersion, curVersion)) { + core.debug(`VersionMap semver gt ${entryVersion} ${entryUrl}`); + curUrl = entryUrl; + curVersion = entryVersion; + } + } + + if (curUrl == '') { + throw new Error( + `No valid download found for version ${version}. Check ${mavenMirror} for a list of valid versions or download your own maven file and add the mavenFile argument` + ); + } + + return {version: curVersion, url: curUrl}; +} + +function normalizeVersion(version: string): string { + if (version.slice(0, 2) === '1.') { + // Trim leading 1. for versions like 1.8 + version = version.slice(2); + if (!version) { + throw new Error('1. is not a valid version'); + } + } + + if (version.split('.').length < 3) { + // Add trailing .x if it is missing + if (version[version.length - 1] != 'x') { + version = version + '.x'; + } + } + + return version; +} diff --git a/src/setup-java.ts b/src/setup-java.ts index d0392175..d9cbb071 100644 --- a/src/setup-java.ts +++ b/src/setup-java.ts @@ -1,5 +1,7 @@ import * as core from '@actions/core'; import * as installer from './installer'; +import * as mavenInstaller from './maven-installer'; +import * as gradleInstaller from './gradle-installer'; import * as auth from './auth'; import * as path from 'path'; @@ -15,6 +17,19 @@ async function run() { await installer.getJava(version, arch, jdkFile, javaPackage); + const mavenVersion = core.getInput('maven-version', {required: false}); + const mavenFile = core.getInput('maven-file', {required: false}) || ''; + const mavenMirror = core.getInput('maven-mirror', {required: false}); + if (mavenVersion) { + await mavenInstaller.getMaven(mavenVersion, mavenFile, mavenMirror); + } + + const gradleVersion = core.getInput('gradle-version', {required: false}); + const gradleFile = core.getInput('gradle-file', {required: false}) || ''; + if (gradleVersion) { + await gradleInstaller.getGradle(gradleVersion, gradleFile); + } + const matchersPath = path.join(__dirname, '..', '.github'); console.log(`##[add-matcher]${path.join(matchersPath, 'java.json')}`);