@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.
- package/LICENSE +7 -0
- package/README.md +158 -0
- package/bin/get-metadata.js +11 -0
- package/bin/git-node.js +30 -0
- package/bin/ncu-ci.js +600 -0
- package/bin/ncu-config.js +101 -0
- package/bin/ncu-team.js +76 -0
- package/components/git/backport.js +70 -0
- package/components/git/epilogue.js +18 -0
- package/components/git/land.js +223 -0
- package/components/git/metadata.js +94 -0
- package/components/git/release.js +99 -0
- package/components/git/security.js +35 -0
- package/components/git/status.js +32 -0
- package/components/git/sync.js +24 -0
- package/components/git/v8.js +121 -0
- package/components/git/vote.js +84 -0
- package/components/git/wpt.js +87 -0
- package/components/metadata.js +49 -0
- package/lib/auth.js +133 -0
- package/lib/backport_session.js +302 -0
- package/lib/cache.js +107 -0
- package/lib/cherry_pick.js +304 -0
- package/lib/ci/build-types/benchmark_run.js +72 -0
- package/lib/ci/build-types/citgm_build.js +194 -0
- package/lib/ci/build-types/citgm_comparison_build.js +174 -0
- package/lib/ci/build-types/commit_build.js +112 -0
- package/lib/ci/build-types/daily_build.js +24 -0
- package/lib/ci/build-types/fanned_build.js +87 -0
- package/lib/ci/build-types/health_build.js +63 -0
- package/lib/ci/build-types/job.js +114 -0
- package/lib/ci/build-types/linter_build.js +35 -0
- package/lib/ci/build-types/normal_build.js +89 -0
- package/lib/ci/build-types/pr_build.js +101 -0
- package/lib/ci/build-types/test_build.js +186 -0
- package/lib/ci/build-types/test_run.js +41 -0
- package/lib/ci/ci_failure_parser.js +325 -0
- package/lib/ci/ci_type_parser.js +203 -0
- package/lib/ci/ci_utils.js +106 -0
- package/lib/ci/failure_aggregator.js +152 -0
- package/lib/ci/jenkins_constants.js +28 -0
- package/lib/ci/run_ci.js +120 -0
- package/lib/cli.js +192 -0
- package/lib/collaborators.js +140 -0
- package/lib/config.js +72 -0
- package/lib/figures.js +7 -0
- package/lib/file.js +43 -0
- package/lib/github/templates/next-security-release.md +97 -0
- package/lib/github/tree.js +162 -0
- package/lib/landing_session.js +506 -0
- package/lib/links.js +123 -0
- package/lib/mergeable_state.js +3 -0
- package/lib/metadata_gen.js +61 -0
- package/lib/pr_checker.js +605 -0
- package/lib/pr_data.js +115 -0
- package/lib/pr_summary.js +62 -0
- package/lib/prepare_release.js +772 -0
- package/lib/prepare_security.js +117 -0
- package/lib/proxy.js +21 -0
- package/lib/queries/DefaultBranchRef.gql +8 -0
- package/lib/queries/LastCommit.gql +16 -0
- package/lib/queries/PR.gql +37 -0
- package/lib/queries/PRComments.gql +27 -0
- package/lib/queries/PRCommits.gql +45 -0
- package/lib/queries/PRs.gql +25 -0
- package/lib/queries/Reviews.gql +23 -0
- package/lib/queries/SearchIssue.gql +51 -0
- package/lib/queries/Team.gql +22 -0
- package/lib/queries/TreeEntries.gql +12 -0
- package/lib/queries/VotePRInfo.gql +28 -0
- package/lib/release/utils.js +53 -0
- package/lib/request.js +185 -0
- package/lib/review_state.js +5 -0
- package/lib/reviews.js +178 -0
- package/lib/run.js +106 -0
- package/lib/session.js +415 -0
- package/lib/sync_session.js +15 -0
- package/lib/team_info.js +95 -0
- package/lib/update-v8/applyNodeChanges.js +49 -0
- package/lib/update-v8/backport.js +258 -0
- package/lib/update-v8/commitUpdate.js +26 -0
- package/lib/update-v8/common.js +35 -0
- package/lib/update-v8/constants.js +86 -0
- package/lib/update-v8/index.js +56 -0
- package/lib/update-v8/majorUpdate.js +171 -0
- package/lib/update-v8/minorUpdate.js +105 -0
- package/lib/update-v8/updateMaintainingDependencies.js +34 -0
- package/lib/update-v8/updateV8Clone.js +53 -0
- package/lib/update-v8/updateVersionNumbers.js +122 -0
- package/lib/update-v8/util.js +62 -0
- package/lib/user.js +4 -0
- package/lib/user_status.js +5 -0
- package/lib/utils.js +66 -0
- package/lib/verbosity.js +26 -0
- package/lib/voting_session.js +136 -0
- package/lib/wpt/index.js +243 -0
- package/lib/wpt/templates/README.md +16 -0
- 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`;
|
package/lib/ci/run_ci.js
ADDED
|
@@ -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+\*\*([^*]+?)\*\* +(?:<|\\<|<<)([^>]+?)(?:>|>)/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
|
+
};
|