diff --git a/README.md b/README.md index c7f492b3..3ffbcc3b 100644 --- a/README.md +++ b/README.md @@ -29,27 +29,27 @@ Examples of version specifications that the java-version parameter will accept: - A major Java version e.g. ```6, 7, 8, 9, 10, 11, 12, 13, ...``` - + - A semver Java version specification e.g. ```8.0.232, 7.0.181, 11.0.4``` - + e.g. ```8.0.x, >11.0.3, >=13.0.1, <8.0.212``` - + - An early access (EA) Java version e.g. ```14-ea, 15-ea``` - + e.g. ```14.0.0-ea, 15.0.0-ea``` - + e.g. ```14.0.0-ea.28, 15.0.0-ea.2``` (syntax for specifying an EA build number) - + Note that, per semver rules, EA builds will be matched by explicit EA version specifications. - + - 1.x syntax e.g. ```1.8``` (same as ```8```) - + e.g. ```1.8.0.212``` (same as ```8.0.212```) @@ -113,12 +113,14 @@ jobs: 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 + gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} # Value of the GPG private key to import - name: Publish to Apache Maven Central - run: mvn deploy + run: mvn deploy env: MAVEN_USERNAME: maven_username123 MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} ``` The two `settings.xml` files created from the above example look like the following. @@ -131,6 +133,16 @@ The two `settings.xml` files created from the above example look like the follow ${env.GITHUB_ACTOR} ${env.GITHUB_TOKEN} + + + + true + + + ${env.GPG_PASSPHRASE} + + + ``` @@ -142,10 +154,20 @@ The two `settings.xml` files created from the above example look like the follow ${env.MAVEN_USERNAME} ${env.MAVEN_CENTRAL_TOKEN} + + + + true + + + ${env.MAVEN_GPG_PASSPHRASE} + + + ``` -***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. @@ -172,7 +194,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__/auth.test.ts b/__tests__/auth.test.ts index 13509682..54ffa4d3 100644 --- a/__tests__/auth.test.ts +++ b/__tests__/auth.test.ts @@ -2,6 +2,7 @@ import io = require('@actions/io'); import fs = require('fs'); import os = require('os'); import path = require('path'); +import exec = require('@actions/exec'); // make the os.homedir() call be local to the tests jest.doMock('os', () => { @@ -10,10 +11,19 @@ jest.doMock('os', () => { }; }); +jest.mock('@actions/exec', () => { + return { + exec: jest.fn() + }; +}); + import * as auth from '../src/auth'; +const env = process.env; const m2Dir = path.join(__dirname, auth.M2_DIR); const settingsFile = path.join(m2Dir, auth.SETTINGS_FILE); +const gpgDir = path.join(__dirname, auth.GPG_DIR); +const gpgFile = path.join(gpgDir, auth.GPG_FILE); describe('auth tests', () => { beforeEach(async () => { @@ -23,6 +33,7 @@ describe('auth tests', () => { afterAll(async () => { try { await io.rmRF(m2Dir); + await io.rmRF(gpgDir); } catch { console.log('Failed to remove test directories'); } @@ -53,17 +64,25 @@ describe('auth tests', () => { await io.rmRF(altHome); }, 100000); - it('creates settings.xml with username and password', async () => { + it('creates settings.xml with all data', async () => { const id = 'packages'; const username = 'UNAME'; const password = 'TOKEN'; + const gpgPrivateKey = 'PRIVATE'; + const gpgPassphrase = 'GPG'; - await auth.configAuthentication(id, username, password); + await auth.configAuthentication( + id, + username, + password, + gpgPrivateKey, + gpgPassphrase + ); expect(fs.existsSync(m2Dir)).toBe(true); expect(fs.existsSync(settingsFile)).toBe(true); expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual( - auth.generate(id, username, password) + auth.generate(id, username, password, gpgPassphrase) ); }, 100000); @@ -128,8 +147,9 @@ describe('auth tests', () => { const id = 'packages'; const username = 'USER'; const password = '&<>"\'\'"><&'; + const gpgPassphrase = 'PASSWORD'; - expect(auth.generate(id, username, password)).toEqual(` + expect(auth.generate(id, username, password, gpgPassphrase)).toEqual(` @@ -138,7 +158,44 @@ describe('auth tests', () => { \${env.&<>"''"><&} + + + + true + + + \${env.${gpgPassphrase}} + + + `); }); + + it('imports gpg private key', async () => { + const id = 'packages'; + const username = 'USERNAME'; + const password = 'PASSWORD'; + const gpgPrivateKey = 'KEY CONTENTS'; + + await auth.configAuthentication(id, username, password, gpgPrivateKey); + + expect(exec.exec).toHaveBeenCalledWith(`gpg --import --batch ${gpgFile}`); + + expect(fs.existsSync(gpgDir)).toBe(false); + }, 100000); + + it('does not import gpg private key when private key is not set', async () => { + const id = 'packages'; + const username = 'USERNAME'; + const password = 'PASSWORD'; + + await auth.configAuthentication(id, username, password); + + expect(exec.exec).not.toHaveBeenCalledWith( + `gpg --import --batch ${gpgFile}` + ); + + expect(fs.existsSync(gpgDir)).toBe(false); + }, 100000); }); diff --git a/action.yml b/action.yml index de7711fa..a468b5fb 100644 --- a/action.yml +++ b/action.yml @@ -36,6 +36,14 @@ inputs: settings-path: description: 'Path to where the settings.xml file will be written. Default is ~/.m2.' required: false + gpg-private-key: + description: 'Environment variable name for the GPG private key to import. Default is + $GPG_PRIVATE_KEY.' + required: false + gpg-passphrase: + description: 'Environment variable name for the GPG private key passphrase. Default is + $GPG_PASSPHRASE.' + required: false runs: using: 'node12' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index 96632653..b3fd50c0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2879,20 +2879,35 @@ const os = __importStar(__webpack_require__(87)); const path = __importStar(__webpack_require__(622)); const core = __importStar(__webpack_require__(470)); const io = __importStar(__webpack_require__(1)); +const exec = __importStar(__webpack_require__(986)); exports.M2_DIR = '.m2'; exports.SETTINGS_FILE = 'settings.xml'; +exports.GPG_DIR = '.gpgtmp'; +exports.GPG_FILE = 'private.asc'; exports.DEFAULT_ID = 'github'; exports.DEFAULT_USERNAME = 'GITHUB_ACTOR'; exports.DEFAULT_PASSWORD = 'GITHUB_TOKEN'; -function configAuthentication(id = exports.DEFAULT_ID, username = exports.DEFAULT_USERNAME, password = exports.DEFAULT_PASSWORD) { +exports.DEFAULT_GPG_PASSPHRASE = 'GPG_PASSPHRASE'; +exports.DEFAULT_GPG_PRIVATE_KEY = ''; +function configAuthentication(id = exports.DEFAULT_ID, username = exports.DEFAULT_USERNAME, password = exports.DEFAULT_PASSWORD, gpgPrivateKey = exports.DEFAULT_GPG_PRIVATE_KEY, gpgPassphrase = exports.DEFAULT_GPG_PASSPHRASE) { return __awaiter(this, void 0, void 0, function* () { - console.log(`creating ${exports.SETTINGS_FILE} with server-id: ${id};`, `environment variables: username=\$${username} and password=\$${password}`); + console.log(`creating ${exports.SETTINGS_FILE} with server-id: ${id};`, 'environment variables:', `username=\$${username},`, `password=\$${password},`, `and gpg-passphrase=\$${gpgPassphrase}`); // when an alternate m2 location is specified use only that location (no .m2 directory) // otherwise use the home/.m2/ path - const directory = path.join(core.getInput('settings-path') || os.homedir(), core.getInput('settings-path') ? '' : exports.M2_DIR); - yield io.mkdirP(directory); - core.debug(`created directory ${directory}`); - yield write(directory, generate(id, username, password)); + const settingsDirectory = path.join(core.getInput('settings-path') || os.homedir(), core.getInput('settings-path') ? '' : exports.M2_DIR); + yield io.mkdirP(settingsDirectory); + core.debug(`created directory ${settingsDirectory}`); + yield write(settingsDirectory, exports.SETTINGS_FILE, generate(id, username, password, gpgPassphrase)); + if (gpgPrivateKey !== exports.DEFAULT_GPG_PRIVATE_KEY) { + console.log('importing gpg key'); + const gpgDirectory = path.join(os.homedir(), exports.GPG_DIR); + yield io.mkdirP(gpgDirectory); + core.debug(`created directory ${gpgDirectory}`); + yield write(gpgDirectory, exports.GPG_FILE, gpgPrivateKey); + yield importGpgKey(gpgDirectory, exports.GPG_FILE); + yield io.rmRF(gpgDirectory); + core.debug(`removed directory ${gpgDirectory}`); + } }); } exports.configAuthentication = configAuthentication; @@ -2905,7 +2920,7 @@ function escapeXML(value) { .replace(/'/g, '''); } // only exported for testing purposes -function generate(id = exports.DEFAULT_ID, username = exports.DEFAULT_USERNAME, password = exports.DEFAULT_PASSWORD) { +function generate(id = exports.DEFAULT_ID, username = exports.DEFAULT_USERNAME, password = exports.DEFAULT_PASSWORD, gpgPassphrase = exports.DEFAULT_GPG_PASSPHRASE) { return ` @@ -2915,25 +2930,41 @@ function generate(id = exports.DEFAULT_ID, username = exports.DEFAULT_USERNAME, \${env.${escapeXML(password)}} + + + + true + + + \${env.${escapeXML(gpgPassphrase)}} + + + `; } exports.generate = generate; -function write(directory, settings) { +function write(directory, file, contents) { return __awaiter(this, void 0, void 0, function* () { - const location = path.join(directory, exports.SETTINGS_FILE); + const location = path.join(directory, file); if (fs.existsSync(location)) { console.warn(`overwriting existing file ${location}`); } else { console.log(`writing ${location}`); } - return fs.writeFileSync(location, settings, { + return fs.writeFileSync(location, contents, { encoding: 'utf-8', flag: 'w' }); }); } +function importGpgKey(directory, file) { + return __awaiter(this, void 0, void 0, function* () { + const location = path.join(directory, file); + exec.exec(`gpg --import --batch ${location}`); + }); +} /***/ }), @@ -4563,7 +4594,9 @@ function run() { 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; - yield auth.configAuthentication(id, username, password); + const gpgPassphrase = core.getInput('gpg-passphrase', { required: false }) || undefined; + const gpgPrivateKey = core.getInput('gpg-private-key', { required: false }) || undefined; + yield auth.configAuthentication(id, username, password, gpgPassphrase, gpgPrivateKey); } catch (error) { core.setFailed(error.message); diff --git a/src/auth.ts b/src/auth.ts index 2e7c6e8e..53e058f8 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -3,32 +3,57 @@ import * as os from 'os'; import * as path from 'path'; import * as core from '@actions/core'; import * as io from '@actions/io'; +import * as exec from '@actions/exec'; export const M2_DIR = '.m2'; export const SETTINGS_FILE = 'settings.xml'; +export const GPG_DIR = '.gpgtmp'; +export const GPG_FILE = 'private.asc'; export const DEFAULT_ID = 'github'; export const DEFAULT_USERNAME = 'GITHUB_ACTOR'; export const DEFAULT_PASSWORD = 'GITHUB_TOKEN'; +export const DEFAULT_GPG_PASSPHRASE = 'GPG_PASSPHRASE'; +export const DEFAULT_GPG_PRIVATE_KEY = ''; export async function configAuthentication( id = DEFAULT_ID, username = DEFAULT_USERNAME, - password = DEFAULT_PASSWORD + password = DEFAULT_PASSWORD, + gpgPrivateKey = DEFAULT_GPG_PRIVATE_KEY, + gpgPassphrase = DEFAULT_GPG_PASSPHRASE ) { console.log( `creating ${SETTINGS_FILE} with server-id: ${id};`, - `environment variables: username=\$${username} and password=\$${password}` + 'environment variables:', + `username=\$${username},`, + `password=\$${password},`, + `and gpg-passphrase=\$${gpgPassphrase}` ); // 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( + const settingsDirectory: 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)); + await io.mkdirP(settingsDirectory); + core.debug(`created directory ${settingsDirectory}`); + await write( + settingsDirectory, + SETTINGS_FILE, + generate(id, username, password, gpgPassphrase) + ); + + if (gpgPrivateKey !== DEFAULT_GPG_PRIVATE_KEY) { + console.log('importing gpg key'); + const gpgDirectory: string = path.join(os.homedir(), GPG_DIR); + await io.mkdirP(gpgDirectory); + core.debug(`created directory ${gpgDirectory}`); + await write(gpgDirectory, GPG_FILE, gpgPrivateKey); + await importGpgKey(gpgDirectory, GPG_FILE); + await io.rmRF(gpgDirectory); + core.debug(`removed directory ${gpgDirectory}`); + } } function escapeXML(value: string) { @@ -44,7 +69,8 @@ function escapeXML(value: string) { export function generate( id = DEFAULT_ID, username = DEFAULT_USERNAME, - password = DEFAULT_PASSWORD + password = DEFAULT_PASSWORD, + gpgPassphrase = DEFAULT_GPG_PASSPHRASE ) { return ` @@ -55,20 +81,35 @@ export function generate( \${env.${escapeXML(password)}} + + + + true + + + \${env.${escapeXML(gpgPassphrase)}} + + + `; } -async function write(directory: string, settings: string) { - const location = path.join(directory, SETTINGS_FILE); +async function write(directory: string, file: string, contents: string) { + const location = path.join(directory, file); if (fs.existsSync(location)) { console.warn(`overwriting existing file ${location}`); } else { console.log(`writing ${location}`); } - return fs.writeFileSync(location, settings, { + return fs.writeFileSync(location, contents, { encoding: 'utf-8', flag: 'w' }); } + +async function importGpgKey(directory: string, file: string) { + const location = path.join(directory, file); + exec.exec(`gpg --import --batch ${location}`); +} diff --git a/src/setup-java.ts b/src/setup-java.ts index d0392175..6564f2cd 100644 --- a/src/setup-java.ts +++ b/src/setup-java.ts @@ -23,8 +23,18 @@ async function run() { core.getInput('server-username', {required: false}) || undefined; const password = core.getInput('server-password', {required: false}) || undefined; + const gpgPassphrase = + core.getInput('gpg-passphrase', {required: false}) || undefined; + const gpgPrivateKey = + core.getInput('gpg-private-key', {required: false}) || undefined; - await auth.configAuthentication(id, username, password); + await auth.configAuthentication( + id, + username, + password, + gpgPassphrase, + gpgPrivateKey + ); } catch (error) { core.setFailed(error.message); }