@apex-actions/cli 0.68.0 → 0.69.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/dist/cli/logs/main.js +49 -2
- package/dist/cli/main.js +2 -0
- package/dist/cli/tenant/main.js +81 -0
- package/dist/src/application/manage-departures.js +18 -0
- package/dist/src/application/read-logs.js +26 -0
- package/dist/src/domain/departures.js +30 -0
- package/dist/src/infrastructure/control-plane-http.js +16 -0
- package/package.json +1 -1
package/dist/cli/logs/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { readLogs } from '../../src/application/read-logs.js';
|
|
3
|
+
import { deleteLogs, readLogs } from '../../src/application/read-logs.js';
|
|
4
4
|
import { readLogView } from '../../src/application/search-logs.js';
|
|
5
5
|
import { HttpControlPlane, NotConfiguredError, RealSleeper } from '../../src/infrastructure/control-plane-http.js';
|
|
6
6
|
import { authorityNote, totalsPhrase } from '../../src/domain/logsearch.js';
|
|
@@ -74,6 +74,10 @@ const handler = async (run, options) => {
|
|
|
74
74
|
console.error(chalk.red(unknownRunMessage(outcome.selector)));
|
|
75
75
|
process.exitCode = 2;
|
|
76
76
|
return;
|
|
77
|
+
case 'deleted':
|
|
78
|
+
console.error(chalk.yellow(`this run’s logs were deleted${outcome.by === undefined ? '' : ` by ${outcome.by}`} on ${outcome.at}`));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
return;
|
|
77
81
|
case 'unknown-job':
|
|
78
82
|
console.error(chalk.red(`no job matches ${outcome.selector}`));
|
|
79
83
|
console.error(chalk.dim(`jobs in this run: ${outcome.available.join(', ')}`));
|
|
@@ -114,6 +118,48 @@ const handler = async (run, options) => {
|
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
};
|
|
121
|
+
const deleteCommand = new Command('delete')
|
|
122
|
+
.description('Delete every job’s log for a finished run — for everyone, and not undoable')
|
|
123
|
+
.argument('<run>', 'run id, #number, or "latest" — required, never assumed')
|
|
124
|
+
.option('--repo <owner/repo>', 'limit the run search to one repository')
|
|
125
|
+
.action(async (run, options) => {
|
|
126
|
+
const repository = options.repo === undefined ? undefined : parseRepository(options.repo);
|
|
127
|
+
if (options.repo !== undefined && repository === undefined) {
|
|
128
|
+
console.error(chalk.red(`--repo must be owner/repo, got ${options.repo}`));
|
|
129
|
+
process.exitCode = 2;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let api;
|
|
133
|
+
try {
|
|
134
|
+
api = new HttpControlPlane();
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
|
|
138
|
+
process.exitCode = 2;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const outcome = await deleteLogs(api, { run, repository });
|
|
142
|
+
switch (outcome.kind) {
|
|
143
|
+
case 'deleted':
|
|
144
|
+
console.log(`deleted the logs of run ${outcome.runId}`);
|
|
145
|
+
return;
|
|
146
|
+
case 'in-progress':
|
|
147
|
+
console.error(chalk.red(`run ${outcome.runId} has not finished; its logs can be deleted once it has`));
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
return;
|
|
150
|
+
case 'no-runs':
|
|
151
|
+
console.error(chalk.yellow('no runs found'));
|
|
152
|
+
process.exitCode = 2;
|
|
153
|
+
return;
|
|
154
|
+
case 'unknown-run':
|
|
155
|
+
console.error(chalk.red(unknownRunMessage(outcome.selector)));
|
|
156
|
+
process.exitCode = 2;
|
|
157
|
+
return;
|
|
158
|
+
case 'refused':
|
|
159
|
+
console.error(chalk.red(outcome.detail));
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
117
163
|
export default new Command('logs')
|
|
118
164
|
.description('Stream or fetch logs for a run or job')
|
|
119
165
|
.argument('[run]', 'run id, #number, or "latest"', 'latest')
|
|
@@ -122,4 +168,5 @@ export default new Command('logs')
|
|
|
122
168
|
.option('--follow', 'keep reading until the run completes')
|
|
123
169
|
.option('--summary', 'print what the log says about the tests it ran, instead of the log')
|
|
124
170
|
.option('--json', 'print the run and its logs as JSON')
|
|
125
|
-
.action(handler)
|
|
171
|
+
.action(handler)
|
|
172
|
+
.addCommand(deleteCommand);
|
package/dist/cli/main.js
CHANGED
|
@@ -24,6 +24,7 @@ import Schedule from './schedule/main.js';
|
|
|
24
24
|
import Oidc from './oidc/main.js';
|
|
25
25
|
import Actions from './actions/main.js';
|
|
26
26
|
import Terms from './terms/main.js';
|
|
27
|
+
import Tenant from './tenant/main.js';
|
|
27
28
|
import Notify from './notify/main.js';
|
|
28
29
|
import Lsp from './lsp/main.js';
|
|
29
30
|
import Login from './login/main.js';
|
|
@@ -78,6 +79,7 @@ const program = new Command()
|
|
|
78
79
|
.addCommand(Oidc)
|
|
79
80
|
.addCommand(Actions)
|
|
80
81
|
.addCommand(Terms)
|
|
82
|
+
.addCommand(Tenant)
|
|
81
83
|
.addCommand(Notify)
|
|
82
84
|
.addCommand(Login)
|
|
83
85
|
.addCommand(Logout)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { listDepartures, purgeDeparture } from '../../src/application/manage-departures.js';
|
|
4
|
+
import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
|
|
5
|
+
import { countsLine, departureLine } from '../../src/domain/departures.js';
|
|
6
|
+
/** Build the API client, or explain what is missing and stop. */
|
|
7
|
+
const api = () => {
|
|
8
|
+
try {
|
|
9
|
+
return new HttpControlPlane();
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
|
|
13
|
+
process.exitCode = 2;
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const departuresCommand = new Command('departures')
|
|
18
|
+
.description('Every tenant that uninstalled, newest first, and when its data is deleted (control-plane ADR-0069)')
|
|
19
|
+
.option('--json', 'machine-readable output')
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
const client = api();
|
|
22
|
+
if (!client)
|
|
23
|
+
return;
|
|
24
|
+
const outcome = await listDepartures(client);
|
|
25
|
+
if (outcome.kind === 'refused') {
|
|
26
|
+
console.error(chalk.red(outcome.message));
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (opts.json === true) {
|
|
31
|
+
console.log(JSON.stringify(outcome.value, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (outcome.value.length === 0) {
|
|
35
|
+
console.log(chalk.green('no tenant has left'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const d of outcome.value)
|
|
39
|
+
console.log(` ${departureLine(d)}`);
|
|
40
|
+
});
|
|
41
|
+
const purgeCommand = new Command('purge')
|
|
42
|
+
.description("Delete one departed tenant's data now, whatever its window — a dry run unless --apply")
|
|
43
|
+
.argument('<installationId>', 'the installation that left')
|
|
44
|
+
.option('--apply', 'delete it (default: report what would be deleted)')
|
|
45
|
+
.option('--json', 'machine-readable output')
|
|
46
|
+
.action(async (raw, opts) => {
|
|
47
|
+
const installationId = Number(raw);
|
|
48
|
+
if (!Number.isSafeInteger(installationId) || installationId <= 0) {
|
|
49
|
+
console.error(chalk.red(`expected a positive installation id, got ${JSON.stringify(raw)}`));
|
|
50
|
+
process.exitCode = 2;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const client = api();
|
|
54
|
+
if (!client)
|
|
55
|
+
return;
|
|
56
|
+
const outcome = await purgeDeparture(client, installationId, opts.apply === true);
|
|
57
|
+
if (outcome.kind === 'refused') {
|
|
58
|
+
console.error(chalk.red(outcome.message));
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const report = outcome.value;
|
|
63
|
+
if (opts.json === true) {
|
|
64
|
+
console.log(JSON.stringify(report, null, 2));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
console.log(departureLine(report.departure));
|
|
68
|
+
if (report.applied && report.counts !== null) {
|
|
69
|
+
console.log(`${chalk.green('✔')} purged: ${countsLine(report.counts)}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (report.departure.purgedAt !== null) {
|
|
73
|
+
console.log(chalk.dim(`already purged${report.counts === null ? '' : `: ${countsLine(report.counts)}`}`));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk.yellow(`${report.repositories.length} repositor${report.repositories.length === 1 ? 'y' : 'ies'} and everything under ${report.repositories.length === 1 ? 'it' : 'them'} would be deleted, with the installation's deliveries and organisation secrets. Re-run with --apply.`));
|
|
77
|
+
});
|
|
78
|
+
export default new Command('tenant')
|
|
79
|
+
.description("Departed tenants: who left, when their data is deleted, and deleting it now on request")
|
|
80
|
+
.addCommand(departuresCommand)
|
|
81
|
+
.addCommand(purgeCommand);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const attempt = async (fn) => {
|
|
2
|
+
try {
|
|
3
|
+
return { kind: 'ok', value: await fn() };
|
|
4
|
+
}
|
|
5
|
+
catch (err) {
|
|
6
|
+
return { kind: 'refused', message: err instanceof Error ? err.message : String(err) };
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
/** Every departed tenant, newest first, and the clock on its data. */
|
|
10
|
+
export const listDepartures = async (api) => await attempt(() => api.departures());
|
|
11
|
+
/**
|
|
12
|
+
* Purge one departed tenant's data now, whatever its window — a dry run unless
|
|
13
|
+
* `apply`. The control plane decides everything: that the tenant left, that its
|
|
14
|
+
* account did not come back, what it held. The machine token is the only
|
|
15
|
+
* caller it accepts, because the tenant has gone and no GitHub authority is
|
|
16
|
+
* left to ask.
|
|
17
|
+
*/
|
|
18
|
+
export const purgeDeparture = async (api, installationId, apply) => await attempt(() => api.purgeDeparture(installationId, apply));
|
|
@@ -26,6 +26,11 @@ export const readLogs = async (api, sleeper, request) => {
|
|
|
26
26
|
break;
|
|
27
27
|
detail = fresh;
|
|
28
28
|
}
|
|
29
|
+
// Deleted is a fact about the run, said once — not an error per job, and
|
|
30
|
+
// not an empty log, which would read as a job that printed nothing.
|
|
31
|
+
if (detail.run.logsDeletedAt !== undefined) {
|
|
32
|
+
return { kind: 'deleted', run: detail.run, at: detail.run.logsDeletedAt, by: detail.run.logsDeletedBy };
|
|
33
|
+
}
|
|
29
34
|
const jobs = selectJobs(detail.jobs, request.job);
|
|
30
35
|
const sections = [];
|
|
31
36
|
for (const job of jobs)
|
|
@@ -38,3 +43,24 @@ export const readLogs = async (api, sleeper, request) => {
|
|
|
38
43
|
exitCode: isLive(detail.run.status) ? 0 : detail.run.conclusion === 'success' ? 0 : 1,
|
|
39
44
|
};
|
|
40
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Delete every job's log for one finished run (DPA §3.5, TASK-0174).
|
|
48
|
+
*
|
|
49
|
+
* The run is resolved the way `apex logs` resolves it, but there is no
|
|
50
|
+
* default: `latest` has to be typed, because the run it names changes under
|
|
51
|
+
* the person between reading it and deleting it.
|
|
52
|
+
*/
|
|
53
|
+
export const deleteLogs = async (api, request) => {
|
|
54
|
+
try {
|
|
55
|
+
const resolved = await resolveRunId(api, request);
|
|
56
|
+
if (resolved.kind !== 'run')
|
|
57
|
+
return resolved;
|
|
58
|
+
const outcome = await api.deleteRunLogs(resolved.id);
|
|
59
|
+
if (outcome === 'not-found')
|
|
60
|
+
return { kind: 'unknown-run', selector: request.run };
|
|
61
|
+
return { kind: outcome, runId: resolved.id };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return { kind: 'refused', detail: err instanceof Error ? err.message : String(err) };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// domain layer — see ../../../../docs/conventions.md §1
|
|
2
|
+
/**
|
|
3
|
+
* Where a departure stands, in the words an operator answering a deletion
|
|
4
|
+
* request needs: gone, kept because the account came back, or still waiting —
|
|
5
|
+
* and until when.
|
|
6
|
+
*/
|
|
7
|
+
export const departureState = (d) => {
|
|
8
|
+
if (d.purgedAt !== null)
|
|
9
|
+
return `purged ${d.purgedAt}`;
|
|
10
|
+
if (d.withdrawnAt !== null)
|
|
11
|
+
return `kept — the account reinstalled ${d.withdrawnAt}`;
|
|
12
|
+
return `due ${d.purgeAfter}`;
|
|
13
|
+
};
|
|
14
|
+
export const departureLine = (d) => `${d.owner} (installation ${d.installationId}) — left ${d.departedAt} by ${d.reason}; ${departureState(d)}`;
|
|
15
|
+
/** The counts as one line, naming only what was there. */
|
|
16
|
+
export const countsLine = (c) => {
|
|
17
|
+
const parts = [
|
|
18
|
+
[c.repositories, 'repository', 'repositories'],
|
|
19
|
+
[c.runs, 'run', 'runs'],
|
|
20
|
+
[c.jobs, 'job', 'jobs'],
|
|
21
|
+
[c.artifacts, 'artifact', 'artifacts'],
|
|
22
|
+
[c.cacheEntries, 'cache entry', 'cache entries'],
|
|
23
|
+
[c.deliveries, 'delivery', 'deliveries'],
|
|
24
|
+
[c.secrets, 'secret', 'secrets'],
|
|
25
|
+
[c.variables, 'variable', 'variables'],
|
|
26
|
+
[c.objects, 'stored object', 'stored objects'],
|
|
27
|
+
];
|
|
28
|
+
const named = parts.filter(([n]) => n > 0).map(([n, one, many]) => `${n} ${n === 1 ? one : many}`);
|
|
29
|
+
return named.length === 0 ? 'nothing was held' : named.join(', ');
|
|
30
|
+
};
|
|
@@ -199,6 +199,12 @@ export class HttpControlPlane {
|
|
|
199
199
|
const body = await this.#json('POST', '/api/v1/terms/acceptance', { installationId, termsVersion: version });
|
|
200
200
|
return body.accepted;
|
|
201
201
|
}
|
|
202
|
+
async departures() {
|
|
203
|
+
return (await this.#json('GET', '/api/v1/admin/departures')).departures;
|
|
204
|
+
}
|
|
205
|
+
async purgeDeparture(installationId, apply) {
|
|
206
|
+
return await this.#json('POST', `/api/v1/departures/${installationId}/purge${apply ? '?apply=true' : ''}`);
|
|
207
|
+
}
|
|
202
208
|
async actionPolicy(owner) {
|
|
203
209
|
return await this.#json('GET', `/api/v1/orgs/${encodeURIComponent(owner)}/action-policy`);
|
|
204
210
|
}
|
|
@@ -258,6 +264,16 @@ export class HttpControlPlane {
|
|
|
258
264
|
throw new Error(await this.#errorMessage(response));
|
|
259
265
|
return await response.text();
|
|
260
266
|
}
|
|
267
|
+
async deleteRunLogs(runId) {
|
|
268
|
+
const response = await this.#request('DELETE', `/api/v1/runs/${encodeURIComponent(runId)}/logs`);
|
|
269
|
+
if (response.status === 404)
|
|
270
|
+
return 'not-found';
|
|
271
|
+
if (response.status === 409)
|
|
272
|
+
return 'in-progress';
|
|
273
|
+
if (!response.ok)
|
|
274
|
+
throw new Error(await this.#errorMessage(response));
|
|
275
|
+
return 'deleted';
|
|
276
|
+
}
|
|
261
277
|
async dispatch(owner, repo, workflow, ref, inputs) {
|
|
262
278
|
return await this.#json('POST', `/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/workflows/${encodeURIComponent(workflow)}/dispatches`, { ref, inputs });
|
|
263
279
|
}
|