@node-core/utils 5.16.2 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -76,18 +76,29 @@ Optionally, if you want to grant write access so `git-node` can write comments:
76
76
 
77
77
  You can also edit the permission of existing tokens later.
78
78
 
79
- After the token is generated, create an rc file with the following content:
80
- (`~/.ncurc` or `$XDG_CONFIG_HOME/ncurc`):
81
-
82
- ```json
83
- {
84
- "username": "your_github_username",
85
- "token": "token_that_you_created"
86
- }
79
+ After the token is generated, you can give it to NCU using:
80
+
81
+ <details open name="set-token"><summary>With encryption (Recommended)</summary>
82
+
83
+ ```sh
84
+ ncu-config set username your_github_username
85
+ # Do not provide the token in the CLI, `ncu-config` will prompt you for it.
86
+ ncu-config set -x token
87
+ ```
88
+
89
+ Note: Encryption is available only if you have `gpg` setup on your machine.
90
+
91
+ </details>
92
+
93
+ <details name="set-token"><summary>Without encryption</summary>
94
+
95
+ ```sh
96
+ ncu-config set username your_github_username
97
+ # Do not provide the token in the CLI, `ncu-config` will prompt you for it.
98
+ ncu-config set token
87
99
  ```
88
100
 
89
- Note: you could use `ncu-config` to configure these variables, but it's not
90
- recommended to leave your tokens in your command line history.
101
+ </details>
91
102
 
92
103
  ### Setting up Jenkins credentials
93
104
 
@@ -108,27 +119,24 @@ To obtain the Jenkins API token
108
119
  `~/.ncurc.gpg` or `$XDG_CONFIG_HOME/ncurc.gpg`) with `jenkins_token` as key,
109
120
  like this:
110
121
 
111
- ```json
112
- {
113
- "username": "your_github_username",
114
- "token": "your_github_token",
115
- "jenkins_token": "your_jenkins_token"
116
- }
122
+ <details open name="set-jenkins-token"><summary>With encryption (recommended)</summary>
123
+
124
+ ```sh
125
+ ncu-config set -x jenkins_token
117
126
  ```
118
127
 
119
- ### Protecting your credentials
128
+ Note: Encryption is available only if you have `gpg` setup on your machine.
120
129
 
121
- If you have `gpg` installed and setup on your local machine, it is strongly recommended
122
- to store an encrypted version of this file:
130
+ </details>
131
+ <details name="set-jenkins-token"><summary>Without encryption</summary>
132
+
133
+ ```sh
134
+ ncu-config set jenkins_token
135
+ ```
123
136
 
124
- ```console
125
- $ gpg --default-recipient-self --encrypt ~/.ncurc
126
- $ rm ~/.ncurc
127
- ```
137
+ </details>
128
138
 
129
- The credentials are now encrypted in `~/.ncurc.gpg` and everytime it's needed,
130
- node-core-utils will invoke `gpg` that may ask you to decrypt it using
131
- your default key via pinentry.
139
+ ### Protecting your credentials
132
140
 
133
141
  Put the following entries into your
134
142
  [global `gitignore` file](https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreexcludesFile)
package/bin/ncu-config.js CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import * as readline from 'node:readline/promises';
4
+ import { stdin as input, stdout as output } from 'node:process';
5
+
3
6
  import yargs from 'yargs';
4
7
  import { hideBin } from 'yargs/helpers';
5
8
 
6
9
  import {
7
- getConfig, updateConfig, GLOBAL_CONFIG, PROJECT_CONFIG, LOCAL_CONFIG
10
+ getConfig, updateConfig, GLOBAL_CONFIG, PROJECT_CONFIG, LOCAL_CONFIG,
11
+ encryptValue
8
12
  } from '../lib/config.js';
9
13
  import { setVerbosityFromEnv } from '../lib/verbosity.js';
10
14
 
@@ -13,10 +17,15 @@ setVerbosityFromEnv();
13
17
  const args = yargs(hideBin(process.argv))
14
18
  .completion('completion')
