@apex-actions/cli 0.98.1 → 0.100.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/README.md CHANGED
@@ -125,7 +125,6 @@ refused · 2 usage or configuration · 3 not found on `delete`.
125
125
  - `src/application/` — use cases over ports
126
126
  - `src/infrastructure/` — adapters: the engine and runner binaries, the REST API
127
127
  - Documentation is the root's: decisions in `docs/adrs/cli/`, the rest by category in `docs/` (TASK-0526)
128
- - `VERSION` — the release version, which `version:sync` writes from the root (ADR-0048); never edited by hand
129
128
 
130
129
  ## Licence
131
130
 
package/dist/cli/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { packageVersion } from '../src/infrastructure/binary-install.js';
3
3
  import { buildProgram } from './program.js';
4
- // The version from the nearest `package.json` — what ships, and what `version:check` keeps equal to
5
- // `VERSION` (see `packageVersion`); it also names which engine and runner the CLI downloads.
4
+ // The version from the nearest `package.json` — the tag's, written at publish (ADR-0053; see `packageVersion`);
5
+ // it also names which engine and runner the CLI downloads.
6
6
  buildProgram(packageVersion()).parse(process.argv);
@@ -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
@@ -12,24 +12,35 @@ import { promisify } from 'node:util';
12
12
  * platform and a SHA256SUMS file per command, under the lockstep version.
13
13
  */
14
14
  export const DIST_BASE = 'https://apexactions.com/dl';
15
- /**
16
- * The CLI's version, from the nearest `package.json` above this file.
17
- *
18
- * `VERSION` is the source of truth in the repository and is not in the published package; what ships is
19
- * `package.json`, and `version:check` guarantees the two agree. Walking up survives the layout differing
20
- * between `tsx` (`src/…`) and a tarball (`dist/src/…`).
21
- */
22
- export const packageVersion = (from = import.meta.dirname) => {
15
+ /** What every workspace's `package.json` says in the repository: a constant, not a release (ADR-0053). */
16
+ const UNRELEASED = '0.0.0';
17
+ /** The first directory at or above `from` that `read` succeeds in, read; undefined when none does. */
18
+ const nearest = (from, read) => {
23
19
  for (let dir = from;; dir = dirname(dir)) {
24
20
  try {
25
- return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version;
21
+ return read(dir);
26
22
  }
27
23
  catch {
28
24
  if (dirname(dir) === dir)
29
- return '0.0.0-unknown';
25
+ return undefined;
30
26
  }
31
27
  }
32
28
  };
29
+ /**
30
+ * The CLI's version, from the nearest `package.json` above this file.
31
+ *
32
+ * In the repository a workspace's `package.json` carries `0.0.0` and the release is the root `VERSION` alone
33
+ * (ADR-0053), so a release changes nothing a build hashes; `publish.yml` writes the tag's version into the package
34
+ * it publishes, and that is what an installed CLI reads. A checkout's `0.0.0` reads on to the root `VERSION`, so a
35
+ * development CLI downloads the last release's binaries rather than ones nobody published. Walking up survives the
36
+ * layout differing between `tsx` (`src/…`) and a tarball (`dist/src/…`).
37
+ */
38
+ export const packageVersion = (from = import.meta.dirname) => {
39
+ const declared = nearest(from, (dir) => JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version);
40
+ if (declared !== undefined && declared !== UNRELEASED)
41
+ return declared;
42
+ return nearest(from, (dir) => readFileSync(join(dir, 'VERSION'), 'utf8').trim()) ?? '0.0.0-unknown';
43
+ };
33
44
  /** Node's names to Go's, for the four targets that are published; anything else has no download. */
34
45
  export const platformOf = (platform, arch) => {
35
46
  const os = platform === 'darwin' ? 'darwin' : platform === 'linux' ? 'linux' : undefined;
@@ -111,4 +111,11 @@ describe('the names', () => {
111
111
  writeFileSync(join(root, 'package.json'), JSON.stringify({ version: '9.8.7' }));
112
112
  expect(packageVersion(join(root, 'dist', 'src'))).toBe('9.8.7');
113
113
  });
114
+ it("reads a checkout's 0.0.0 on to the root VERSION, the release (ADR-0053)", () => {
115
+ const root = mkdtempSync(join(tmpdir(), 'repo-'));
116
+ mkdirSync(join(root, 'apps', 'cli', 'src'), { recursive: true });
117
+ writeFileSync(join(root, 'VERSION'), '0.99.0\n');
118
+ writeFileSync(join(root, 'apps', 'cli', 'package.json'), JSON.stringify({ version: '0.0.0' }));
119
+ expect(packageVersion(join(root, 'apps', 'cli', 'src'))).toBe('0.99.0');
120
+ });
114
121
  });
@@ -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.1",
3
+ "version": "0.100.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": [