@apex-actions/cli 0.73.1 → 0.74.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/environment/main.js +2 -2
- package/dist/cli/tenant/main.js +60 -4
- package/dist/src/application/manage-accounts.js +22 -0
- package/dist/src/application/manage-environments.js +7 -4
- package/dist/src/domain/accounts.js +11 -0
- package/dist/src/infrastructure/control-plane-http.js +6 -0
- package/package.json +1 -1
|
@@ -59,7 +59,7 @@ const setCommand = new Command('set')
|
|
|
59
59
|
.argument('<repository>', 'owner/repo')
|
|
60
60
|
.argument('<name>', 'environment name, as the workflow spells it')
|
|
61
61
|
.option('--wait-timer <minutes>', 'delay before the job may start; 0 removes it', (v) => Number(v))
|
|
62
|
-
.option('--reviewer <who...>', 'user:<login>,
|
|
62
|
+
.option('--reviewer <who...>', 'user:<login>, role:<owner|account_admin|billing|member|viewer>, or a bare login (a user)')
|
|
63
63
|
.option('--branch-pattern <glob...>', 'refs permitted to deploy here (main, releases/*, v?.0)')
|
|
64
64
|
.option('--prevent-self-review', 'refuse an approval from whoever triggered the run')
|
|
65
65
|
.option('--allow-self-review', 'permit it (the default)')
|
|
@@ -78,7 +78,7 @@ const setCommand = new Command('set')
|
|
|
78
78
|
for (const raw of opts.reviewer) {
|
|
79
79
|
const parsed = parseReviewer(raw);
|
|
80
80
|
if (!parsed) {
|
|
81
|
-
console.error(chalk.red(`expected user:<login>,
|
|
81
|
+
console.error(chalk.red(`expected user:<login>, role:<owner|account_admin|billing|member|viewer> or a bare login, got ${JSON.stringify(raw)}`));
|
|
82
82
|
process.exitCode = 2;
|
|
83
83
|
return;
|
|
84
84
|
}
|
package/dist/cli/tenant/main.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { listDepartures, purgeDeparture } from '../../src/application/manage-departures.js';
|
|
4
|
-
import { backfillAccounts } from '../../src/application/manage-accounts.js';
|
|
5
|
-
import { backfillLines } from '../../src/domain/accounts.js';
|
|
4
|
+
import { backfillAccounts, connectInstallation, listUnconnected } from '../../src/application/manage-accounts.js';
|
|
5
|
+
import { backfillLines, connectLine, unconnectedLine } from '../../src/domain/accounts.js';
|
|
6
6
|
import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
|
|
7
7
|
import { countsLine, departureLine } from '../../src/domain/departures.js';
|
|
8
8
|
/** Build the API client, or explain what is missing and stop. */
|
|
@@ -103,8 +103,64 @@ const backfillCommand = new Command('backfill')
|
|
|
103
103
|
if (!outcome.value.applied)
|
|
104
104
|
console.log(chalk.yellow('\nNothing was changed. Review the above, then re-run with --apply.'));
|
|
105
105
|
});
|
|
106
|
+
const unconnectedCommand = new Command('unconnected')
|
|
107
|
+
.description('Installations no Apex Account owns, the longest-waiting first — their people are refused until connected (decisions-log O20)')
|
|
108
|
+
.option('--json', 'machine-readable output')
|
|
109
|
+
.action(async (opts) => {
|
|
110
|
+
const client = api();
|
|
111
|
+
if (!client)
|
|
112
|
+
return;
|
|
113
|
+
const outcome = await listUnconnected(client);
|
|
114
|
+
if (outcome.kind === 'refused') {
|
|
115
|
+
console.error(chalk.red(outcome.message));
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (opts.json === true) {
|
|
120
|
+
console.log(JSON.stringify(outcome.value, null, 2));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (outcome.value.length === 0) {
|
|
124
|
+
console.log(chalk.green('every installation is connected to an Account'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const now = new Date();
|
|
128
|
+
for (const i of outcome.value)
|
|
129
|
+
console.log(` ${unconnectedLine(i, now)}`);
|
|
130
|
+
console.log(chalk.dim('\nConnect one with: apex tenant connect <installationId> --owner <github-login> [--name <Account name>]'));
|
|
131
|
+
});
|
|
132
|
+
const connectCommand = new Command('connect')
|
|
133
|
+
.description('Connect one installation to the Account --owner owns, creating it with them its Owner when they own none (decisions-log O20)')
|
|
134
|
+
.argument('<installationId>', 'the installation to connect (see: apex tenant unconnected)')
|
|
135
|
+
.requiredOption('--owner <login>', 'the GitHub login of the Account’s Owner; they must have signed in once')
|
|
136
|
+
.option('--name <name>', 'the Account’s name — required when the Owner owns no Account yet')
|
|
137
|
+
.option('--json', 'machine-readable output')
|
|
138
|
+
.action(async (raw, opts) => {
|
|
139
|
+
const installationId = Number(raw);
|
|
140
|
+
if (!Number.isSafeInteger(installationId) || installationId <= 0) {
|
|
141
|
+
console.error(chalk.red(`expected a positive installation id, got ${JSON.stringify(raw)}`));
|
|
142
|
+
process.exitCode = 2;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const client = api();
|
|
146
|
+
if (!client)
|
|
147
|
+
return;
|
|
148
|
+
const outcome = await connectInstallation(client, { installationId, ownerLogin: opts.owner, name: opts.name });
|
|
149
|
+
if (outcome.kind === 'refused') {
|
|
150
|
+
console.error(chalk.red(outcome.message));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (opts.json === true) {
|
|
155
|
+
console.log(JSON.stringify(outcome.value, null, 2));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(`${chalk.green('✔')} ${connectLine(outcome.value)}`);
|
|
159
|
+
});
|
|
106
160
|
export default new Command('tenant')
|
|
107
|
-
.description('Tenants: who left and deleting their data on request, and the one-time move into Accounts')
|
|
161
|
+
.description('Tenants: connecting new installations to Accounts, who left and deleting their data on request, and the one-time move into Accounts')
|
|
108
162
|
.addCommand(departuresCommand)
|
|
109
163
|
.addCommand(purgeCommand)
|
|
110
|
-
.addCommand(backfillCommand)
|
|
164
|
+
.addCommand(backfillCommand)
|
|
165
|
+
.addCommand(unconnectedCommand)
|
|
166
|
+
.addCommand(connectCommand);
|
|
@@ -10,3 +10,25 @@ export const backfillAccounts = async (api, input, apply) => {
|
|
|
10
10
|
return { kind: 'refused', message: err instanceof Error ? err.message : String(err) };
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
|
+
const refusal = (err) => ({ kind: 'refused', message: err instanceof Error ? err.message : String(err) });
|
|
14
|
+
/** The installations waiting for an operator (decisions-log O20), the longest-waiting first. */
|
|
15
|
+
export const listUnconnected = async (api) => {
|
|
16
|
+
try {
|
|
17
|
+
return { kind: 'ok', value: await api.accountsUnconnected() };
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
return refusal(err);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Connect one installation to the Account `ownerLogin` owns — created, with them
|
|
25
|
+
* its Owner, when they own none. The operator's step until onboarding (TASK-0204).
|
|
26
|
+
*/
|
|
27
|
+
export const connectInstallation = async (api, input) => {
|
|
28
|
+
try {
|
|
29
|
+
return { kind: 'ok', value: await api.accountsConnect(input) };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return refusal(err);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// application layer — see ../../../../docs/conventions.md §1
|
|
2
|
+
/** The Account roles a reviewer may name (ADR-0033), exactly as the control plane accepts them. */
|
|
3
|
+
export const REVIEWER_ROLES = ['owner', 'account_admin', 'billing', 'member', 'viewer'];
|
|
2
4
|
const refused = (err) => ({ kind: 'refused', detail: err instanceof Error ? err.message : String(err) });
|
|
3
5
|
export const listEnvironments = async (api, owner, repo) => {
|
|
4
6
|
try {
|
|
@@ -26,8 +28,9 @@ export const deleteEnvironment = async (api, owner, repo, name) => {
|
|
|
26
28
|
}
|
|
27
29
|
};
|
|
28
30
|
/**
|
|
29
|
-
* `user:ana`, `
|
|
30
|
-
* what people mean when they type one).
|
|
31
|
+
* `user:ana`, `role:account_admin`, or a bare name (a user, because that is
|
|
32
|
+
* what people mean when they type one). A GitHub team no longer approves
|
|
33
|
+
* (decisions-log O20), so `team:` is refused rather than sent.
|
|
31
34
|
*/
|
|
32
35
|
export const parseReviewer = (raw) => {
|
|
33
36
|
const [head, ...rest] = raw.split(':');
|
|
@@ -40,8 +43,8 @@ export const parseReviewer = (raw) => {
|
|
|
40
43
|
return undefined;
|
|
41
44
|
if (head === 'user')
|
|
42
45
|
return { type: 'user', name };
|
|
43
|
-
if (head === '
|
|
44
|
-
return
|
|
46
|
+
if (head === 'role')
|
|
47
|
+
return REVIEWER_ROLES.includes(name) ? { type: 'role', name } : undefined;
|
|
45
48
|
return undefined;
|
|
46
49
|
};
|
|
47
50
|
export const listDeployments = async (api, runId) => {
|
|
@@ -31,3 +31,14 @@ export const backfillLines = (r) => {
|
|
|
31
31
|
lines.push('Notes:', ...r.notes.map((n) => ` ${n}`));
|
|
32
32
|
return lines;
|
|
33
33
|
};
|
|
34
|
+
const waited = (seenAt, now) => {
|
|
35
|
+
const minutes = Math.max(0, Math.floor((now.getTime() - Date.parse(seenAt)) / 60_000));
|
|
36
|
+
if (minutes < 60)
|
|
37
|
+
return `${minutes} min`;
|
|
38
|
+
const hours = Math.floor(minutes / 60);
|
|
39
|
+
return hours < 48 ? `${hours} h` : `${Math.floor(hours / 24)} d`;
|
|
40
|
+
};
|
|
41
|
+
/** One waiting installation as a line: who, which installation, and for how long. */
|
|
42
|
+
export const unconnectedLine = (i, now) => `${i.login} (${i.type === 'User' ? 'personal account' : 'organization'}) — installation ${i.installationId}, waiting ${waited(i.seenAt, now)}`;
|
|
43
|
+
/** A connection as a line, saying whether the Account was made for it. */
|
|
44
|
+
export const connectLine = (c) => `${c.organization.login} (installation ${c.organization.installationId}) connected to ${c.account.created ? 'the new Account' : 'the Account'} ${c.account.name} (${c.account.id})`;
|
|
@@ -223,6 +223,12 @@ export class HttpControlPlane {
|
|
|
223
223
|
async accountsBackfill(input, apply) {
|
|
224
224
|
return await this.#json('POST', `/api/v1/admin/accounts/backfill${apply ? '?apply=true' : ''}`, input);
|
|
225
225
|
}
|
|
226
|
+
async accountsUnconnected() {
|
|
227
|
+
return (await this.#json('GET', '/api/v1/admin/accounts/unconnected')).installations;
|
|
228
|
+
}
|
|
229
|
+
async accountsConnect(input) {
|
|
230
|
+
return await this.#json('POST', '/api/v1/admin/accounts/connect', input);
|
|
231
|
+
}
|
|
226
232
|
async actionPolicy(owner) {
|
|
227
233
|
return await this.#json('GET', `/api/v1/orgs/${encodeURIComponent(owner)}/action-policy`);
|
|
228
234
|
}
|