@node-core/utils 4.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.
Files changed (98) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +158 -0
  3. package/bin/get-metadata.js +11 -0
  4. package/bin/git-node.js +30 -0
  5. package/bin/ncu-ci.js +600 -0
  6. package/bin/ncu-config.js +101 -0
  7. package/bin/ncu-team.js +76 -0
  8. package/components/git/backport.js +70 -0
  9. package/components/git/epilogue.js +18 -0
  10. package/components/git/land.js +223 -0
  11. package/components/git/metadata.js +94 -0
  12. package/components/git/release.js +99 -0
  13. package/components/git/security.js +35 -0
  14. package/components/git/status.js +32 -0
  15. package/components/git/sync.js +24 -0
  16. package/components/git/v8.js +121 -0
  17. package/components/git/vote.js +84 -0
  18. package/components/git/wpt.js +87 -0
  19. package/components/metadata.js +49 -0
  20. package/lib/auth.js +133 -0
  21. package/lib/backport_session.js +302 -0
  22. package/lib/cache.js +107 -0
  23. package/lib/cherry_pick.js +304 -0
  24. package/lib/ci/build-types/benchmark_run.js +72 -0
  25. package/lib/ci/build-types/citgm_build.js +194 -0
  26. package/lib/ci/build-types/citgm_comparison_build.js +174 -0
  27. package/lib/ci/build-types/commit_build.js +112 -0
  28. package/lib/ci/build-types/daily_build.js +24 -0
  29. package/lib/ci/build-types/fanned_build.js +87 -0
  30. package/lib/ci/build-types/health_build.js +63 -0
  31. package/lib/ci/build-types/job.js +114 -0
  32. package/lib/ci/build-types/linter_build.js +35 -0
  33. package/lib/ci/build-types/normal_build.js +89 -0
  34. package/lib/ci/build-types/pr_build.js +101 -0
  35. package/lib/ci/build-types/test_build.js +186 -0
  36. package/lib/ci/build-types/test_run.js +41 -0
  37. package/lib/ci/ci_failure_parser.js +325 -0
  38. package/lib/ci/ci_type_parser.js +203 -0
  39. package/lib/ci/ci_utils.js +106 -0
  40. package/lib/ci/failure_aggregator.js +152 -0
  41. package/lib/ci/jenkins_constants.js +28 -0
  42. package/lib/ci/run_ci.js +120 -0
  43. package/lib/cli.js +192 -0
  44. package/lib/collaborators.js +140 -0
  45. package/lib/config.js +72 -0
  46. package/lib/figures.js +7 -0
  47. package/lib/file.js +43 -0
  48. package/lib/github/templates/next-security-release.md +97 -0
  49. package/lib/github/tree.js +162 -0
  50. package/lib/landing_session.js +506 -0
  51. package/lib/links.js +123 -0
  52. package/lib/mergeable_state.js +3 -0
  53. package/lib/metadata_gen.js +61 -0
  54. package/lib/pr_checker.js +605 -0
  55. package/lib/pr_data.js +115 -0
  56. package/lib/pr_summary.js +62 -0
  57. package/lib/prepare_release.js +772 -0
  58. package/lib/prepare_security.js +117 -0
  59. package/lib/proxy.js +21 -0
  60. package/lib/queries/DefaultBranchRef.gql +8 -0
  61. package/lib/queries/LastCommit.gql +16 -0
  62. package/lib/queries/PR.gql +37 -0
  63. package/lib/queries/PRComments.gql +27 -0
  64. package/lib/queries/PRCommits.gql +45 -0
  65. package/lib/queries/PRs.gql +25 -0
  66. package/lib/queries/Reviews.gql +23 -0
  67. package/lib/queries/SearchIssue.gql +51 -0
  68. package/lib/queries/Team.gql +22 -0
  69. package/lib/queries/TreeEntries.gql +12 -0
  70. package/lib/queries/VotePRInfo.gql +28 -0
  71. package/lib/release/utils.js +53 -0
  72. package/lib/request.js +185 -0
  73. package/lib/review_state.js +5 -0
  74. package/lib/reviews.js +178 -0
  75. package/lib/run.js +106 -0
  76. package/lib/session.js +415 -0
  77. package/lib/sync_session.js +15 -0
  78. package/lib/team_info.js +95 -0
  79. package/lib/update-v8/applyNodeChanges.js +49 -0
  80. package/lib/update-v8/backport.js +258 -0
  81. package/lib/update-v8/commitUpdate.js +26 -0
  82. package/lib/update-v8/common.js +35 -0
  83. package/lib/update-v8/constants.js +86 -0
  84. package/lib/update-v8/index.js +56 -0
  85. package/lib/update-v8/majorUpdate.js +171 -0
  86. package/lib/update-v8/minorUpdate.js +105 -0
  87. package/lib/update-v8/updateMaintainingDependencies.js +34 -0
  88. package/lib/update-v8/updateV8Clone.js +53 -0
  89. package/lib/update-v8/updateVersionNumbers.js +122 -0
  90. package/lib/update-v8/util.js +62 -0
  91. package/lib/user.js +4 -0
  92. package/lib/user_status.js +5 -0
  93. package/lib/utils.js +66 -0
  94. package/lib/verbosity.js +26 -0
  95. package/lib/voting_session.js +136 -0
  96. package/lib/wpt/index.js +243 -0
  97. package/lib/wpt/templates/README.md +16 -0
  98. package/package.json +69 -0
