@apex-actions/cli 0.98.0 → 0.99.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.
@@ -173,6 +173,7 @@ export const REFERENCE = {
173
173
  examples: [
174
174
  { command: 'apex why 4812', description: 'Why a run happened: the event, the workflow and the rule that matched.' },
175
175
  { command: 'apex why --delivery 7c1e0a52-3b9f-11ef-8e6b-2f4d1c9a0b77', description: 'Why a webhook produced no run at all.' },
176
+ { command: 'apex why --repo acme/web --pr 168', description: 'Why a pull request has no checks: the newest delivery about it that started nothing.' },
176
177
  ],
177
178
  },
178
179
  reproduce: {
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { explainWhy } from '../../src/application/explain-runs.js';
4
4
  import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
5
- import { jobMarker, retryNote, verdictLine, worthShowing } from '../../src/domain/provenance.js';
5
+ import { jobMarker, quietHeadline, retryNote, verdictLine, worthShowing } from '../../src/domain/provenance.js';
6
6
  import { parseRepository, unknownRunMessage } from '../../src/domain/runs.js';
7
7
  const printRun = (e) => {
8
8
  const { run, repository, trigger } = e;
@@ -36,8 +36,9 @@ const printRun = (e) => {
36
36
  const printDelivery = (e) => {
37
37
  const { delivery } = e;
38
38
  const action = delivery.action === undefined ? '' : `.${delivery.action}`;
39
- console.log(chalk.bold(`Delivery ${delivery.deliveryId}`) + chalk.dim(` · ${delivery.event}${action} · ${delivery.status}`));
40
- console.log(` ${e.summary}`);
39
+ const pr = delivery.pullRequest === undefined ? '' : ` · #${delivery.pullRequest}`;
40
+ console.log(chalk.bold(`Delivery ${delivery.deliveryId}`) + chalk.dim(` · ${delivery.event}${action}${pr} · ${delivery.status}`));
41
+ console.log(` ${quietHeadline(e)}`);
41
42
  if (e.verdicts.length > 0) {
42
43
  console.log(chalk.bold('\nWorkflows considered'));
43
44
  for (const t of e.verdicts) {
@@ -53,6 +54,12 @@ const handler = async (run, options) => {
53
54
  process.exitCode = 2;
54
55
  return;
55
56
  }
57
+ if (options.pr !== undefined && !/^[1-9][0-9]{0,8}$/.test(options.pr)) {
58
+ console.error(chalk.red(`--pr must be a pull request number, got ${options.pr}`));
59
+ process.exitCode = 2;
60
+ return;
61
+ }
62
+ const pullRequest = options.pr === undefined ? undefined : Number(options.pr);
56
63
  let api;
57
64
  try {
58
65
  api = new HttpControlPlane();
@@ -64,7 +71,7 @@ const handler = async (run, options) => {
64
71
  }
65
72
  let outcome;
66
73
  try {
67
- outcome = await explainWhy(api, { run: run ?? 'latest', repository, delivery: options.delivery });
74
+ outcome = await explainWhy(api, { run: run ?? 'latest', repository, delivery: options.delivery, pullRequest, nothing: options.nothing });
68
75
  }
69
76
  catch (err) {
70
77
  console.error(chalk.red(err instanceof Error ? err.message : String(err)));
@@ -84,6 +91,16 @@ const handler = async (run, options) => {
84
91
  console.error(chalk.red(`no delivery ${outcome.selector} is stored`));
85
92
  process.exitCode = 2;
86
93
  return;
94
+ case 'needs-repository':
95
+ console.error(chalk.red('--pr and --nothing need --repo <owner/repo>'));
96
+ process.exitCode = 2;
97
+ return;
98
+ case 'no-quiet-delivery':
99
+ console.error(chalk.yellow(outcome.pullRequest === undefined
100
+ ? 'every stored delivery for this repository started a run'
101
+ : `no stored delivery about #${outcome.pullRequest} started nothing — if it has no checks, GitHub may not have sent one`));
102
+ process.exitCode = 2;
103
+ return;
87
104
  case 'run':
88
105
  if (options.json === true)
89
106
  console.log(JSON.stringify(outcome.explanation, null, 2));
@@ -102,5 +119,7 @@ export default new Command('why')
102
119
  .argument('[run]', 'run id, #number, or "latest"', 'latest')
103
120
  .option('--repo <owner/repo>', 'limit the run search to one repository')
104
121
  .option('--delivery <id>', 'explain a webhook delivery instead of a run')
122
+ .option('--pr <number>', "with --repo, explain the newest delivery about this pull request that started no run")
123
+ .option('--nothing', "with --repo, explain the repository's newest delivery that started no run")
105
124
  .option('--json', 'print the explanation as JSON')
106
125
  .action(handler);
@@ -11,6 +11,14 @@ export const explainWhy = async (api, request) => {
11
11
  const explanation = await api.explainDelivery(request.delivery);
12
12
  return explanation === undefined ? { kind: 'unknown-delivery', selector: request.delivery } : { kind: 'delivery', explanation };
13
13
  }
14
+ // A pull request with no checks has no run to start from, and its delivery id is in nobody's hands: the
15
+ // repository's activity names the newest delivery that decided nothing, and why (BUG-0559).
16
+ if (request.pullRequest !== undefined || request.nothing === true) {
17
+ if (request.repository === undefined)
18
+ return { kind: 'needs-repository' };
19
+ const explanation = await api.newestQuietDelivery(request.repository, request.pullRequest);
20
+ return explanation === undefined ? { kind: 'no-quiet-delivery', pullRequest: request.pullRequest } : { kind: 'delivery', explanation };
21
+ }
14
22
  const resolved = await resolveRunId(api, request);
15
23
  if (resolved.kind !== 'run')
16
24
  return resolved;
@@ -23,6 +23,20 @@ export const retryNote = (job) => {
23
23
  : job.requeueReason;
24
24
  return why === undefined ? `attempt ${attempt}` : `attempt ${attempt} — ${why}`;
25
25
  };
26
+ /**
27
+ * The first line about a delivery that started nothing, said of what a person was looking at: their pull request.
28
+ * GitHub runs nothing for a pull request it cannot merge, and says so nowhere; the one it is most often asked about
29
+ * gets its own sentence ("#168 has a merge conflict with its base"), any other the delivery's own summary.
30
+ */
31
+ export const quietHeadline = (e) => {
32
+ const pr = e.delivery.pullRequest;
33
+ if (pr === undefined)
34
+ return e.summary;
35
+ if (e.delivery.reasonCode === 'pull-request-conflict') {
36
+ return `#${pr} has a merge conflict with its base, so there is no merge commit to run against and nothing ran — resolve the conflict and push`;
37
+ }
38
+ return `#${pr}: ${e.summary}`;
39
+ };
26
40
  /**
27
41
  * A verdict worth printing under "and here is what else this delivery
28
42
  * decided". `planned` is the run the reader is already looking at, and
@@ -191,6 +191,12 @@ export class HttpControlPlane {
191
191
  return undefined;
192
192
  return (await this.#decodeOk(response));
193
193
  }
194
+ async newestQuietDelivery(repository, pullRequest) {
195
+ const pr = pullRequest === undefined ? '' : `&pull_request=${pullRequest}`;
196
+ const path = `/api/v1/repos/${encodeURIComponent(repository.owner)}/${encodeURIComponent(repository.repo)}/activity?outcome=nothing&limit=1${pr}`;
197
+ const body = await this.#json('GET', path);
198
+ return body.activity[0];
199
+ }
194
200
  async reproduction(runId, job) {
195
201
  const query = job === undefined ? '' : `?job=${encodeURIComponent(job)}`;
196
202
  const response = await this.#request('GET', `/api/v1/runs/${encodeURIComponent(runId)}/reproduction${query}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-actions/cli",
3
- "version": "0.98.0",
3
+ "version": "0.99.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "//files": "Without this npm ships src, test and the CI config alongside the binary a user installs.",
6
6
  "files": [