Skip to content

Commit a22e127

Browse files
authored
feat: add metadata JSON readiness output (#1113)
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 a22e127

6 files changed

Lines changed: 514 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: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,71 @@ 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.WAIT_TIME
26+
]);
27+
28+
export function classifyMetadataReadiness(ready, reasonCodes) {
29+
if (ready) {
30+
return METADATA_READINESS.READY;
31+
}
32+
33+
if (reasonCodes.length > 0 &&
34+
reasonCodes.every((code) => DEFERRABLE_REASON_CODES.has(code))) {
35+
return METADATA_READINESS.DEFERRABLE;
36+
}
37+
38+
return METADATA_READINESS.FAILED;
39+
}
40+
41+
export function getMetadataExitCode(readiness) {
42+
switch (readiness) {
43+
case METADATA_READINESS.READY:
44+
return METADATA_EXIT_CODES.READY;
45+
case METADATA_READINESS.DEFERRABLE:
46+
return METADATA_EXIT_CODES.DEFERRABLE;
47+
default:
48+
return METADATA_EXIT_CODES.FAILED;
49+
}
50+
}
51+
52+
export function formatMetadataResult({ status, data, metadata, checker }) {
53+
const reasonCodes = [...new Set(checker.reasons.map(({ code }) => code))];
54+
const readiness = classifyMetadataReadiness(status, reasonCodes);
55+
const exitCode = getMetadataExitCode(readiness);
56+
return {
57+
ready: status,
58+
readiness,
59+
exitCode,
60+
pullRequest: {
61+
owner: data.owner,
62+
repo: data.repo,
63+
number: data.prid,
64+
url: data.pr.url
65+
},
66+
metadata,
67+
reasonCodes,
68+
reasons: checker.reasons
69+
};
70+
}
71+
1072
export async function getMetadata(argv, skipRefs, cli) {
1173
const credentials = await auth({
1274
github: true,
@@ -22,7 +84,7 @@ export async function getMetadata(argv, skipRefs, cli) {
2284
summary.display();
2385

2486
const metadata = new MetadataGenerator({ skipRefs, ...data }).getMetadata();
25-
if (!process.stdout.isTTY) {
87+
if (!argv.json && !process.stdout.isTTY) {
2688
process.stdout.write(metadata);
2789
}
2890

@@ -33,17 +95,23 @@ export async function getMetadata(argv, skipRefs, cli) {
3395
cli.stopSpinner(`Done writing metadata to ${argv.file}`);
3496
}
3597

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

40104
const checker = new PRChecker(cli, data, request, argv);
41105
const status = await checker.checkAll(argv.checkComments, argv.checkCI);
42-
return {
106+
const result = {
43107
status,
44108
request,
45109
data,
46110
metadata,
47111
checker
48112
};
113+
return {
114+
...result,
115+
json: formatMetadataResult(result)
116+
};
49117
};

docs/git-node.md

Lines changed: 24 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,26 @@ $ 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 the deferrable metadata
338+
reason currently owned by the lightweight queue selector: `wait-time`
339+
- `40`: the pull request is not ready for a hard or mixed reason and should be
340+
handled by the existing landing/failure path
341+
342+
Exit codes `20`-`29` are reserved for future deferrable metadata states, and
343+
`40`-`49` are reserved for future hard metadata failure states. Unexpected tool
344+
or runtime failures continue to use exit code `1`.
345+
322346
<a id="git-node-metadata-optional-settings"></a>
323347

324348
### Optional Settings

0 commit comments

Comments
 (0)