@@ -0,0 +1,152 @@
1
+ import _ from 'lodash';
2
+ import chalk from 'chalk';
3
+
4
+ import { getMachineUrl, parsePRFromURL } from '../links.js';
5
+ import CIFailureParser from './ci_failure_parser.js';
6
+ import {
7
+ parseJobFromURL,
8
+ CI_TYPES
9
+ } from './ci_type_parser.js';
10
+ import {
11
+ fold,
12
+ getHighlight,
13
+ markdownRow
14
+ } from './ci_utils.js';
15
+
16
+ const { FAILURE_TYPES_NAME } = CIFailureParser;
17
+
18
+ export class FailureAggregator {
19
+ constructor(cli, data) {
20
+ this.cli = cli;
21
+ this.health = data[0];
22
+ this.failures = data.slice(1);
23
+ this.aggregates = null;
24
+ }
25
+
26
+ aggregate() {
27
+ const failures = this.failures;
28
+ const groupedByReason = _.chain(failures)
29
+ .groupBy(getHighlight)
30
+ .toPairs()
31
+ .sortBy(0)
32
+ .value();
33
+ const data = [];
34
+ for (const item of groupedByReason) {
35
+ const [reason, failures] = item;
36
+ // Uncomment this and redirect stderr away to see matched highlights
37
+ // console.log('HIGHLIGHT', reason);
38
+
39
+ // If multiple sub builds of one PR are failed by the same reason,
40
+ // we'll only take one of those builds, as that might be a genuine failure
41
+ const prs = _.chain(failures)
42
+ .uniqBy('source')
43
+ .sortBy((f) => parseJobFromURL(f.upstream).jobid)
44
+ .map((item) => ({ source: item.source, upstream: item.upstream }))
45
+ .value();
46
+ const machines = _.uniq(failures.map(f => f.builtOn));
47
+ data.push({
48
+ reason, type: failures[0].type, failures, prs, machines
49
+ });
50
+ };
51
+
52
+ const groupedByType = _.groupBy(data, 'type');
53
+ for (const type of Object.keys(groupedByType)) {
54
+ groupedByType[type] =
55
+ _.sortBy(groupedByType[type], r => 0 - (r.prs.length));
56
+ }
57
+ this.aggregates = groupedByType;
58
+ return groupedByType;
59
+ }
60
+
61
+ formatAsMarkdown() {
62
+ let { aggregates } = this;
63
+ if (!aggregates) {
64
+ aggregates = this.aggregates = this.aggregate();
65
+ }
66
+
67
+ const last = parseJobFromURL(this.failures[0].upstream);
68
+ const first = parseJobFromURL(
69
+ this.failures[this.failures.length - 1].upstream
70
+ );
71
+ const jobName = CI_TYPES.get(first.type).jobName;
72
+ let output = 'Failures in ';
73
+ output += `[${jobName}/${first.jobid}](${first.link}) to `;
74
+ output += `[${jobName}/${last.jobid}](${last.link}) `;
75
+ output += 'that failed 2 or more PRs\n';
76
+ output += '(Generated with `ncu-ci ';
77
+ output += `${process.argv.slice(2).join(' ')}\`)\n\n`;
78
+
79
+ output += this.health.formatAsMarkdown() + '\n';
80
+
81
+ const todo = [];
82
+ for (const type of Object.keys(aggregates)) {
83
+ if (aggregates[type].length === 0) {
84
+ continue;
85
+ }
86
+ output += `\n### ${FAILURE_TYPES_NAME[type]}\n\n`;
87
+ for (const item of aggregates[type]) {
88
+ const { reason, type, prs, failures, machines } = item;
89
+ if (prs.length < 2) { continue; }
90
+ todo.push({ count: prs.length, reason });
91
+ output += markdownRow('Reason', `<code>${reason}</code>`);
92
+ output += markdownRow('-', ':-');
93
+ output += markdownRow('Type', type);
94
+ const source = prs.map(f => f.source);
95
+ output += markdownRow(
96
+ 'Failed PR', `${source.length} (${source.join(', ')})`
97
+ );
98
+ output += markdownRow(
99
+ 'Appeared', machines.map(getMachineUrl).join(', ')
100
+ );
101
+ if (prs.length > 1) {
102
+ output += markdownRow('First CI', `${prs[0].upstream}`);
103
+ }
104
+ output += markdownRow('Last CI', `${prs[prs.length - 1].upstream}`);
105
+ output += '\n';
106
+ const example = failures[0].reason;
107
+ output += fold(
108
+ `<a href="${failures[0].url}">Example</a>`,
109
+ (example.length > 1024 ? example.slice(0, 1024) + '...' : example)
110
+ );
111
+ output += '\n\n-------\n\n';
112
+ }
113
+ }
114
+
115
+ output += '### Progress\n\n';
116
+ output += todo.map(
117
+ ({ count, reason }) => `- [ ] \`${reason}\` (${count})`).join('\n'
118
+ );
119
+ return output + '\n';
120
+ }
121
+
122
+ display() {
123
+ let { cli, aggregates } = this;
124
+ if (!aggregates) {
125
+ aggregates = this.aggregates = this.aggregate();
126
+ }
127
+
128
+ for (const type of Object.keys(aggregates)) {
129
+ cli.separator(type);
130
+ for (const item of aggregates[type]) {
131
+ const { reason, type, prs, failures, machines } = item;
132
+ cli.table('Reason', reason);
133
+ cli.table('Type', type);
134
+ const source = prs
135
+ .map(f => {
136
+ const parsed = parsePRFromURL(f.source);
137
+ return parsed ? `#${parsed.prid}` : f.source;
138
+ });
139
+ cli.table('Failed PR', `${source.length} (${source.join(', ')})`);
140
+ cli.table('Appeared', machines.join(', '));
141
+ if (prs.length > 1) {
142
+ cli.table('First CI', `${prs[0].upstream}`);
143
+ }
144
+ cli.table('Last CI', `${prs[prs.length - 1].upstream}`);
145
+ cli.log('\n' + chalk.bold('Example: ') + `${failures[0].url}\n`);
146
+ const example = failures[0].reason;
147
+ cli.log(example.length > 512 ? example.slice(0, 512) + '...' : example);
148
+ cli.separator();
149
+ }
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,28 @@
1
+ // com.tikal.jenkins.plugins.multijob.MultiJobBuild
2
+ const BUILD_FIELDS = 'builtOn,buildNumber,jobName,result,url';
3
+ const ACTION_TREE = 'actions[parameters[name,value]]';
4
+ const CHANGE_FIELDS = 'commitId,author[absoluteUrl,fullName],authorEmail,' +
5
+ 'msg,date';
6
+ const CHANGE_TREE = `changeSet[items[${CHANGE_FIELDS}]]`;
7
+ export const PR_TREE =
8
+ `result,url,number,${ACTION_TREE},${CHANGE_TREE},builtOn,` +
9
+ `subBuilds[${BUILD_FIELDS},build[subBuilds[${BUILD_FIELDS}]]]`;
10
+ export const COMMIT_TREE =
11
+ `result,url,number,${ACTION_TREE},${CHANGE_TREE},builtOn,` +
12
+ `subBuilds[${BUILD_FIELDS}]`;
13
+ export const CITGM_MAIN_TREE =
14
+ `result,url,number,${ACTION_TREE},${CHANGE_TREE},builtOn`;
15
+
16
+ export const FANNED_TREE =
17
+ `result,url,number,subBuilds[phaseName,${BUILD_FIELDS}]`;
18
+
19
+ // hudson.tasks.test.MatrixTestResult
20
+ const RESULT_TREE = 'result[suites[cases[name,status]]]';
21
+ export const CITGM_REPORT_TREE =
22
+ `failCount,skipCount,totalCount,childReports[child[url],${RESULT_TREE}]`;
23
+
24
+ // hudson.matrix.MatrixBuild
25
+ export const BUILD_TREE = 'result,runs[url,number,result],builtOn';
26
+ export const LINTER_TREE = 'result,url,number,builtOn';
27
+ const CAUSE_TREE = 'upstreamBuild,upstreamProject,shortDescription,_class';
28
+ export const RUN_TREE = `actions[causes[${CAUSE_TREE}]],builtOn`;
@@ -0,0 +1,120 @@
1
+ import { FormData } from 'undici';
2
+
3
+ import {
4
+ CI_DOMAIN,
5
+ CI_TYPES,
6
+ CI_TYPES_KEYS
7
+ } from './ci_type_parser.js';
8
+ import PRData from '../pr_data.js';
9
+ import { debuglog } from '../verbosity.js';
10
+
11
+ export const CI_CRUMB_URL = `https://${CI_DOMAIN}/crumbIssuer/api/json`;
12
+ const CI_PR_NAME = CI_TYPES.get(CI_TYPES_KEYS.PR).jobName;
13
+ export const CI_PR_URL = `https://${CI_DOMAIN}/job/${CI_PR_NAME}/build`;
14
+
15
+ const CI_V8_NAME = CI_TYPES.get(CI_TYPES_KEYS.V8).jobName;
16
+ export const CI_V8_URL = `https://${CI_DOMAIN}/job/${CI_V8_NAME}/build`;
17
+
18
+ export class RunPRJob {
19
+ constructor(cli, request, owner, repo, prid) {
20
+ this.cli = cli;
21
+ this.request = request;
22
+ this.owner = owner;
23
+ this.repo = repo;
24
+ this.prid = prid;
25
+ this.prData = new PRData({ prid, owner, repo }, cli, request);
26
+ }
27
+
28
+ async getCrumb() {
29
+ try {
30
+ const { crumb } = await this.request.json(CI_CRUMB_URL);
31
+ return crumb;
32
+ } catch (e) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ get payload() {
38
+ const payload = new FormData();
39
+ payload.append('json', JSON.stringify({
40
+ parameter: [
41
+ { name: 'CERTIFY_SAFE', value: 'on' },
42
+ { name: 'TARGET_GITHUB_ORG', value: this.owner },
43
+ { name: 'TARGET_REPO_NAME', value: this.repo },
44
+ { name: 'PR_ID', value: this.prid },
45
+ { name: 'REBASE_ONTO', value: '<pr base branch>' },
46
+ { name: 'DESCRIPTION_SETTER_DESCRIPTION', value: '' }
47
+ ]
48
+ }));
49
+ return payload;
50
+ }
51
+
52
+ get v8Payload() {
53
+ const payload = new FormData();
54
+ payload.append('json', JSON.stringify({
55
+ parameter: [
56
+ { name: 'GITHUB_ORG', value: this.owner },
57
+ { name: 'REPO_NAME', value: this.repo },
58
+ { name: 'GIT_REMOTE_REF', value: `refs/pull/${this.prid}/head` }
59
+ ]
60
+ }));
61
+ return payload;
62
+ }
63
+
64
+ async start() {
65
+ const { cli } = this;
66
+ cli.startSpinner('Validating Jenkins credentials');
67
+ const crumb = await this.getCrumb();
68
+
69
+ if (crumb === false) {
70
+ cli.stopSpinner('Jenkins credentials invalid',
71
+ this.cli.SPINNER_STATUS.FAILED);
72
+ return false;
73
+ }
74
+ cli.stopSpinner('Jenkins credentials valid');
75
+
76
+ try {
77
+ cli.startSpinner('Starting PR CI job');
78
+ const response = await this.request.fetch(CI_PR_URL, {
79
+ method: 'POST',
80
+ headers: {
81
+ 'Jenkins-Crumb': crumb
82
+ },
83
+ body: this.payload
84
+ });
85
+ if (response.status !== 201) {
86
+ cli.stopSpinner(
87
+ `Failed to start PR CI: ${response.status} ${response.statusText}`,
88
+ this.cli.SPINNER_STATUS.FAILED);
89
+ return false;
90
+ }
91
+ cli.stopSpinner('PR CI job successfully started');
92
+
93
+ // check if the job need a v8 build and trigger it
94
+ await this.prData.getPR();
95
+ const labels = this.prData.pr.labels;
96
+ if (labels.nodes.map(i => i.name).includes('v8 engine')) {
97
+ cli.startSpinner('Starting V8 CI job');
98
+ const response = await this.request.fetch(CI_V8_URL, {
99
+ method: 'POST',
100
+ headers: {
101
+ 'Jenkins-Crumb': crumb
102
+ },
103
+ body: this.v8Payload
104
+ });
105
+ if (response.status !== 201) {
106
+ cli.stopSpinner(
107
+ `Failed to start V8 CI: ${response.status} ${response.statusText}`,
108
+ this.cli.SPINNER_STATUS.FAILED);
109
+ return false;
110
+ }
111
+ cli.stopSpinner('V8 CI job successfully started');
112
+ }
113
+ } catch (err) {
114
+ debuglog(err);
115
+ cli.stopSpinner('Failed to start CI', this.cli.SPINNER_STATUS.FAILED);
116
+ return false;
117
+ }
118
+ return true;
119
+ }
120
+ }
package/lib/cli.js ADDED
@@ -0,0 +1,192 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+
5
+ import { warning, error, info, success } from './figures.js';
6
+
7
+ const SPINNER_STATUS = {
8
+ SUCCESS: 'success',
9
+ FAILED: 'failed',
10
+ WARN: 'warn',
11
+ INFO: 'info'
12
+ };
13
+
14
+ const { SUCCESS, FAILED, WARN, INFO } = SPINNER_STATUS;
15
+
16
+ const QUESTION_TYPE = {
17
+ INPUT: 'input',
18
+ NUMBER: 'number',
19
+ CONFIRM: 'confirm'
20
+ };
21
+
22
+ const formatter = new Intl.ListFormat('en', { type: 'disjunction' });
23
+
24
+ function head(text, length = 11) {
25
+ return chalk.bold(text.padEnd(length));
26
+ }
27
+
28
+ export default class CLI {
29
+ constructor(stream, options = {}) {
30
+ const spinnerOptions = options.spinner ?? {};
31
+ this.stream = stream || process.stderr;
32
+ this.spinner = ora({ stream: this.stream, ...spinnerOptions });
33
+ this.SPINNER_STATUS = SPINNER_STATUS;
34
+ this.QUESTION_TYPE = QUESTION_TYPE;
35
+ this.figureIndent = ' ';
36
+ this.assumeYes = false;
37
+ }
38
+
39
+ get eolIndent() {
40
+ return `\n${this.figureIndent}`;
41
+ }
42
+
43
+ setFigureIndent(indent) {
44
+ this.figureIndent = ' '.repeat(indent);
45
+ }
46
+
47
+ async prompt(question, opts = {
48
+ defaultAnswer: true,
49
+ noSeparator: false,
50
+ questionType: 'confirm'
51
+ }) {
52
+ if (!opts.noSeparator) {
53
+ this.separator();
54
+ }
55
+
56
+ const questionType = opts.questionType || QUESTION_TYPE.CONFIRM;
57
+ const availableTypes = Object.values(QUESTION_TYPE);
58
+ if (!availableTypes.includes(questionType)) {
59
+ throw new Error(
60
+ `${questionType} must be one of ${formatter.format(availableTypes)}`);
61
+ }
62
+
63
+ const defaultAnswer = (opts.defaultAnswer === undefined) ||
64
+ opts.defaultAnswer;
65
+ if (typeof defaultAnswer === 'boolean' &&
66
+ questionType !== QUESTION_TYPE.CONFIRM) {
67
+ throw new Error(
68
+ 'defaultAnswer must be provided for non-confirmation prompts');
69
+ }
70
+
71
+ const { isSpinning, text: spinningMessage } = this.spinner;
72
+
73
+ if (isSpinning) {
74
+ this.spinner.stop();
75
+ }
76
+
77
+ if (this.assumeYes) {
78
+ if (isSpinning) {
79
+ this.spinner.start(spinningMessage);
80
+ }
81
+ return defaultAnswer;
82
+ }
83
+
84
+ const { answer } = await inquirer.prompt([{
85
+ type: questionType,
86
+ name: 'answer',
87
+ message: question,
88
+ default: defaultAnswer
89
+ }]);
90
+
91
+ if (isSpinning) {
92
+ this.spinner.start(spinningMessage);
93
+ }
94
+
95
+ return answer;
96
+ }
97
+
98
+ setAssumeYes() {
99
+ this.assumeYes = true;
100
+ }
101
+
102
+ startSpinner(text) {
103
+ this.spinner.text = text;
104
+ this.spinner.start();
105
+ }
106
+
107
+ updateSpinner(text) {
108
+ this.spinner.text = text;
109
+ }
110
+
111
+ stopSpinner(rawText, status = SUCCESS) {
112
+ let symbol;
113
+ switch (status) {
114
+ case SUCCESS:
115
+ symbol = success;
116
+ break;
117
+ case FAILED:
118
+ symbol = error;
119
+ break;
120
+ case WARN:
121
+ symbol = warning;
122
+ break;
123
+ case INFO:
124
+ symbol = info;
125
+ }
126
+ const text = ' ' + rawText;
127
+ this.spinner.stopAndPersist({
128
+ symbol, text
129
+ });
130
+ }
131
+
132
+ write(text) {
133
+ this.stream.write(text);
134
+ }
135
+
136
+ log(text) {
137
+ this.write(text + '\n');
138
+ }
139
+
140
+ table(first, second = '', length = 11) {
141
+ this.log(head(first, length) + second);
142
+ }
143
+
144
+ separator(text = '', length = 80, sep = '-') {
145
+ if (!text) {
146
+ this.log(sep.repeat(length));
147
+ return;
148
+ }
149
+ const rest = (length - text.length - 2);
150
+ const half = sep.repeat(Math.abs(Math.floor(rest / 2)));
151
+ if (rest % 2 === 0) {
152
+ this.log(`${half} ${chalk.bold(text)} ${half}`);
153
+ } else {
154
+ this.log(`${half} ${chalk.bold(text)} ${sep}${half}`);
155
+ }
156
+ }
157
+
158
+ ok(text, options = {}) {
159
+ const prefix = options.newline ? this.eolIndent : this.figureIndent;
160
+ this.log(`${prefix}${success} ${text}`);
161
+ }
162
+
163
+ warn(text, options = {}) {
164
+ const prefix = options.newline ? this.eolIndent : this.figureIndent;
165
+ this.log(prefix + chalk.bold(`${warning} ${text}`));
166
+ }
167
+
168
+ info(text, options = {}) {
169
+ const prefix = options.newline ? this.eolIndent : this.figureIndent;
170
+ this.log(`${prefix}${info} ${text}`);
171
+ }
172
+
173
+ error(obj, options = {}) {
174
+ const prefix = options.newline ? this.eolIndent : this.figureIndent;
175
+ if (obj instanceof Error) {
176
+ this.log(`${prefix}${error} ${obj.message}`);
177
+ this.log(obj.stack);
178
+ if (obj.data) {
179
+ this.log(JSON.stringify(obj.data, null, 2));
180
+ }
181
+ } else {
182
+ this.log(prefix + chalk.bold(`${error} ${obj}`));
183
+ }
184
+ }
185
+
186
+ setExitCode(statusCode) {
187
+ process.exitCode = statusCode;
188
+ }
189
+ };
190
+
191
+ CLI.SPINNER_STATUS = SPINNER_STATUS;
192
+ CLI.QUESTION_TYPE = QUESTION_TYPE;
@@ -0,0 +1,140 @@
1
+ import fs from 'node:fs';
2
+
3
+ const TSC_TITLE = '#### TSC voting members';
4
+ const TSC_REGULAR_TITLE = '#### TSC regular members';
5
+ const TSCE_TITLE = '#### TSC emeriti members';
6
+ const CL_TITLE = '### Collaborators';
7
+ const CLE_TITLE = '### Collaborator emeriti';
8
+ const CONTACT_RE = /\* +\[(.+?)\]\(.+?\) +-\s+\*\*([^*]+?)\*\* +(?:&lt;|\\<|<<)([^>]+?)(?:&gt;|>)/mg;
9
+
10
+ const TSC = 'TSC';
11
+ const COLLABORATOR = 'COLLABORATOR';
12
+
13
+ export class Collaborator {
14
+ constructor(login, name, email, type) {
15
+ this.login = login; // This is not lowercased
16
+ this.name = name;
17
+ this.email = email;
18
+ this.type = type;
19
+ }
20
+
21
+ isActor(actor) {
22
+ if (!actor || !actor.login) { // ghost
23
+ return false;
24
+ }
25
+ return actor.login.toLowerCase() === this.login.toLowerCase();
26
+ }
27
+
28
+ isTSC() {
29
+ return this.type === TSC;
30
+ }
31
+
32
+ getName() {
33
+ return `${this.name} (@${this.login})`;
34
+ }
35
+
36
+ getContact() {
37
+ return `${this.name} <${this.email}>`;
38
+ }
39
+ }
40
+
41
+ Collaborator.TYPES = {
42
+ TSC, COLLABORATOR
43
+ };
44
+
45
+ export async function getCollaborators(cli, request, argv) {
46
+ const { readme, owner, repo } = argv;
47
+ let readmeText;
48
+ if (readme) {
49
+ cli.updateSpinner(`Reading collaborator contacts from ${readme}`);
50
+ readmeText = fs.readFileSync(readme, 'utf8');
51
+ } else {
52
+ cli.updateSpinner(
53
+ 'Getting collaborator contacts from README of nodejs/node');
54
+ const url = 'https://raw.githubusercontent.com/nodejs/node/HEAD/README.md';
55
+ readmeText = await request.text(url);
56
+ }
57
+
58
+ let collaborators;
59
+ try {
60
+ collaborators = parseCollaborators(readmeText, cli);
61
+ } catch (err) {
62
+ const readmePath = readme || `${owner}/${repo}/README.md`;
63
+ cli.stopSpinner(`Failed to get collaborator info from ${readmePath}`,
64
+ cli.SPINNER_STATUS.FAILED);
65
+ throw err;
66
+ }
67
+ return collaborators;
68
+ }
69
+
70
+ function parseCollaborators(readme, cli) {
71
+ // This is more or less taken from
72
+ // https://github.com/rvagg/archived-iojs-tools/blob/main/pr-metadata/pr-metadata.js
73
+ const collaborators = new Map();
74
+ let m;
75
+
76
+ const tscIndex = readme.toUpperCase().indexOf(TSC_TITLE.toUpperCase());
77
+ const tscrIndex = readme.toUpperCase().indexOf(TSC_REGULAR_TITLE.toUpperCase());
78
+ const tsceIndex = readme.toUpperCase().indexOf(TSCE_TITLE.toUpperCase());
79
+ const clIndex = readme.toUpperCase().indexOf(CL_TITLE.toUpperCase());
80
+ const cleIndex = readme.toUpperCase().indexOf(CLE_TITLE.toUpperCase());
81
+
82
+ if (tscIndex === -1) {
83
+ throw new Error(`Couldn't find ${TSC_TITLE} in the README`);
84
+ }
85
+ if (tscrIndex === -1) {
86
+ throw new Error(`Couldn't find ${TSC_REGULAR_TITLE} in the README`);
87
+ }
88
+ if (tsceIndex === -1) {
89
+ throw new Error(`Couldn't find ${TSCE_TITLE} in the README`);
90
+ }
91
+ if (clIndex === -1) {
92
+ throw new Error(`Couldn't find ${CL_TITLE} in the README`);
93
+ }
94
+ if (cleIndex === -1) {
95
+ throw new Error(`Couldn't find ${CLE_TITLE} in the README`);
96
+ }
97
+
98
+ if (!(tscIndex < tscrIndex &&
99
+ tscrIndex < tsceIndex &&
100
+ tsceIndex < clIndex &&
101
+ clIndex < cleIndex)) {
102
+ cli.warn('Contacts in the README is out of order, ' +
103
+ 'analysis could go wrong.', { newline: true });
104
+ }
105
+
106
+ // We also assume that TSC & TSC Emeriti are also listed as collaborators
107
+ CONTACT_RE.lastIndex = tscIndex;
108
+ // eslint-disable-next-line no-cond-assign
109
+ while ((m = CONTACT_RE.exec(readme)) && CONTACT_RE.lastIndex < tscrIndex) {
110
+ const login = m[1].toLowerCase();
111
+ const user = new Collaborator(m[1], m[2], m[3], TSC);
112
+ collaborators.set(login, user);
113
+ }
114
+
115
+ CONTACT_RE.lastIndex = clIndex;
116
+ // eslint-disable-next-line no-cond-assign
117
+ while ((m = CONTACT_RE.exec(readme)) &&
118
+ CONTACT_RE.lastIndex < cleIndex) {
119
+ const login = m[1].toLowerCase();
120
+ if (!collaborators.get(login)) {
121
+ const user = new Collaborator(m[1], m[2], m[3], COLLABORATOR);
122
+ collaborators.set(login, user);
123
+ }
124
+ }
125
+
126
+ if (!collaborators.size) {
127
+ throw new Error('Could not find any collaborators');
128
+ }
129
+
130
+ return collaborators;
131
+ }
132
+
133
+ /**
134
+ * @param {Map<string, Collaborator>} collaborators
135
+ * @param {{login?: string}} user
136
+ */
137
+ export function isCollaborator(collaborators, user) {
138
+ return (user && user.login && // could be a ghost
139
+ collaborators.get(user.login.toLowerCase()));
140
+ }
package/lib/config.js ADDED
@@ -0,0 +1,72 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+
4
+ import { readJson, writeJson } from './file.js';
5
+
6
+ export const GLOBAL_CONFIG = Symbol('globalConfig');
7
+ export const PROJECT_CONFIG = Symbol('projectConfig');
8
+ export const LOCAL_CONFIG = Symbol('localConfig');
9
+
10
+ export function getNcurcPath() {
11
+ if (process.env.XDG_CONFIG_HOME !== 'undefined' &&
12
+ process.env.XDG_CONFIG_HOME !== undefined) {
13
+ return path.join(process.env.XDG_CONFIG_HOME, 'ncurc');
14
+ } else {
15
+ return path.join(os.homedir(), '.ncurc');
16
+ }
17
+ }
18
+
19
+ export function getMergedConfig(dir, home) {
20
+ const globalConfig = getConfig(GLOBAL_CONFIG, home);
21
+ const projectConfig = getConfig(PROJECT_CONFIG, dir);
22
+ const localConfig = getConfig(LOCAL_CONFIG, dir);
23
+ return Object.assign(globalConfig, projectConfig, localConfig);
24
+ };
25
+
26
+ export function getConfig(configType, dir) {
27
+ const configPath = getConfigPath(configType, dir);
28
+ try {
29
+ return readJson(configPath);
30
+ } catch (cause) {
31
+ throw new Error('Unable to parse config file ' + configPath, { cause });
32
+ }
33
+ };
34
+
35
+ export function getConfigPath(configType, dir) {
36
+ switch (configType) {
37
+ case GLOBAL_CONFIG:
38
+ return getNcurcPath();
39
+ case PROJECT_CONFIG: {
40
+ const projectRcPath = path.join(dir || process.cwd(), '.ncurc');
41
+ return projectRcPath;
42
+ }
43
+ case LOCAL_CONFIG: {
44
+ const ncuDir = getNcuDir(dir);
45
+ const configPath = path.join(ncuDir, 'config');
46
+ return configPath;
47
+ }
48
+ default:
49
+ throw Error('Invalid configType');
50
+ }
51
+ };
52
+
53
+ export function writeConfig(configType, obj, dir) {
54
+ writeJson(getConfigPath(configType, dir), obj);
55
+ };
56
+
57
+ export function updateConfig(configType, obj, dir) {
58
+ const config = getConfig(configType, dir);
59
+ const configPath = getConfigPath(configType, dir);
60
+ writeJson(configPath, Object.assign(config, obj));
61
+ };
62
+
63
+ export function getHomeDir(home) {
64
+ if (process.env.XDG_CONFIG_HOME) {
65
+ return process.env.XDG_CONFIG_HOME;
66
+ }
67
+ return home || os.homedir();
68
+ };
69
+
70
+ export function getNcuDir(dir) {
71
+ return path.join(dir || process.cwd(), '.ncu');
72
+ };