15
19
  .command({
16
- command: 'set <key> <value>',
20
+ command: 'set <key> [<value>]',
17
21
  desc: 'Set a config variable',
18
22
  builder: (yargs) => {
19
23
  yargs
24
+ .option('encrypt', {
25
+ describe: 'Store the value encrypted using gpg',
26
+ alias: 'x',
27
+ type: 'boolean'
28
+ })
20
29
  .positional('key', {
21
30
  describe: 'key of the configuration',
22
31
  type: 'string'
@@ -61,8 +70,6 @@ const args = yargs(hideBin(process.argv))
61
70
  .conflicts('global', 'project')
62
71
  .help();
63
72
 
64
- const argv = args.parse();
65
-
66
73
  function getConfigType(argv) {
67
74
  if (argv.global) {
68
75
  return { configName: 'global', configType: GLOBAL_CONFIG };
@@ -73,9 +80,19 @@ function getConfigType(argv) {
73
80
  return { configName: 'local', configType: LOCAL_CONFIG };
74
81
  }
75
82
 
76
- function setHandler(argv) {
83
+ async function setHandler(argv) {
77
84
  const { configName, configType } = getConfigType(argv);
78
85
  const config = getConfig(configType);
86
+ if (!argv.value) {
87
+ const rl = readline.createInterface({ input, output });
88
+ argv.value = await rl.question('What value do you want to set? ');
89
+ rl.close();
90
+ } else if (argv.encrypt) {
91
+ console.warn('Passing sensitive config value via the shell is discouraged');
92
+ }
93
+ if (argv.encrypt) {
94
+ argv.value = await encryptValue(argv.value);
95
+ }
79
96
  console.log(
80
97
  `Updating ${configName} configuration ` +
81
98
  `[${argv.key}]: ${config[argv.key]} -> ${argv.value}`);
@@ -96,6 +113,8 @@ function listHandler(argv) {
96
113
  }
97
114
  }
98
115
 
116
+ const argv = await args.parse();
117
+
99
118
  if (!['get', 'set', 'list'].includes(argv._[0])) {
100
119
  args.showHelp();
101
120
  }
@@ -2,12 +2,12 @@ import path from 'node:path';
2
2
 
3
3
  import logSymbols from 'log-symbols';
4
4
 
5
- import { minor, major, backport } from '../../lib/update-v8/index.js';
5
+ import { minor, major, backport, deps } from '../../lib/update-v8/index.js';
6
6
  import { defaultBaseDir } from '../../lib/update-v8/constants.js';
7
7
  import { checkCwd } from '../../lib/update-v8/common.js';
8
8
  import { forceRunAsync } from '../../lib/run.js';
9
9
 
10
- export const command = 'v8 [major|minor|backport]';
10
+ export const command = 'v8 [major|minor|backport|deps]';
11
11
  export const describe = 'Update or patch the V8 engine';
12
12
 
13
13
  export function builder(yargs) {
@@ -26,6 +26,11 @@ export function builder(yargs) {
26
26
  describe: 'Bump the NODE_MODULE_VERSION constant',
27
27
  default: true
28
28
  });
29
+ yargs.option('concurrent', {
30
+ type: 'boolean',
31
+ describe: 'Update dependencies concurrently',
32
+ default: true,
33
+ });
29
34
  }
30
35
  })
31
36
  .command({
@@ -64,6 +69,19 @@ export function builder(yargs) {
64
69
  });
65
70
  }
66
71
  })
72
+ .command({
73
+ command: 'deps',
74
+ desc: 'Update V8 dependencies from the DEPS file',
75
+ handler,
76
+ builder: (yargs) => {
77
+ yargs
78
+ .option('concurrent', {
79
+ type: 'boolean',
80
+ describe: 'Update dependencies concurrently',
81
+ default: true,
82
+ });
83
+ }
84
+ })
67
85
  .demandCommand(1, 'Please provide a valid command')
68
86
  .option('node-dir', {
69
87
  describe: 'Directory of a Node.js clone',
@@ -126,6 +144,8 @@ export function handler(argv) {
126
144
  return major(options);
127
145
  case 'backport':
128
146
  return backport(options);
147
+ case 'deps':
148
+ return deps(options);
129
149
  }
130
150
  })
131
151
  .catch((err) => {
package/lib/auth.js CHANGED
@@ -3,7 +3,7 @@ import { ClientRequest } from 'node:http';
3
3
 
4
4
  import ghauth from 'ghauth';
5
5
 
6
- import { getMergedConfig, getNcurcPath } from './config.js';
6
+ import { clearCachedConfig, encryptValue, getMergedConfig, getNcurcPath } from './config.js';
7
7
 
8
8
  export default lazy(auth);
9
9
 
@@ -60,67 +60,90 @@ function encode(name, token) {
60
60
  return Buffer.from(`${name}:${token}`).toString('base64');
61
61
  }
62
62
 
63
+ function setOwnProperty(target, key, value) {
64
+ return Object.defineProperty(target, key, {
65
+ __proto__: null,
66
+ configurable: true,
67
+ enumerable: true,
68
+ value
69
+ });
70
+ }
71
+
63
72
  // TODO: support jenkins only...or not necessary?
64
73
  // TODO: make this a class with dependency (CLI) injectable for testing
65
74
  async function auth(
66
75
  options = { github: true },
67
76
  githubAuth = ghauth) {
68
- const result = {};
77
+ const result = {
78
+ get github() {
79
+ let username;
80
+ let token;
81
+ try {
82
+ ({ username, token } = getMergedConfig());
83
+ } catch (e) {
84
+ // Ignore error and prompt
85
+ }
86
+
87
+ check(username, token);
88
+ const github = encode(username, token);
89
+ setOwnProperty(result, 'github', github);
90
+ return github;
91
+ },
92
+
93
+ get jenkins() {
94
+ const { username, jenkins_token } = getMergedConfig();
95
+ if (!username || !jenkins_token) {
96
+ errorExit(
97
+ 'Get your Jenkins API token in https://ci.nodejs.org/me/security ' +
98
+ 'and run the following command to add it to your ncu config: ' +
99
+ 'ncu-config --global set -x jenkins_token'
100
+ );
101
+ };
102
+ check(username, jenkins_token);
103
+ const jenkins = encode(username, jenkins_token);
104
+ setOwnProperty(result, 'jenkins', jenkins);
105
+ return jenkins;
106
+ },
107
+
108
+ get h1() {
109
+ const { h1_username, h1_token } = getMergedConfig();
110
+ check(h1_username, h1_token);
111
+ const h1 = encode(h1_username, h1_token);
112
+ setOwnProperty(result, 'h1', h1);
113
+ return h1;
114
+ }
115
+ };
69
116
  if (options.github) {
70
- let username;
71
- let token;
117
+ let config;
72
118
  try {
73
- ({ username, token } = getMergedConfig());
74
- } catch (e) {
75
- // Ignore error and prompt
119
+ config = getMergedConfig();
120
+ } catch {
121
+ config = {};
76
122
  }
77
-
78
- if (!username || !token) {
123
+ if (!Object.hasOwn(config, 'token') || !Object.hasOwn(config, 'username')) {
79
124
  process.stdout.write(
80
125
  'If this is your first time running this command, ' +
81
126
  'follow the instructions to create an access token' +
82
127
  '. If you prefer to create it yourself on Github, ' +
83
128
  'see https://github.com/nodejs/node-core-utils/blob/main/README.md.\n');
84
129
  const credentials = await tryCreateGitHubToken(githubAuth);
85
- username = credentials.user;
86
- token = credentials.token;
130
+ const username = credentials.user;
131
+ let token;
132
+ try {
133
+ token = await encryptValue(credentials.token);
134
+ } catch (err) {
135
+ console.warn('Failed encrypt token, storing unencrypted instead');
136
+ token = credentials.token;
137
+ }
87
138
  const json = JSON.stringify({ username, token }, null, 2);
88
139
  fs.writeFileSync(getNcurcPath(), json, {
89
140
  mode: 0o600 /* owner read/write */
90
141
  });
91
142
  // Try again reading the file
92
- ({ username, token } = getMergedConfig());
143
+ clearCachedConfig();
93
144
  }
94
- check(username, token);
95
- result.github = encode(username, token);
96
145
  }
97
146
 
98
- if (options.jenkins) {
99
- const { username, jenkins_token } = getMergedConfig();
100
- if (!username || !jenkins_token) {
101
- errorExit(
102
- 'Get your Jenkins API token in https://ci.nodejs.org/me/configure ' +
103
- 'and run the following command to add it to your ncu config: ' +
104
- 'ncu-config --global set jenkins_token TOKEN'
105
- );
106
- };
107
- check(username, jenkins_token);
108
- result.jenkins = encode(username, jenkins_token);
109
- }
110
-
111
- if (options.h1) {
112
- const { h1_username, h1_token } = getMergedConfig();
113
- if (!h1_username || !h1_token) {
114
- errorExit(
115
- 'Get your HackerOne API token in ' +
116
- 'https://docs.hackerone.com/organizations/api-tokens.html ' +
117
- 'and run the following command to add it to your ncu config: ' +
118
- 'ncu-config --global set h1_token TOKEN or ' +
119
- 'ncu-config --global set h1_username USERNAME'
120
- );
121
- };
122
- result.h1 = encode(h1_username, h1_token);
123
- }
124
147
  return result;
125
148
  }
126
149
 
package/lib/config.js CHANGED
@@ -4,6 +4,7 @@ import os from 'node:os';
4
4
  import { readJson, writeJson } from './file.js';
5
5
  import { existsSync, mkdtempSync, rmSync } from 'node:fs';
6
6
  import { spawnSync } from 'node:child_process';
7
+ import { forceRunAsync, runSync } from './run.js';
7
8
 
8
9
  export const GLOBAL_CONFIG = Symbol('globalConfig');
9
10
  export const PROJECT_CONFIG = Symbol('projectConfig');
@@ -18,32 +19,95 @@ export function getNcurcPath() {
18
19
  }
19
20
  }
20
21
 
21
- export function getMergedConfig(dir, home) {
22
- const globalConfig = getConfig(GLOBAL_CONFIG, home);
23
- const projectConfig = getConfig(PROJECT_CONFIG, dir);
24
- const localConfig = getConfig(LOCAL_CONFIG, dir);
25
- return Object.assign(globalConfig, projectConfig, localConfig);
22
+ let mergedConfig;
23
+ export function getMergedConfig(dir, home, additional) {
24
+ if (mergedConfig == null) {
25
+ const globalConfig = getConfig(GLOBAL_CONFIG, home);
26
+ const projectConfig = getConfig(PROJECT_CONFIG, dir);
27
+ const localConfig = getConfig(LOCAL_CONFIG, dir);
28
+ mergedConfig = Object.assign(globalConfig, projectConfig, localConfig, additional);
29
+ }
30
+ return mergedConfig;
26
31
  };
32
+ export function clearCachedConfig() {
33
+ mergedConfig = null;
34
+ }
35
+
36
+ export async function encryptValue(input) {
37
+ console.warn('Spawning gpg to encrypt the config value');
38
+ return forceRunAsync(
39
+ process.env.GPG_BIN || 'gpg',
40
+ ['--default-recipient-self', '--encrypt', '--armor'],
41
+ {
42
+ captureStdout: true,
43
+ ignoreFailure: false,
44
+ input
45
+ }
46
+ );
47
+ }
48
+
49
+ function setOwnProperty(target, key, value) {
50
+ return Object.defineProperty(target, key, {
51
+ __proto__: null,
52
+ configurable: true,
53
+ enumerable: true,
54
+ value
55
+ });
56
+ }
57
+ function addEncryptedPropertyGetter(target, key, input) {
58
+ if (input?.startsWith?.('-----BEGIN PGP MESSAGE-----\n')) {
59
+ return Object.defineProperty(target, key, {
60
+ __proto__: null,
61
+ configurable: true,
62
+ get() {
63
+ // Using an error object to get a stack trace in debug mode.
64
+ const warn = new Error(
65
+ `The config value for ${key} is encrypted, spawning gpg to decrypt it...`
66
+ );
67
+ console.warn(setOwnProperty(warn, 'name', 'Warning'));
68
+ const value = runSync(process.env.GPG_BIN || 'gpg', ['--decrypt'], { input });
69
+ setOwnProperty(target, key, value);
70
+ return value;
71
+ },
72
+ set(newValue) {
73
+ if (!addEncryptedPropertyGetter(target, key, newValue)) {
74
+ throw new Error(
75
+ 'Refusing to override an encrypted value with a non-encrypted one. ' +
76
+ 'Please use an encrypted one, or delete the config key first.'
77
+ );
78
+ }
79
+ }
80
+ });
81
+ }
82
+ }
27
83
 
28
- export function getConfig(configType, dir) {
84
+ export function getConfig(configType, dir, raw = false) {
29
85
  const configPath = getConfigPath(configType, dir);
30
86
  const encryptedConfigPath = configPath + '.gpg';
31
87
  if (existsSync(encryptedConfigPath)) {
32
88
  console.warn('Encrypted config detected, spawning gpg to decrypt it...');
33
89
  const { status, stdout } =
34
- spawnSync('gpg', ['--decrypt', encryptedConfigPath]);
90
+ spawnSync(process.env.GPG_BIN || 'gpg', ['--decrypt', encryptedConfigPath]);
35
91
  if (status === 0) {
36
92
  return JSON.parse(stdout.toString('utf-8'));
37
93
  }
38
94
  }
39
95
  try {
40
- return readJson(configPath);
96
+ const json = readJson(configPath);
97
+ if (!raw) {
98
+ // Raw config means encrypted values are returned as is.
99
+ // Otherwise we install getters to decrypt them when accessed.
100
+ for (const [key, val] of Object.entries(json)) {
101
+ addEncryptedPropertyGetter(json, key, val);
102
+ }
103
+ }
104
+ return json;
41
105
  } catch (cause) {
42
106
  throw new Error('Unable to parse config file ' + configPath, { cause });
43
107
  }
44
108
  };
45
109
 
46
- export function getConfigPath(configType, dir) {
110
+ function getConfigPath(configType, dir) {
47
111
  switch (configType) {
48
112
  case GLOBAL_CONFIG:
49
113
  return getNcurcPath();
@@ -61,7 +125,7 @@ export function getConfigPath(configType, dir) {
61
125
  }
62
126
  };
63
127
 
64
- export function writeConfig(configType, obj, dir) {
128
+ function writeConfig(configType, obj, dir) {
65
129
  const configPath = getConfigPath(configType, dir);
66
130
  const encryptedConfigPath = configPath + '.gpg';
67
131
  if (existsSync(encryptedConfigPath)) {
@@ -69,7 +133,7 @@ export function writeConfig(configType, obj, dir) {
69
133
  const tmpFile = path.join(tmpDir, 'config.json');
70
134
  try {
71
135
  writeJson(tmpFile, obj);
72
- const { status } = spawnSync('gpg',
136
+ const { status } = spawnSync(process.env.GPG_BIN || 'gpg',
73
137
  ['--default-recipient-self', '--yes', '--encrypt', '--output', encryptedConfigPath, tmpFile]
74
138
  );
75
139
  if (status !== 0) {
@@ -85,7 +149,7 @@ export function writeConfig(configType, obj, dir) {
85
149
  };
86
150
 
87
151
  export function updateConfig(configType, obj, dir) {
88
- const config = getConfig(configType, dir);
152
+ const config = getConfig(configType, dir, true);
89
153
  writeConfig(configType, Object.assign(config, obj), dir);
90
154
  };
91
155
 
@@ -464,8 +464,8 @@ export default class LandingSession extends Session {
464
464
  const url = `https://github.com/${owner}/${repo}/pull/${prid}`;
465
465
  cli.log(`2. Post "Landed in ${willBeLanded}" in ${url}`);
466
466
  if (isGhAvailable()) {
467
- cli.log(` gh pr comment ${prid} --body "Landed in ${willBeLanded}"`);
468
- cli.log(` gh pr close ${prid}`);
467
+ cli.log(` gh pr comment ${url} --body "Landed in ${willBeLanded}"`);
468
+ cli.log(` gh pr close ${url}`);
469
469
  }
470
470
  }
471
471
 
package/lib/pr_checker.js CHANGED
@@ -19,7 +19,6 @@ const { FROM_COMMENT, FROM_REVIEW_COMMENT } = REVIEW_SOURCES;
19
19
 
20
20
  const SECOND = 1000;
21
21
  const MINUTE = SECOND * 60;
22
- const HOUR = MINUTE * 60;
23
22
 
24
23
  const WAIT_TIME_MULTI_APPROVAL = 24 * 2;
25
24
  const WAIT_TIME_SINGLE_APPROVAL = 24 * 7;
@@ -196,10 +195,18 @@ export default class PRChecker {
196
195
 
197
196
  const createTime = new Date(this.pr.createdAt);
198
197
  const msFromCreateTime = now.getTime() - createTime.getTime();
199
- const minutesFromCreateTime = Math.ceil(msFromCreateTime / MINUTE);
200
- const hoursFromCreateTime = Math.ceil(msFromCreateTime / HOUR);
201
- let timeLeftMulti = this.waitTimeMultiApproval - hoursFromCreateTime;
202
- const timeLeftSingle = this.waitTimeSingleApproval - hoursFromCreateTime;
198
+ const minutesFromCreateTime = msFromCreateTime / MINUTE;
199
+ const timeLeftMulti = this.waitTimeMultiApproval * 60 - minutesFromCreateTime;
200
+ const timeLeftSingle = this.waitTimeSingleApproval * 60 - minutesFromCreateTime;
201
+ const timeToText = (time, liaison_word = undefined) => {
202
+ let unity = 'minute';
203
+ if (time > 59) {
204
+ unity = 'hour';
205
+ time /= 60;
206
+ }
207
+ time = Math.ceil(time);
208
+ return `${time} ${liaison_word ? liaison_word + ' ' : ''}${unity}${time === 1 ? '' : 's'}`;
209
+ };
203
210
 
204
211
  if (approved.length >= 2) {
205
212
  if (isFastTracked || isCodeAndLearn) {
@@ -208,15 +215,9 @@ export default class PRChecker {
208
215
  if (timeLeftMulti < 0) {
209
216
  return true;
210
217
  }
211
- if (timeLeftMulti === 0) {
212
- const timeLeftMins =
213
- this.waitTimeMultiApproval * 60 - minutesFromCreateTime;
214
- cli.error(`This PR needs to wait ${timeLeftMins} ` +
215
- `more minutes to land${fastTrackAppendix}`);
216
- return false;
217
- }
218
- cli.error(`This PR needs to wait ${timeLeftMulti} more ` +
219
- `hours to land${fastTrackAppendix}`);
218
+ cli.error(
219
+ `This PR needs to wait ${timeToText(timeLeftMulti, 'more')} to land${fastTrackAppendix}`
220
+ );
220
221
  return false;
221
222
  }
222
223
 
@@ -224,10 +225,9 @@ export default class PRChecker {
224
225
  if (timeLeftSingle < 0) {
225
226
  return true;
226
227
  }
227
- timeLeftMulti = timeLeftMulti < 0 || isFastTracked ? 0 : timeLeftMulti;
228
- cli.error(`This PR needs to wait ${timeLeftSingle} more hours to land ` +
229
- `(or ${timeLeftMulti} hours if there is one more approval)` +
230
- fastTrackAppendix);
228
+ cli.error(`This PR needs to wait ${timeToText(timeLeftSingle, 'more')} to land (or ${
229
+ timeToText(timeLeftMulti < 0 || isFastTracked ? 0 : timeLeftMulti)
230
+ } if there is one more approval)${fastTrackAppendix}`);
231
231
  return false;
232
232
  }
233
233
  }
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { pipeline } from 'node:stream/promises';
3
5
  import semver from 'semver';
4
6
  import * as gst from 'git-secure-tag';
5
7
 
@@ -196,31 +198,44 @@ export default class ReleasePromotion extends Session {
196
198
 
197
199
  async verifyTagSignature(version) {
198
200
  const { cli } = this;
199
- const verifyTagPattern = /gpg:[^\n]+\ngpg:\s+using \w+ key ([^\n]+)\ngpg:\s+issuer "([^"]+)"\ngpg:\s+Good signature from (?:"[^"]+"(?: \[ultimate\])?\ngpg:\s+aka )*"([^<]+) <\2>"/;
200
- const [verifyTagOutput, haystack] = await Promise.all([forceRunAsync(
201
- 'git', ['--no-pager',
202
- 'verify-tag',
203
- `v${version}`
204
- ], { ignoreFailure: false, captureStderr: true }), fs.readFile('README.md')]);
205
- const match = verifyTagPattern.exec(verifyTagOutput);
206
- if (match == null) {
207
- cli.warn('git was not able to verify the tag:');
208
- cli.info(verifyTagOutput);
209
- } else {
210
- const [, keyID, email, name] = match;
211
- const needle = `* **${name}** <<${email}>>\n ${'`'}${keyID}${'`'}`;
212
- if (haystack.includes(needle)) {
213
- return;
201
+
202
+ cli.startSpinner('Downloading active releasers keyring from nodejs/release-keys...');
203
+ const [keyRingStream, [GNUPGHOME, keyRingFd]] = await Promise.all([
204
+ fetch('https://github.com/nodejs/release-keys/raw/HEAD/gpg-only-active-keys/pubring.kbx'),
205
+ fs.mkdtemp(path.join(tmpdir(), 'ncu-'))
206
+ .then(async d => [d, await fs.open(path.join(d, 'pubring.kbx'), 'w')]),
207
+ ]);
208
+ if (!keyRingStream.ok) throw new Error('Failed to download keyring', { cause: keyRingStream });
209
+ await pipeline(keyRingStream.body, keyRingFd.createWriteStream());
210
+ cli.stopSpinner('Active releasers keyring stored in temp directory');
211
+
212
+ try {
213
+ await forceRunAsync(
214
+ 'git', ['--no-pager',
215
+ 'verify-tag',
216
+ `v${version}`
217
+ ], {
218
+ ignoreFailure: false,
219
+ spawnArgs: { env: { ...process.env, GNUPGHOME } },
220
+ });
221
+ cli.ok('git tag signature verified');
222
+ } catch (cause) {
223
+ cli.error('git was not able to verify the tag');
224
+ cli.warn('This means that either the tag was signed with the wrong key,');
225
+ cli.warn('or that nodejs/release-keys contains outdated information.');
226
+ cli.warn('The release should not proceed.');
227
+ if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
228
+ if (await cli.prompt('Do you want to delete the local tag?')) {
229
+ await forceRunAsync('git', ['tag', '-d', `v${version}`]);
230
+ } else {
231
+ cli.info(`Run 'git tag -d v${version}' to remove the local tag.`);
232
+ }
233
+ throw new Error('Aborted', { cause });
214
234
  }
215
- cli.warn('Tag was signed with an undocumented identity/key pair!');
216
- cli.info('Expected to find the following entry in the README:');
217
- cli.info(needle);
218
- cli.info('If you are using a subkey, it might be OK.');
219
- }
220
- cli.info(`If that doesn't sound right, consider removing the tag (git tag -d v${version
221
- }), check your local config, and start the process over.`);
222
- if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
223
- throw new Error('Aborted');
235
+ } finally {
236
+ cli.startSpinner('Cleaning up temp files');
237
+ await fs.rm(GNUPGHOME, { force: true, recursive: true });
238
+ cli.stopSpinner('Temp files removed');
224
239
  }
225
240
  }
226
241