From 6acdcd5bd943bacaf9dbe0a9a9838207c730357a Mon Sep 17 00:00:00 2001 From: Florian Friedrich Date: Mon, 29 Apr 2024 11:11:31 +0200 Subject: [PATCH] Allow using any user id of the key's user ids as committer identity. Fixes #156 --- README.md | 13 +++++++------ __tests__/openpgp.test.ts | 40 ++++++++++++++++++++++++++++++--------- action.yml | 6 ++++-- src/main.ts | 25 ++++++++++++++---------- src/openpgp.ts | 22 +++++++++++++++------ 5 files changed, 73 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 6d0a36b9..9d6ebec3 100644 --- a/README.md +++ b/README.md @@ -229,12 +229,13 @@ The following inputs can be used as `step.with` keys Following outputs are available -| Name | Type | Description | -|---------------|--------|---------------------------------------------------------------------------------------------------------------------------------| -| `fingerprint` | String | Fingerprint of the GPG key (recommended as [user ID](https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html)) | -| `keyid` | String | Low 64 bits of the X.509 certificate SHA-1 fingerprint | -| `name` | String | Name associated with the GPG key | -| `email` | String | Email address associated with the GPG key | +| Name | Type | Description | +|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `fingerprint` | String | Fingerprint of the GPG key (recommended as [user ID](https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html)) | +| `keyid` | String | Low 64 bits of the X.509 certificate SHA-1 fingerprint | +| `name` | String | Name associated with the GPG key | +| `email` | String | Email address associated with the GPG key | +| `userids` | String (JSON) | All user ids (including primary) associated with the GPG Key.
The output is a JSON array where each object has a `name` and `email` key. Use [fromJson](https://docs.github.com/en/actions/learn-github-actions/expressions#fromjson) to turn the String back into a JSON array | ## Contributing diff --git a/__tests__/openpgp.test.ts b/__tests__/openpgp.test.ts index 565c8c91..86e01fb8 100644 --- a/__tests__/openpgp.test.ts +++ b/__tests__/openpgp.test.ts @@ -17,8 +17,16 @@ const userInfos = [ encoding: 'utf8', flag: 'r' }), - name: 'Joe Tester', - email: 'joe@foo.bar', + primaryUserId: { + name: 'Joe Tester', + email: 'joe@foo.bar' + }, + userIds: [ + { + name: 'Joe Tester', + email: 'joe@foo.bar' + } + ], keyID: '7D851EB72D73BDA0', fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', keygrip: '3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627' @@ -37,8 +45,16 @@ const userInfos = [ encoding: 'utf8', flag: 'r' }), - name: 'Joe Bar', - email: 'joe@bar.foo', + primaryUserId: { + name: 'Joe Bar', + email: 'joe@bar.foo' + }, + userIds: [ + { + name: 'Joe Bar', + email: 'joe@bar.foo' + } + ], keyID: '6071D218380FDCC8', fingerprint: '87F257B89CE462100BEC0FFE6071D218380FDCC8', keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB'] @@ -52,16 +68,22 @@ for (const userInfo of userInfos) { it('returns a PGP private key from an armored string', async () => { await openpgp.readPrivateKey(userInfo.pgp).then(privateKey => { expect(privateKey.keyID).toEqual(userInfo.keyID); - expect(privateKey.name).toEqual(userInfo.name); - expect(privateKey.email).toEqual(userInfo.email); + expect(privateKey.primaryUserId.name).toEqual(userInfo.primaryUserId.name); + expect(privateKey.primaryUserId.email).toEqual(userInfo.primaryUserId.email); + expect(privateKey.allUserIds).toHaveLength(userInfo.userIds.length); + expect(privateKey.allUserIds[0].name).toEqual(userInfo.userIds[0].name); + expect(privateKey.allUserIds[0].email).toEqual(userInfo.userIds[0].email); expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); }); }); it('returns a PGP private key from a base64 armored string', async () => { await openpgp.readPrivateKey(userInfo.pgp_base64).then(privateKey => { expect(privateKey.keyID).toEqual(userInfo.keyID); - expect(privateKey.name).toEqual(userInfo.name); - expect(privateKey.email).toEqual(userInfo.email); + expect(privateKey.primaryUserId.name).toEqual(userInfo.primaryUserId.name); + expect(privateKey.primaryUserId.email).toEqual(userInfo.primaryUserId.email); + expect(privateKey.allUserIds).toHaveLength(userInfo.userIds.length); + expect(privateKey.allUserIds[0].name).toEqual(userInfo.userIds[0].name); + expect(privateKey.allUserIds[0].email).toEqual(userInfo.userIds[0].email); expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); }); }); @@ -69,7 +91,7 @@ for (const userInfo of userInfos) { describe('generateKeyPair', () => { it('generates a PGP key pair', async () => { - await openpgp.generateKeyPair(userInfo.name, userInfo.email, userInfo.passphrase).then(keyPair => { + await openpgp.generateKeyPair(userInfo.primaryUserId.name, userInfo.primaryUserId.email, userInfo.passphrase).then(keyPair => { expect(keyPair).not.toBeUndefined(); expect(keyPair.publicKey).not.toBeUndefined(); expect(keyPair.privateKey).not.toBeUndefined(); diff --git a/action.yml b/action.yml index c9e89a73..8dec262a 100644 --- a/action.yml +++ b/action.yml @@ -56,9 +56,11 @@ outputs: keyid: description: 'Low 64 bits of the X.509 certificate SHA-1 fingerprint' name: - description: 'Name associated with the GPG key' + description: 'Primary name associated with the GPG key' email: - description: 'Email address associated with the GPG key' + description: 'Primary email address associated with the GPG key' + userids: + description: 'All user ids (including primary) associated with the GPG Key' runs: using: 'node20' diff --git a/src/main.ts b/src/main.ts index 591f23ba..c4449f00 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,8 +28,10 @@ async function run(): Promise { await core.group(`GPG private key info`, async () => { core.info(`Fingerprint : ${privateKey.fingerprint}`); core.info(`KeyID : ${privateKey.keyID}`); - core.info(`Name : ${privateKey.name}`); - core.info(`Email : ${privateKey.email}`); + for (const userId of privateKey.allUserIds) { + const isPrimary = userId.email === privateKey.primaryUserId.email; + core.info(`User ID : ${userId.name} <${userId.email}>${isPrimary ? ' (primary)' : ''}`); + } core.info(`CreationTime : ${privateKey.creationTime}`); }); @@ -91,21 +93,24 @@ async function run(): Promise { core.setOutput('fingerprint', fingerprint); core.info(`keyid=${privateKey.keyID}`); core.setOutput('keyid', privateKey.keyID); - core.info(`name=${privateKey.name}`); - core.setOutput('name', privateKey.name); - core.info(`email=${privateKey.email}`); - core.setOutput('email', privateKey.email); + core.info(`name=${privateKey.primaryUserId.name}`); + core.setOutput('name', privateKey.primaryUserId.name); + core.info(`email=${privateKey.primaryUserId.email}`); + core.setOutput('email', privateKey.primaryUserId.email); + core.info(`userids=${JSON.stringify(privateKey.allUserIds)}`); + core.setOutput('userids', privateKey.allUserIds); }); if (inputs.gitUserSigningkey) { core.info('Setting GPG signing keyID for this Git repository'); await git.setConfig('user.signingkey', privateKey.keyID, inputs.gitConfigGlobal); - const userEmail = inputs.gitCommitterEmail || privateKey.email; - const userName = inputs.gitCommitterName || privateKey.name; + const userName = inputs.gitCommitterName || privateKey.primaryUserId.name; + const userEmail = inputs.gitCommitterEmail || privateKey.primaryUserId.email; - if (userEmail != privateKey.email) { - core.setFailed(`Committer email "${inputs.gitCommitterEmail}" (name: "${inputs.gitCommitterName}") does not match GPG private key email "${privateKey.email}" (name: "${privateKey.name}")`); + if (!privateKey.allUserIds.some(id => id.email === userEmail)) { + const keyIdentities = privateKey.allUserIds.map(id => `"${id.email}" (name: "${id.name}")`).join(', '); + core.setFailed(`Committer email "${inputs.gitCommitterEmail}" (name: "${inputs.gitCommitterName}") does not match GPG any of the private key user id email addresses: ${keyIdentities}`); return; } diff --git a/src/openpgp.ts b/src/openpgp.ts index 91908efd..d3a382ff 100644 --- a/src/openpgp.ts +++ b/src/openpgp.ts @@ -1,11 +1,16 @@ import * as openpgp from 'openpgp'; import addressparser from 'addressparser'; +export interface UserId { + name: string; + email: string; +} + export interface PrivateKey { fingerprint: string; keyID: string; - name: string; - email: string; + primaryUserId: UserId; + allUserIds: UserId[]; creationTime: Date; } @@ -19,15 +24,20 @@ export const readPrivateKey = async (key: string): Promise => { armoredKey: (await isArmored(key)) ? key : Buffer.from(key, 'base64').toString() }); - const address = await privateKey.getPrimaryUser().then(primaryUser => { - return addressparser(primaryUser.user.userID?.userID)[0]; + const primaryUserId: UserId = await privateKey.getPrimaryUser().then(primaryUser => { + const address = addressparser(primaryUser.user.userID?.userID)[0]; + return {name: address.name, email: address.address}; + }); + const allUserIds: UserId[] = privateKey.getUserIDs().map(userId => { + const address = addressparser(userId)[0]; + return {name: address.name, email: address.address}; }); return { fingerprint: privateKey.getFingerprint().toUpperCase(), keyID: privateKey.getKeyID().toHex().toUpperCase(), - name: address.name, - email: address.address, + primaryUserId: primaryUserId, + allUserIds: allUserIds, creationTime: privateKey.getCreationTime() }; };