@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 +0 -1
- package/dist/cli/main.js +2 -2
- package/dist/cli/reference.js +1 -0
- package/dist/cli/why/main.js +23 -4
- package/dist/src/application/explain-runs.js +8 -0
- package/dist/src/domain/provenance.js +14 -0
- package/dist/src/infrastructure/binary-install.js +21 -10
- package/dist/src/infrastructure/binary-install.test.js +7 -0
- package/dist/src/infrastructure/control-plane-http.js +6 -0
- package/package.json +1 -1
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` —
|
|
5
|
-
//
|
|
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);
|
package/dist/cli/reference.js
CHANGED
|
@@ -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: {
|
package/dist/cli/why/main.js
CHANGED
|
@@ -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
|
-
|
|
40
|
-
console.log(`
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
21
|
+
return read(dir);
|
|
26
22
|
}
|
|
27
23
|
catch {
|
|
28
24
|
if (dirname(dir) === dir)
|
|
29
|
-
return
|
|
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}`);
|