Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion components/git/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const options = {
describe: 'Check for \'LGTM\' in comments',
type: 'boolean'
},
json: {
describe: 'Print metadata and PR readiness result as JSON',
type: 'boolean'
},
'max-commits': {
describe: 'Number of commits to warn',
type: 'number',
Expand All @@ -45,6 +49,29 @@ const options = {

let yargsInstance;

function writeStdout(text) {
return new Promise((resolve, reject) => {
process.stdout.write(text, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}

export async function writeMetadataJsonResult(metadataPromise) {
try {
const { json } = await metadataPromise;
await writeStdout(`${JSON.stringify(json, null, 2)}\n`);
process.exitCode = json.exitCode;
} catch (error) {
console.error(error);
process.exitCode = 1;
}
}

export function builder(yargs) {
yargsInstance = yargs;
return yargs
Expand Down Expand Up @@ -81,10 +108,16 @@ export function handler(argv) {
return yargsInstance.showHelp();
}

const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
const logStream = argv.json || !process.stdout.isTTY
? process.stderr
: process.stdout;
const cli = new CLI(logStream);

const merged = Object.assign({}, argv, parsed, config);
if (argv.json) {
return writeMetadataJsonResult(getMetadata(merged, false, cli));
}

return runPromise(getMetadata(merged, false, cli)
.then(({ status }) => {
if (status === false) {
Expand Down
80 changes: 74 additions & 6 deletions components/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,71 @@ import Request from '../lib/request.js';
import auth from '../lib/auth.js';
import PRData from '../lib/pr_data.js';
import PRSummary from '../lib/pr_summary.js';
import PRChecker from '../lib/pr_checker.js';
import PRChecker, {
PR_CHECK_REASON_CODES
} from '../lib/pr_checker.js';
import MetadataGenerator from '../lib/metadata_gen.js';

export const METADATA_READINESS = Object.freeze({
READY: 'ready',
DEFERRABLE: 'deferrable',
FAILED: 'failed'
});

export const METADATA_EXIT_CODES = Object.freeze({
READY: 0,
DEFERRABLE: 20,
FAILED: 40
});

const DEFERRABLE_REASON_CODES = new Set([
PR_CHECK_REASON_CODES.WAIT_TIME
]);

export function classifyMetadataReadiness(ready, reasonCodes) {
if (ready) {
return METADATA_READINESS.READY;
}

if (reasonCodes.length > 0 &&
reasonCodes.every((code) => DEFERRABLE_REASON_CODES.has(code))) {
return METADATA_READINESS.DEFERRABLE;
}

return METADATA_READINESS.FAILED;
}

export function getMetadataExitCode(readiness) {
switch (readiness) {
case METADATA_READINESS.READY:
return METADATA_EXIT_CODES.READY;
case METADATA_READINESS.DEFERRABLE:
return METADATA_EXIT_CODES.DEFERRABLE;
default:
return METADATA_EXIT_CODES.FAILED;
}
}

export function formatMetadataResult({ status, data, metadata, checker }) {
const reasonCodes = [...new Set(checker.reasons.map(({ code }) => code))];
const readiness = classifyMetadataReadiness(status, reasonCodes);
const exitCode = getMetadataExitCode(readiness);
return {
ready: status,
readiness,
exitCode,
pullRequest: {
owner: data.owner,
repo: data.repo,
number: data.prid,
url: data.pr.url
},
metadata,
reasonCodes,
reasons: checker.reasons
};
}

export async function getMetadata(argv, skipRefs, cli) {
const credentials = await auth({
github: true,
Expand All @@ -22,7 +84,7 @@ export async function getMetadata(argv, skipRefs, cli) {
summary.display();

const metadata = new MetadataGenerator({ skipRefs, ...data }).getMetadata();
if (!process.stdout.isTTY) {
if (!argv.json && !process.stdout.isTTY) {
process.stdout.write(metadata);
}

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

cli.separator('Generated metadata');
cli.write(metadata);
cli.separator();
if (!argv.json) {
cli.separator('Generated metadata');
cli.write(metadata);
cli.separator();
}

const checker = new PRChecker(cli, data, request, argv);
const status = await checker.checkAll(argv.checkComments, argv.checkCI);
return {
const result = {
status,
request,
data,
metadata,
checker
};
return {
...result,
json: formatMetadataResult(result)
};
};
24 changes: 24 additions & 0 deletions docs/git-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ Options:
--file, -f File to write the metadata in [string]
--readme Path to file that contains collaborator contacts [string]
--check-comments Check for 'LGTM' in comments [boolean]
--json Print metadata and PR readiness result as JSON [boolean]
--max-commits Number of commits to warn [number] [default: 3]
```

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

# Or, get machine-readable metadata and readiness reason codes
$ git node metadata $PRID --json

# Using it to amend commit messages:
$ git node metadata $PRID -f msg.txt
$ echo -e "$(git show -s --format=%B)\n\n$(cat msg.txt)" > msg.txt
Expand All @@ -319,6 +323,26 @@ $ git commit --amend -F msg.txt
git node metadata 167 --repo llnode --readme ../node/README.md
```

When `--json` is used, stdout contains a JSON object and progress/check output
is written to stderr. The command still exits non-zero when the pull request is
not ready to land. The JSON includes `ready`, `readiness`, `exitCode`,
`metadata`, `reasonCodes`, and `reasons`. `reasonCodes` is a de-duplicated list
of stable machine-readable codes such as `missing-approval`,
`missing-tsc-approval`, `wait-time`, `missing-github-ci`, `pending-github-ci`,
`conflict`, `requested-changes`, and `stale-review`.

The metadata JSON exit-code contract is:

- `0`: the pull request is ready
- `20`: the pull request is not ready, but only for the deferrable metadata
reason currently owned by the lightweight queue selector: `wait-time`
- `40`: the pull request is not ready for a hard or mixed reason and should be
handled by the existing landing/failure path

Exit codes `20`-`29` are reserved for future deferrable metadata states, and
`40`-`49` are reserved for future hard metadata failure states. Unexpected tool
or runtime failures continue to use exit code `1`.

<a id="git-node-metadata-optional-settings"></a>

### Optional Settings
Expand Down
Loading
Loading