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

Added support for GPG signing #63

Merged
merged 12 commits into from
Jul 22, 2020
51 changes: 36 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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```)


Expand Down Expand Up @@ -113,39 +113,60 @@ 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
gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase

- 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.
jaredpetersen marked this conversation as resolved.
Show resolved Hide resolved

`settings.xml` file created for the first deploy to GitHub Packages
```xml
<servers>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>github</id>
<username>${env.GITHUB_ACTOR}</username>
<password>${env.GITHUB_TOKEN}</password>
</server>
</servers>
<server>
<id>gpg.passphrase</id>
<passphrase>${env.GPG_PASSPHRASE}</passphrase>
</server>
</servers>
</settings>
```

`settings.xml` file created for the second deploy to Apache Maven Central
```xml
<servers>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>maven</id>
<username>${env.MAVEN_USERNAME}</username>
<password>${env.MAVEN_CENTRAL_TOKEN}</password>
</server>
</servers>
<server>
<id>gpg.passphrase</id>
<passphrase>${env.MAVEN_GPG_PASSPHRASE}</passphrase>
</server>
</servers>
</settings>
```

***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.***

If `gpg-private-key` and `gpg-passphrase` inputs are provided, the private key will be written to a file in the runner's temp directory, the private key file will be imported into the GPG keychain, and then the file will be promptly removed before proceeding with the rest of the setup process. A cleanup step will remove the imported private key from the GPG keychain after the job completes regardless of the job status. This ensures that the private key is no longer accessible on self-hosted runners and cannot "leak" between jobs (hosted runners are always clean instances).
jaredpetersen marked this conversation as resolved.
Show resolved Hide resolved
jaredpetersen marked this conversation as resolved.
Show resolved Hide resolved

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.

Expand All @@ -172,7 +193,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.

Expand Down
106 changes: 56 additions & 50 deletions __tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('auth tests', () => {
await io.rmRF(altHome);
}, 100000);

it('creates settings.xml with username and password', async () => {
it('creates settings.xml with minimal configuration', async () => {
const id = 'packages';
const username = 'UNAME';
const password = 'TOKEN';
Expand All @@ -67,78 +67,84 @@ describe('auth tests', () => {
);
}, 100000);

it('overwrites existing settings.xml files', async () => {
it('creates settings.xml with additional configuration', 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);
const username = 'UNAME';
const password = 'TOKEN';
const gpgPassphrase = 'GPG';

await auth.configAuthentication(id, username, password);
await auth.configAuthentication(id, username, password, 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);

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');
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);
expect(fs.readFileSync(settingsFile, 'utf-8')).toEqual(
auth.generate(auth.DEFAULT_ID, auth.DEFAULT_USERNAME, 'BAZ')
);

await auth.configAuthentication();
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(
auth.DEFAULT_ID,
auth.DEFAULT_USERNAME,
auth.DEFAULT_PASSWORD
)
auth.generate(id, username, password)
);
}, 100000);

it('escapes invalid XML inputs', () => {
it('generates valid settings.xml with minimal configuration', () => {
const id = 'packages';
const username = 'USER';
const password = '&<>"\'\'"><&';

expect(auth.generate(id, username, password)).toEqual(`
<settings>
<servers>
<server>
<id>${id}</id>
<username>\${env.${username}}</username>
<password>\${env.&amp;&lt;&gt;&quot;&apos;&apos;&quot;&gt;&lt;&amp;}</password>
</server>
</servers>
</settings>
`);
const expectedSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>${id}</id>
<username>\${env.${username}}</username>
<password>\${env.&amp;&lt;&gt;"''"&gt;&lt;&amp;}</password>
</server>
</servers>
</settings>`;

expect(auth.generate(id, username, password)).toEqual(expectedSettings);
});

it('generates valid settings.xml with additional configuration', () => {
const id = 'packages';
const username = 'USER';
const password = '&<>"\'\'"><&';
const gpgPassphrase = 'PASSPHRASE';

const expectedSettings = `<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>${id}</id>
<username>\${env.${username}}</username>
<password>\${env.&amp;&lt;&gt;"''"&gt;&lt;&amp;}</password>
</server>
<server>
<id>gpg.passphrase</id>
<passphrase>\${env.${gpgPassphrase}}</passphrase>
</server>
</servers>
</settings>`;

expect(auth.generate(id, username, password, gpgPassphrase)).toEqual(
expectedSettings
);
});
});
56 changes: 56 additions & 0 deletions __tests__/gpg.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import path = require('path');
import io = require('@actions/io');
import exec = require('@actions/exec');

jest.mock('@actions/exec', () => {
return {
exec: jest.fn()
};
});

const tempDir = path.join(__dirname, 'runner', 'temp');
process.env['RUNNER_TEMP'] = tempDir;

import gpg = require('../src/gpg');

describe('gpg tests', () => {
beforeEach(async () => {
await io.mkdirP(tempDir);
});

afterAll(async () => {
try {
await io.rmRF(tempDir);
} catch {
console.log('Failed to remove test directories');
}
});

describe('importKey', () => {
it('attempts to import private key and returns null key id on failure', async () => {
const privateKey = 'KEY CONTENTS';
const keyId = await gpg.importKey(privateKey);

expect(keyId).toBeNull();

expect(exec.exec).toHaveBeenCalledWith(
'gpg',
expect.anything(),
expect.anything()
);
});
});

describe('deleteKey', () => {
it('deletes private key', async () => {
const keyId = 'asdfhjkl';
await gpg.deleteKey(keyId);

expect(exec.exec).toHaveBeenCalledWith(
'gpg',
expect.anything(),
expect.anything()
);
});
});
});
61 changes: 61 additions & 0 deletions __tests__/util.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import path = require('path');

const env = process.env;

describe('util tests', () => {
beforeEach(() => {
const tempEnv = Object.assign({}, env);
delete tempEnv.RUNNER_TEMP;
delete tempEnv.USERPROFILE;
process.env = tempEnv;
Object.defineProperty(process, 'platform', {value: 'linux'});
});

describe('getTempDir', () => {
it('gets temp dir using env', () => {
process.env['RUNNER_TEMP'] = 'defaulttmp';
const util = require('../src/util');

const tempDir = util.getTempDir();

expect(tempDir).toEqual(process.env['RUNNER_TEMP']);
});

it('gets temp dir for windows using userprofile', () => {
Object.defineProperty(process, 'platform', {value: 'win32'});
process.env['USERPROFILE'] = 'winusertmp';
const util = require('../src/util');

const tempDir = util.getTempDir();

expect(tempDir).toEqual(
path.join(process.env['USERPROFILE'], 'actions', 'temp')
);
});

it('gets temp dir for windows using c drive', () => {
Object.defineProperty(process, 'platform', {value: 'win32'});
const util = require('../src/util');

const tempDir = util.getTempDir();

expect(tempDir).toEqual(path.join('C:\\', 'actions', 'temp'));
});

it('gets temp dir for mac', () => {
Object.defineProperty(process, 'platform', {value: 'darwin'});
const util = require('../src/util');

const tempDir = util.getTempDir();

expect(tempDir).toEqual(path.join('/Users', 'actions', 'temp'));
});

it('gets temp dir for linux', () => {
const util = require('../src/util');
const tempDir = util.getTempDir();

expect(tempDir).toEqual(path.join('/home', 'actions', 'temp'));
});
});
});
Loading