diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f787af87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +dist/index.js -diff -merge +dist/index.js linguist-generated=true diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 4558c6fb..59371d4f 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,5 +1,5 @@ name: Main workflow -on: [push] +on: [push, pull_request] jobs: run: name: Run diff --git a/.gitignore b/.gitignore index 370c1f6e..e6dc0cc5 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,4 @@ typings/ # DynamoDB Local files .dynamodb/ +.vscode/ diff --git a/README.md b/README.md index 568fd1be..05b42909 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This action sets up a java environment for use in actions by: See [action.yml](action.yml) -Basic: +## Basic ```yaml steps: - uses: actions/checkout@v1 @@ -25,7 +25,7 @@ steps: - run: java -cp java HelloWorldApp ``` -From local file: +## Local file ```yaml steps: - uses: actions/checkout@v1 @@ -37,7 +37,7 @@ steps: - run: java -cp java HelloWorldApp ``` -Matrix Testing: +## Matrix Testing ```yaml jobs: build: @@ -45,7 +45,7 @@ jobs: strategy: matrix: # test against latest update of each major Java version, as well as specific updates of LTS versions: - java: [ 1.6, 6.0.83, 7, 7.0.181, 8, 8.0.192, 9.0,x, 10, 11.0.x, 11.0.3, 12, 13 ] + java: [ 1.6, 6.0.83, 7, 7.0.181, 8, 8.0.192, 9.0.x, 10, 11.0.x, 11.0.3, 12, 13 ] name: Java ${{ matrix.java }} sample steps: - uses: actions/checkout@master @@ -56,6 +56,126 @@ jobs: - run: java -cp java HelloWorldApp ``` +## Publishing using Apache Maven +```yaml +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Publish to GitHub Packages Apache Maven + run: mvn deploy + env: + GITHUB_TOKEN: ${{ github.token }} # GITHUB_TOKEN is the default env for the password + + - name: Set up Apache Maven Central + uses: actions/setup-java@v1 + with: # running setup-java again overwrites the settings.xml + java-version: 1.8 + server-id: maven # Value of the distributionManagement/repository/id field of the pom.xml + server-username: MAVEN_USERNAME # env variable for username in deploy + server-password: MAVEN_CENTRAL_TOKEN # env variable for token in deploy + + - name: Publish to Apache Maven Central + run: mvn deploy + env: + MAVEN_USERNAME: maven_username123 + MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} +``` + +The two `settings.xml` files created from the above example look like the following. + +`settings.xml` file created for the first deploy to GitHub Packages +```xml + + + github + ${env.GITHUB_ACTOR} + ${env.GITHUB_TOKEN} + + +``` + +`settings.xml` file created for the second deploy to Apache Maven Central +```xml + + + maven + ${env.MAVEN_USERNAME} + ${env.MAVEN_CENTRAL_TOKEN} + + +``` + +***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. + +## Publishing using Gradle +```yaml +jobs: + + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + + - name: Build with Gradle + run: gradle build + + - name: Publish to GitHub Packages + run: gradle publish + env: + USERNAME: ${{ github.actor }} + 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`.*** + +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. + +## Apache Maven with a settings path + +When using an Actions self-hosted runner with multiple shared runners the default `$HOME` directory can be shared by a number runners at the same time which could overwrite existing settings file. Setting the `settings-path` variable allows you to choose a unique location for your settings file. + +```yaml +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 1.8 for Shared Runner + uses: actions/setup-java@v1 + with: + java-version: 1.8 + server-id: github # Value of the distributionManagement/repository/id field of the pom.xml + settings-path: ${{ github.workspace }} # location for the settings.xml file + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Publish to GitHub Packages Apache Maven + run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml + env: + GITHUB_TOKEN: ${{ github.token }} +``` + # License The scripts and documentation in this project are released under the [MIT License](LICENSE) diff --git a/__tests__/auth.test.ts b/__tests__/auth.test.ts new file mode 100644 index 00000000..13509682 --- /dev/null +++ b/__tests__/auth.test.ts @@ -0,0 +1,144 @@ +import io = require('@actions/io'); +import fs = require('fs'); +import os = require('os'); +import path = require('path'); + +// make the os.homedir() call be local to the tests +jest.doMock('os', () => { + return { + homedir: jest.fn(() => __dirname) + }; +}); + +import * as auth from '../src/auth'; + +const m2Dir = path.join(__dirname, auth.M2_DIR); +const settingsFile = path.join(m2Dir, auth.SETTINGS_FILE); + +describe('auth tests', () => { + beforeEach(async () => { + await io.rmRF(m2Dir); + }, 300000); + + afterAll(async () => { + try { + await io.rmRF(m2Dir); + } catch { + console.log('Failed to remove test directories'); + } + }, 100000); + + it('creates settings.xml in alternate locations', async () => { + const id = 'packages'; + const username = 'UNAMI'; + const password = 'TOLKIEN'; + + const altHome = path.join(__dirname, 'runner', 'settings'); + const altSettingsFile = path.join(altHome, auth.SETTINGS_FILE); + process.env[`INPUT_SETTINGS-PATH`] = altHome; + await io.rmRF(altHome); // ensure it doesn't already exist + + await auth.configAuthentication(id, username, password); + + expect(fs.existsSync(m2Dir)).toBe(false); + expect(fs.existsSync(settingsFile)).toBe(false); + + expect(fs.existsSync(altHome)).toBe(true); + expect(fs.existsSync(altSettingsFile)).toBe(true); + expect(fs.readFileSync(altSettingsFile, 'utf-8')).toEqual( + auth.generate(id, username, password) + ); + + delete process.env[`INPUT_SETTINGS-PATH`]; + await io.rmRF(altHome); + }, 100000); + + it('creates settings.xml with username and password', async () => { + const id = 'packages'; + const username = 'UNAME'; + const password = 'TOKEN'; + + await auth.configAuthentication(id, username, password); + + expect(fs.existsSync(m2Dir)).toBe(true); + expect(fs.existsSync(settingsFile)).toBe(true); + expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( + auth.generate(id, username, password) + ); + }, 100000); + + it('overwrites existing settings.xml files', async () => { + const id = 'packages'; + const username = 'USERNAME'; + const password = 'PASSWORD'; + + fs.mkdirSync(m2Dir, {recursive: true}); + fs.writeFileSync(settingsFile, 'FAKE FILE'); + expect(fs.existsSync(m2Dir)).toBe(true); + expect(fs.existsSync(settingsFile)).toBe(true); + + await auth.configAuthentication(id, username, password); + + expect(fs.existsSync(m2Dir)).toBe(true); + expect(fs.existsSync(settingsFile)).toBe(true); + expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( + auth.generate(id, username, password) + ); + }, 100000); + + it('does not create settings.xml without required parameters', async () => { + await auth.configAuthentication('FOO'); + + expect(fs.existsSync(m2Dir)).toBe(true); + expect(fs.existsSync(settingsFile)).toBe(true); + expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( + auth.generate('FOO', auth.DEFAULT_USERNAME, auth.DEFAULT_PASSWORD) + ); + + await auth.configAuthentication(undefined, 'BAR', undefined); + + expect(fs.existsSync(m2Dir)).toBe(true); + expect(fs.existsSync(settingsFile)).toBe(true); + expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( + auth.generate(auth.DEFAULT_ID, 'BAR', auth.DEFAULT_PASSWORD) + ); + + await auth.configAuthentication(undefined, undefined, 'BAZ'); + + expect(fs.existsSync(m2Dir)).toBe(true); + expect(fs.existsSync(settingsFile)).toBe(true); + expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( + auth.generate(auth.DEFAULT_ID, auth.DEFAULT_USERNAME, 'BAZ') + ); + + await auth.configAuthentication(); + + expect(fs.existsSync(m2Dir)).toBe(true); + expect(fs.existsSync(settingsFile)).toBe(true); + expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( + auth.generate( + auth.DEFAULT_ID, + auth.DEFAULT_USERNAME, + auth.DEFAULT_PASSWORD + ) + ); + }, 100000); + + it('escapes invalid XML inputs', () => { + const id = 'packages'; + const username = 'USER'; + const password = '&<>"\'\'"><&'; + + expect(auth.generate(id, username, password)).toEqual(` + + + + ${id} + \${env.${username}} + \${env.&<>"''"><&} + + + + `); + }); +}); diff --git a/action.yml b/action.yml index 2bcf1ff4..6337613f 100644 --- a/action.yml +++ b/action.yml @@ -1,9 +1,11 @@ name: 'Setup Java JDK' -description: 'Set up a specific version of the Java JDK and add the command-line tools to the PATH' +description: 'Set up a specific version of the Java JDK and add the + command-line tools to the PATH' author: 'GitHub' -inputs: +inputs: java-version: - description: 'The Java version to make available on the path. Takes a whole or semver Java version, or 1.x syntax (e.g. 1.8 => Java 8.x)' + description: 'The Java version to make available on the path. Takes a whole + or semver Java version, or 1.x syntax (e.g. 1.8 => Java 8.x)' required: true java-package: description: 'The package type (jre, jdk, jdk+fx)' @@ -14,7 +16,23 @@ inputs: required: false default: 'x64' jdkFile: - description: 'Path to where the compressed JDK is located. The path could be in your source repository or a local path on the agent.' + description: 'Path to where the compressed JDK is located. The path could + be in your source repository or a local path on the agent.' + required: false + server-id: + description: 'ID of the distributionManagement repository in the pom.xml + file. Default is `github`' + required: false + server-username: + description: 'Environment variable name for the username for authentication + to the Apache Maven repository. Default is $GITHUB_ACTOR' + required: false + server-password: + description: 'Environment variable name for password or token for + authentication to the Apache Maven repository. Default is $GITHUB_TOKEN' + required: false + settings-path: + description: 'Path to where the settings.xml file will be written. Default is ~/.m2.' required: false runs: using: 'node12' diff --git a/dist/index.js b/dist/index.js index 2ec6b325..9e0ab8c4 100644 Binary files a/dist/index.js and b/dist/index.js differ diff --git a/dist/unzip b/dist/unzip new file mode 100644 index 00000000..40824180 Binary files /dev/null and b/dist/unzip differ diff --git a/package-lock.json b/package-lock.json index f33f89c1..d0a41b10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2289,9 +2289,9 @@ "dev": true }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", diff --git a/package.json b/package.json index aa088e8f..00922b49 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "description": "setup java action", "main": "dist/index.js", "scripts": { - "build": "tsc", + "build": "ncc build src/setup-java.ts", "format": "prettier --write **/*.ts", "format-check": "prettier --check **/*.ts", - "release": "ncc build && git add -f dist/", + "prerelease": "npm run-script build", + "release": "git add -f dist/index.js", "test": "jest" }, "repository": { diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 00000000..2e7c6e8e --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,74 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as core from '@actions/core'; +import * as io from '@actions/io'; + +export const M2_DIR = '.m2'; +export const SETTINGS_FILE = 'settings.xml'; + +export const DEFAULT_ID = 'github'; +export const DEFAULT_USERNAME = 'GITHUB_ACTOR'; +export const DEFAULT_PASSWORD = 'GITHUB_TOKEN'; + +export async function configAuthentication( + id = DEFAULT_ID, + username = DEFAULT_USERNAME, + password = DEFAULT_PASSWORD +) { + console.log( + `creating ${SETTINGS_FILE} with server-id: ${id};`, + `environment variables: username=\$${username} and password=\$${password}` + ); + // when an alternate m2 location is specified use only that location (no .m2 directory) + // otherwise use the home/.m2/ path + const directory: string = path.join( + core.getInput('settings-path') || os.homedir(), + core.getInput('settings-path') ? '' : M2_DIR + ); + await io.mkdirP(directory); + core.debug(`created directory ${directory}`); + await write(directory, generate(id, username, password)); +} + +function escapeXML(value: string) { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +// only exported for testing purposes +export function generate( + id = DEFAULT_ID, + username = DEFAULT_USERNAME, + password = DEFAULT_PASSWORD +) { + return ` + + + + ${escapeXML(id)} + \${env.${escapeXML(username)}} + \${env.${escapeXML(password)}} + + + + `; +} + +async function write(directory: string, settings: string) { + const location = path.join(directory, SETTINGS_FILE); + if (fs.existsSync(location)) { + console.warn(`overwriting existing file ${location}`); + } else { + console.log(`writing ${location}`); + } + + return fs.writeFileSync(location, settings, { + encoding: 'utf-8', + flag: 'w' + }); +} diff --git a/src/installer.ts b/src/installer.ts index f05171fb..ab4f466b 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -266,8 +266,17 @@ function normalizeVersion(version: string): string { } } - // Add trailing .x if it is missing - if (version.split('.').length != 3) { + if (version.endsWith('-ea')) { + // convert e.g. 14-ea to 14.0.0-ea + if (version.indexOf('.') == -1) { + version = version.slice(0, version.length - 3) + '.0.0-ea'; + } + // match anything in -ea.X (semver won't do .x matching on pre-release versions) + if (version[0] >= '0' && version[0] <= '9') { + version = '>=' + version; + } + } else if (version.split('.').length < 3) { + // For non-ea versions, add trailing .x if it is missing if (version[version.length - 1] != 'x') { version = version + '.x'; } diff --git a/src/setup-java.ts b/src/setup-java.ts index 1d26bffe..d0392175 100644 --- a/src/setup-java.ts +++ b/src/setup-java.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core'; import * as installer from './installer'; +import * as auth from './auth'; import * as path from 'path'; async function run() { @@ -16,6 +17,14 @@ async function run() { const matchersPath = path.join(__dirname, '..', '.github'); console.log(`##[add-matcher]${path.join(matchersPath, 'java.json')}`); + + const id = core.getInput('server-id', {required: false}) || undefined; + const username = + core.getInput('server-username', {required: false}) || undefined; + const password = + core.getInput('server-password', {required: false}) || undefined; + + await auth.configAuthentication(id, username, password); } catch (error) { core.setFailed(error.message); }