Skip to content

Commit 7949e79

Browse files
committed
tools: add metadata JSON readiness output
Expose stable PR readiness reason codes from PRChecker and add git node metadata --json for machine-readable metadata readiness results. The JSON output includes readiness classification, reserved exit-code categories, generated metadata, unique reason codes, and detailed reasons. Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 9250fba commit 7949e79

6 files changed

Lines changed: 495 additions & 39 deletions

File tree

components/git/metadata.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ const options = {
3636
describe: 'Check for \'LGTM\' in comments',
3737
type: 'boolean'
3838
},
39+
json: {
40+
describe: 'Print metadata and PR readiness result as JSON',
41+
type: 'boolean'
42+
},
3943
'max-commits': {
4044
describe: 'Number of commits to warn',
4145
type: 'number',
@@ -45,6 +49,29 @@ const options = {
4549

4650
let yargsInstance;
4751

52+
function writeStdout(text) {
53+
return new Promise((resolve, reject) => {
54+
process.stdout.write(text, (error) => {
55+
if (error) {
56+
reject(error);
57+
} else {
58+
resolve();
59+
}
60+
});
61+
});
62+
}
63+
64+
export async function writeMetadataJsonResult(metadataPromise) {
65+
try {
66+
const { json } = await metadataPromise;
67+
await writeStdout(`${JSON.stringify(json, null, 2)}\n`);
68+
process.exitCode = json.exitCode;
69+
} catch (error) {
70+
console.error(error);
71+
process.exitCode = 1;
72+
}
73+
}
74+
4875
export function builder(yargs) {
4976
yargsInstance = yargs;
5077
return yargs
@@ -81,10 +108,16 @@ export function handler(argv) {
81108
return yargsInstance.showHelp();
82109
}
83110

84-
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
111+
const logStream = argv.json || !process.stdout.isTTY
112+
? process.stderr
113+
: process.stdout;
85114
const cli = new CLI(logStream);
86115

87116
const merged = Object.assign({}, argv, parsed, config);
117+
if (argv.json) {
118+
return writeMetadataJsonResult(getMetadata(merged, false, cli));
119+
}
120+
88121
return runPromise(getMetadata(merged, false, cli)
89122
.then(({ status }) => {
90123
if (status === false) {

components/metadata.js

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,73 @@ import Request from '../lib/request.js';
44
import auth from '../lib/auth.js';
55
import PRData from '../lib/pr_data.js';
66
import PRSummary from '../lib/pr_summary.js';
7-
import PRChecker from '../lib/pr_checker.js';
7+
import PRChecker, {
8+
PR_CHECK_REASON_CODES
9+
} from '../lib/pr_checker.js';
810
import MetadataGenerator from '../lib/metadata_gen.js';
911

12+
export const METADATA_READINESS = Object.freeze({
13+
READY: 'ready',
14+
DEFERRABLE: 'deferrable',
15+
FAILED: 'failed'
16+
});
17+
18+
export const METADATA_EXIT_CODES = Object.freeze({
19+
READY: 0,
20+
DEFERRABLE: 20,
21+
FAILED: 40
22+
});
23+
24+
const DEFERRABLE_REASON_CODES = new Set([
25+
PR_CHECK_REASON_CODES.MISSING_APPROVAL,
26+
PR_CHECK_REASON_CODES.MISSING_TSC_APPROVAL,
27+
PR_CHECK_REASON_CODES.WAIT_TIME
28+
]);
29+
30+
export function classifyMetadataReadiness(ready, reasonCodes) {
31+
if (ready) {
32+
return METADATA_READINESS.READY;
33+
}
34+
35+
if (reasonCodes.length > 0 &&
36+
reasonCodes.every((code) => DEFERRABLE_REASON_CODES.has(code))) {
37+
return METADATA_READINESS.DEFERRABLE;
38+
}
39+
40+
return METADATA_READINESS.FAILED;
41+
}
42+
43+
export function getMetadataExitCode(readiness) {
44+
switch (readiness) {
45+
case METADATA_READINESS.READY:
46+
return METADATA_EXIT_CODES.READY;
47+
case METADATA_READINESS.DEFERRABLE:
48+
return METADATA_EXIT_CODES.DEFERRABLE;
49+
default:
50+
return METADATA_EXIT_CODES.FAILED;
51+
}
52+
}
53+
54+
export function formatMetadataResult({ status, data, metadata, checker }) {
55+
const reasonCodes = [...new Set(checker.reasons.map(({ code }) => code))];
56+
const readiness = classifyMetadataReadiness(status, reasonCodes);
57+
const exitCode = getMetadataExitCode(readiness);
58+
return {
59+
ready: status,
60+
readiness,
61+
exitCode,
62+
pullRequest: {
63+
owner: data.owner,
64+
repo: data.repo,
65+
number: data.prid,
66+
url: data.pr.url
67+
},
68+
metadata,
69+
reasonCodes,
70+
reasons: checker.reasons
71+
};
72+
}
73+
1074
export async function getMetadata(argv, skipRefs, cli) {
1175
const credentials = await auth({
1276
github: true,
@@ -22,7 +86,7 @@ export async function getMetadata(argv, skipRefs, cli) {
2286
summary.display();
2387

2488
const metadata = new MetadataGenerator({ skipRefs, ...data }).getMetadata();
25-
if (!process.stdout.isTTY) {
89+
if (!argv.json && !process.stdout.isTTY) {
2690
process.stdout.write(metadata);
2791
}
2892

@@ -33,17 +97,23 @@ export async function getMetadata(argv, skipRefs, cli) {
3397
cli.stopSpinner(`Done writing metadata to ${argv.file}`);
3498
}
3599

36-
cli.separator('Generated metadata');
37-
cli.write(metadata);
38-
cli.separator();
100+
if (!argv.json) {
101+
cli.separator('Generated metadata');
102+
cli.write(metadata);
103+
cli.separator();
104+
}
39105

40106
const checker = new PRChecker(cli, data, request, argv);
41107
const status = await checker.checkAll(argv.checkComments, argv.checkCI);
42-
return {
108+
const result = {
43109
status,
44110
request,
45111
data,
46112
metadata,
47113
checker
48114
};
115+
return {
116+
...result,
117+
json: formatMetadataResult(result)
118+
};
49119
};

docs/git-node.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ Options:
291291
--file, -f File to write the metadata in [string]
292292
--readme Path to file that contains collaborator contacts [string]
293293
--check-comments Check for 'LGTM' in comments [boolean]
294+
--json Print metadata and PR readiness result as JSON [boolean]
294295
--max-commits Number of commits to warn [number] [default: 3]
295296
```
296297

@@ -309,6 +310,9 @@ $ git node metadata $PRID -o nodejs -r node
309310
# Or, redirect the metadata to a file while see the checks in stderr
310311
$ git node metadata $PRID > msg.txt
311312

313+
# Or, get machine-readable metadata and readiness reason codes
314+
$ git node metadata $PRID --json
315+
312316
# Using it to amend commit messages:
313317
$ git node metadata $PRID -f msg.txt
314318
$ echo -e "$(git show -s --format=%B)\n\n$(cat msg.txt)" > msg.txt
@@ -319,6 +323,27 @@ $ git commit --amend -F msg.txt
319323
git node metadata 167 --repo llnode --readme ../node/README.md
320324
```
321325

326+
When `--json` is used, stdout contains a JSON object and progress/check output
327+
is written to stderr. The command still exits non-zero when the pull request is
328+
not ready to land. The JSON includes `ready`, `readiness`, `exitCode`,
329+
`metadata`, `reasonCodes`, and `reasons`. `reasonCodes` is a de-duplicated list
330+
of stable machine-readable codes such as `missing-approval`,
331+
`missing-tsc-approval`, `wait-time`, `missing-github-ci`, `pending-github-ci`,
332+
`conflict`, `requested-changes`, and `stale-review`.
333+
334+
The metadata JSON exit-code contract is:
335+
336+
- `0`: the pull request is ready
337+
- `20`: the pull request is not ready, but only for deferrable metadata reasons
338+
currently owned by the lightweight queue selector: `missing-approval`,
339+
`missing-tsc-approval`, and `wait-time`
340+
- `40`: the pull request is not ready for a hard or mixed reason and should be
341+
handled by the existing landing/failure path
342+
343+
Exit codes `20`-`29` are reserved for future deferrable metadata states, and
344+
`40`-`49` are reserved for future hard metadata failure states. Unexpected tool
345+
or runtime failures continue to use exit code `1`.
346+
322347
<a id="git-node-metadata-optional-settings"></a>
323348

324349
### Optional Settings

0 commit comments

Comments
 (0)