From e563ea4d9d6f16b593f10e6e116b0d728bf497a9 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 12 Oct 2025 20:56:14 +0200 Subject: [PATCH 1/2] feat(git-node): verify tag with official keyring --- lib/promote_release.js | 59 +++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index 89abc497..a8f7000e 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -1,5 +1,7 @@ import path from 'node:path'; import fs from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { pipeline } from 'node:stream/promises'; import semver from 'semver'; import * as gst from 'git-secure-tag'; @@ -196,31 +198,40 @@ export default class ReleasePromotion extends Session { async verifyTagSignature(version) { const { cli } = this; - const verifyTagPattern = /gpg:[^\n]+\ngpg:\s+using \w+ key ([^\n]+)\ngpg:\s+issuer "([^"]+)"\ngpg:\s+Good signature from (?:"[^"]+"(?: \[ultimate\])?\ngpg:\s+aka )*"([^<]+) <\2>"/; - const [verifyTagOutput, haystack] = await Promise.all([forceRunAsync( - 'git', ['--no-pager', - 'verify-tag', - `v${version}` - ], { ignoreFailure: false, captureStderr: true }), fs.readFile('README.md')]); - const match = verifyTagPattern.exec(verifyTagOutput); - if (match == null) { - cli.warn('git was not able to verify the tag:'); - cli.info(verifyTagOutput); - } else { - const [, keyID, email, name] = match; - const needle = `* **${name}** <<${email}>>\n ${'`'}${keyID}${'`'}`; - if (haystack.includes(needle)) { - return; + + cli.startSpinner('Downloading active releasers keyring from nodejs/release-keys...'); + const [keyRingStream, [GNUPGHOME, keyRingFd]] = await Promise.all([ + fetch('https://github.com/nodejs/release-keys/raw/HEAD/gpg-only-active-keys/pubring.kbx'), + fs.mkdtemp(path.join(tmpdir(), 'ncu-')) + .then(async d => [d, await fs.open(path.join(d, 'pubring.kbx'), 'w')]), + ]); + if (!keyRingStream.ok) throw new Error('Failed to download keyring', { cause: keyRingStream }); + await pipeline(keyRingStream.body, keyRingFd.createWriteStream()); + cli.stopSpinner('Active releasers keyring stored in temp directory'); + + try { + await forceRunAsync( + 'git', ['--no-pager', + 'verify-tag', + `v${version}` + ], { + ignoreFailure: false, + spawnArgs: { env: { ...process.env, GNUPGHOME } }, + }); + cli.ok('git tag signature verified'); + } catch (cause) { + cli.error('git was not able to verify the tag'); + cli.warn('This means that either the tag was signed with the wrong key,'); + cli.warn('or that nodejs/release-keys contains outdated information.'); + cli.warn('The release should not proceed.'); + if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) { + cli.info(`Run 'git tag -d v${version}' to remove the local tag.`); + throw new Error('Aborted', { cause }); } - cli.warn('Tag was signed with an undocumented identity/key pair!'); - cli.info('Expected to find the following entry in the README:'); - cli.info(needle); - cli.info('If you are using a subkey, it might be OK.'); - } - cli.info(`If that doesn't sound right, consider removing the tag (git tag -d v${version - }), check your local config, and start the process over.`); - if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) { - throw new Error('Aborted'); + } finally { + cli.startSpinner('Cleaning up temp files'); + await fs.rm(GNUPGHOME, { force: true, recursive: true }); + cli.stopSpinner('Temp files removed'); } } From a176326e8346acac2601e1babec14bc329e275f6 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 13 Oct 2025 12:00:04 +0200 Subject: [PATCH 2/2] fixup! feat(git-node): verify tag with official keyring --- lib/promote_release.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index a8f7000e..bdddacbb 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -225,7 +225,11 @@ export default class ReleasePromotion extends Session { cli.warn('or that nodejs/release-keys contains outdated information.'); cli.warn('The release should not proceed.'); if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) { - cli.info(`Run 'git tag -d v${version}' to remove the local tag.`); + if (await cli.prompt('Do you want to delete the local tag?')) { + await forceRunAsync('git', ['tag', '-d', `v${version}`]); + } else { + cli.info(`Run 'git tag -d v${version}' to remove the local tag.`); + } throw new Error('Aborted', { cause }); } } finally {