@apex-actions/cli 0.74.0 → 0.75.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 +29 -11
- package/dist/cli/login/main.js +18 -6
- package/dist/cli/logout/main.js +5 -3
- package/dist/cli/reference.js +197 -38
- package/dist/cli/spend/main.js +1 -1
- package/dist/cli/token/main.js +132 -29
- package/dist/cli/whoami/main.js +44 -2
- package/dist/src/application/authenticate.js +24 -8
- package/dist/src/application/manage-tokens.js +83 -3
- package/dist/src/domain/credentials.js +115 -16
- package/dist/src/infrastructure/control-plane-http.js +73 -16
- package/dist/src/infrastructure/credentials.js +30 -8
- package/dist/src/infrastructure/loopback-login.js +43 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,12 +11,15 @@ Part of the [Apex Actions](../) superproject. Conventions, architecture, and age
|
|
|
11
11
|
| `apex run` | the runner binary, locally | `apex-runner` on `PATH` |
|
|
12
12
|
| `apex validate` | the engine binary, locally | `apex-engine` on `PATH` |
|
|
13
13
|
| `apex migrate scan` | the engine binary + the GitHub REST API | `apex-engine` on `PATH`; `GITHUB_TOKEN` unless `--path` |
|
|
14
|
-
| `apex logs` | the control-plane REST API |
|
|
15
|
-
| `apex dispatch` | the control-plane REST API | the operator token
|
|
14
|
+
| `apex logs` | the control-plane REST API | a customer token with `actions:read` (`logs delete`: `actions:write`), or the operator token |
|
|
15
|
+
| `apex dispatch` | the control-plane REST API | a customer token with `actions:write`, or the operator token |
|
|
16
16
|
| `apex login` · `logout` · `whoami` | the browser and the control-plane REST API | nothing — `login` mints the credential |
|
|
17
17
|
| `apex token create` | the browser and the control-plane REST API | a person signed in to the dashboard, who approves it |
|
|
18
|
+
| `apex token edit` | the control-plane REST API; the browser to widen | a customer token to narrow; a person signed in to the dashboard to widen |
|
|
18
19
|
| `apex token list` · `revoke` | the control-plane REST API | a customer token (`APEX_TOKEN` or `apex login`) |
|
|
19
|
-
| `apex secret` · `apex variable` | the control-plane REST API | a customer token (`APEX_TOKEN` or `apex login`)
|
|
20
|
+
| `apex secret` · `apex variable` | the control-plane REST API | a customer token with `secrets:*` / `variables:*` (`APEX_TOKEN` or `apex login`) |
|
|
21
|
+
|
|
22
|
+
Every other customer command in `cli/reference.ts` names the permission it needs; the public reference shows it.
|
|
20
23
|
|
|
21
24
|
Generated reference: `docs/feature-docs/cli-commands.md` (`pnpm docs:cli` from `packages/release`), and
|
|
22
25
|
the structured one the public site renders, `docs/feature-docs/cli-reference.json` (`pnpm docs:reference`;
|
|
@@ -43,21 +46,33 @@ apex migrate scan acme --repo acme/api --json # one repository of an organizat
|
|
|
43
46
|
|
|
44
47
|
### Credentials
|
|
45
48
|
|
|
46
|
-
The CLI resolves one credential, in this order (control-plane ADR-0064
|
|
49
|
+
The CLI resolves one credential, in this order (control-plane ADR-0064, amended by
|
|
50
|
+
[ADR-0005](docs/adr/0005-an-exported-token-outranks-a-saved-login.md)) — **the environment outranks
|
|
51
|
+
the file**:
|
|
47
52
|
|
|
48
53
|
1. `APEX_TOKEN` — a token minted on the dashboard (`/settings/tokens`), for CI and scripts;
|
|
49
|
-
2. the token
|
|
54
|
+
2. `APEX_ADMIN_TOKEN` — the operator's machine token, unchanged: unscoped, so treat it as a root
|
|
55
|
+
credential. An exported variable is the deliberate choice, as `GH_TOKEN` outranks `gh`'s stored
|
|
56
|
+
login; a saved login used to win, so an operator shell acted as whichever customer had last run
|
|
57
|
+
`apex login` on the machine;
|
|
58
|
+
3. the token `apex login` saved in `$XDG_CONFIG_HOME/apex/credentials` (default `~/.config`),
|
|
50
59
|
mode `0600` in a `0700` directory — a file readable by group or others is **refused**, as `ssh`
|
|
51
60
|
refuses a private key;
|
|
52
|
-
3. `APEX_ADMIN_TOKEN` — the operator's machine token, unchanged: unscoped, so treat it as a root
|
|
53
|
-
credential;
|
|
54
61
|
4. otherwise the command stops with exit 2, naming `APEX_TOKEN` and `apex login`.
|
|
55
62
|
|
|
56
63
|
`APEX_API_URL` selects the control plane (default `https://app.apexactions.com`); a saved login is
|
|
57
64
|
only ever sent to the control plane that issued it. `apex login` opens the consent page on the web
|
|
58
65
|
app — `APEX_WEB_URL`, or the API's origin — and listens on `127.0.0.1` for the loopback callback
|
|
59
|
-
(RFC 8252, PKCE)
|
|
60
|
-
|
|
66
|
+
(RFC 8252, PKCE); once the code is redeemed it hands the tab back to the web app's bare `/cli/done`
|
|
67
|
+
page (control-plane ADR-0075). A customer token carries **permissions** (`resource:read|write`, write implying
|
|
68
|
+
read; resources `actions`, `approvals`, `deployments`, `secrets`, `variables`, `environments`, `notifications`,
|
|
69
|
+
`administration`) and belongs to **one Account** (control-plane ADR-0071). It reaches the repository routes its
|
|
70
|
+
grants cover, within its person's role and the Account owner's token policy; `apex login` takes the policy's
|
|
71
|
+
defaults for the person's role (the Owner's: everything), in the Account `--account` names when there is more than
|
|
72
|
+
one. A route it lacks the grant for answers 403 `this token needs <permission>`, which the CLI completes with the
|
|
73
|
+
`apex token edit <id> --add-permission <permission>` that would add it. `apex whoami`
|
|
74
|
+
names the credential in use, where it was found, and any other credential present that it
|
|
75
|
+
outranked. `apex logout` revokes the saved token on the server,
|
|
61
76
|
then deletes the file. No command prints a token — except `apex token create`, whose whole job is to
|
|
62
77
|
print the new one, once.
|
|
63
78
|
|
|
@@ -73,8 +88,11 @@ A pipeline gets its own token. It is approved in the browser like a login, print
|
|
|
73
88
|
and never saved (control-plane ADR-0064: a token never mints a token):
|
|
74
89
|
|
|
75
90
|
```bash
|
|
76
|
-
apex token create ci-deploy --
|
|
77
|
-
|
|
91
|
+
apex token create ci-deploy --permission actions:write --permission secrets:write \
|
|
92
|
+
--repo acme/api --expires-in-days 30 # token on stdout, details on stderr
|
|
93
|
+
apex token edit <id> --remove-permission secrets:write # narrowing: applies at once
|
|
94
|
+
apex token edit <id> --add-permission deployments:write # widening: approved in the browser
|
|
95
|
+
apex token list # by Account, with grants; * marks the one in use
|
|
78
96
|
apex token revoke 0b6f1c2e-9d4a-4f7e-8a31-5c2d9e7b1a40 # or --all
|
|
79
97
|
```
|
|
80
98
|
|
package/dist/cli/login/main.js
CHANGED
|
@@ -2,8 +2,8 @@ import { hostname } from 'node:os';
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { login } from '../../src/application/authenticate.js';
|
|
5
|
-
import { TOKENS_PAGE, webBaseFor } from '../../src/domain/credentials.js';
|
|
6
|
-
import { FileCredentialStore, TOKEN_ENV, WEB_URL_ENV, apiUrlFrom, credentialsPath } from '../../src/infrastructure/credentials.js';
|
|
5
|
+
import { TOKENS_PAGE, describeGrants, webBaseFor } from '../../src/domain/credentials.js';
|
|
6
|
+
import { ADMIN_TOKEN_ENV, FileCredentialStore, TOKEN_ENV, WEB_URL_ENV, apiUrlFrom, credentialsPath } from '../../src/infrastructure/credentials.js';
|
|
7
7
|
import { HttpLoginExchange, listenForCallback, newPkcePair, openInBrowser } from '../../src/infrastructure/loopback-login.js';
|
|
8
8
|
/**
|
|
9
9
|
* `apex login`: sign the CLI in with the browser a person already uses
|
|
@@ -12,9 +12,14 @@ import { HttpLoginExchange, listenForCallback, newPkcePair, openInBrowser } from
|
|
|
12
12
|
*
|
|
13
13
|
* It opens a browser only because it was asked to — this command is the
|
|
14
14
|
* asking — and prints the URL either way, for a machine where none opens.
|
|
15
|
+
*
|
|
16
|
+
* The token carries the Account token policy's defaults for the person's role
|
|
17
|
+
* there (the Owner's: everything), and belongs to that one Account — named with
|
|
18
|
+
* `--account` when the person is in more than one (control-plane ADR-0071).
|
|
15
19
|
*/
|
|
16
20
|
export default new Command('login')
|
|
17
|
-
.description(
|
|
21
|
+
.description("Sign the CLI in through the browser; the token is saved for this user only and carries your Account's default permissions for your role")
|
|
22
|
+
.option('--account <id>', 'the Account to sign in to — needed when you belong to more than one')
|
|
18
23
|
.option('--no-browser', 'print the URL to open instead of opening a browser')
|
|
19
24
|
.action(async (opts) => {
|
|
20
25
|
const apiUrl = apiUrlFrom();
|
|
@@ -36,6 +41,7 @@ export default new Command('login')
|
|
|
36
41
|
apiUrl,
|
|
37
42
|
webBase,
|
|
38
43
|
name: hostname().slice(0, 100) || 'apex CLI',
|
|
44
|
+
accountId: opts.account,
|
|
39
45
|
});
|
|
40
46
|
}
|
|
41
47
|
catch (err) {
|
|
@@ -46,9 +52,15 @@ export default new Command('login')
|
|
|
46
52
|
switch (outcome.kind) {
|
|
47
53
|
case 'logged-in':
|
|
48
54
|
console.log(`${chalk.green('✔')} signed in as ${outcome.login} — the token expires ${outcome.expiresAt}`);
|
|
55
|
+
if (outcome.permissions !== undefined) {
|
|
56
|
+
console.log(` permissions: ${describeGrants(outcome.permissions)}${outcome.accountId === undefined ? '' : ` · Account ${outcome.accountId}`}`);
|
|
57
|
+
}
|
|
49
58
|
console.log(chalk.dim(`Saved to ${outcome.location}. Revoke it any time at ${webBase}${TOKENS_PAGE}.`));
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
// The environment outranks the saved login (ADR-0005): say so, or the login looks like it did nothing.
|
|
60
|
+
for (const name of [TOKEN_ENV, ADMIN_TOKEN_ENV]) {
|
|
61
|
+
if (process.env[name])
|
|
62
|
+
console.log(chalk.yellow(`${name} is set in this shell and is used before the saved login; unset it to use the login.`));
|
|
63
|
+
}
|
|
52
64
|
return;
|
|
53
65
|
case 'denied':
|
|
54
66
|
console.error(chalk.red(`the login was not approved: ${outcome.detail}`));
|
|
@@ -63,7 +75,7 @@ export default new Command('login')
|
|
|
63
75
|
process.exitCode = 1;
|
|
64
76
|
return;
|
|
65
77
|
case 'timeout':
|
|
66
|
-
console.error(chalk.red('no approval arrived within 5 minutes; run `apex login` again'));
|
|
78
|
+
console.error(chalk.red('no approval arrived within 5 minutes; the approval page must be opened in a browser on this machine — run `apex login` again'));
|
|
67
79
|
process.exitCode = 1;
|
|
68
80
|
return;
|
|
69
81
|
case 'refused':
|
package/dist/cli/logout/main.js
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { logout } from '../../src/application/authenticate.js';
|
|
4
4
|
import { TOKENS_PAGE, webBaseFor } from '../../src/domain/credentials.js';
|
|
5
5
|
import { HttpControlPlane } from '../../src/infrastructure/control-plane-http.js';
|
|
6
|
-
import { FileCredentialStore, TOKEN_ENV, WEB_URL_ENV, apiUrlFrom, credentialsPath } from '../../src/infrastructure/credentials.js';
|
|
6
|
+
import { ADMIN_TOKEN_ENV, FileCredentialStore, TOKEN_ENV, WEB_URL_ENV, apiUrlFrom, credentialsPath } from '../../src/infrastructure/credentials.js';
|
|
7
7
|
/**
|
|
8
8
|
* `apex logout`: revoke the token `apex login` saved, then forget it.
|
|
9
9
|
*
|
|
@@ -38,6 +38,8 @@ export default new Command('logout')
|
|
|
38
38
|
console.log(chalk.yellow(`The token could not be revoked (${outcome.revoked.failed}) and may still be valid — revoke it at ${tokensPage}.`));
|
|
39
39
|
process.exitCode = 1;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
for (const name of [TOKEN_ENV, ADMIN_TOKEN_ENV]) {
|
|
42
|
+
if (process.env[name])
|
|
43
|
+
console.log(chalk.yellow(`${name} is still set in this shell, and the CLI still uses it.`));
|
|
44
|
+
}
|
|
43
45
|
});
|
package/dist/cli/reference.js
CHANGED
|
@@ -8,12 +8,21 @@
|
|
|
8
8
|
* fails the test, so nothing reaches the site — or escapes it — unclassified.
|
|
9
9
|
*
|
|
10
10
|
* **`audience` is what a customer can run today, not what the command is
|
|
11
|
-
* for.** A customer's credential is an API token
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* for.** A customer's credential is an API token carrying the permissions its
|
|
12
|
+
* person chose (`resource:read|write`, write implying read), within one
|
|
13
|
+
* Account, their role there and the Account's token policy (control-plane
|
|
14
|
+
* ADR-0071). A token reaches the control plane's repository routes its grants
|
|
15
|
+
* cover, plus its own identity and tokens; every other route — the fleet, the
|
|
16
|
+
* operator's organisation, tenants, billing — answers it 403. So a command is
|
|
17
|
+
* `customer` exactly when every route it calls is a repository or self route in
|
|
18
|
+
* the control plane's `route-access.ts`, and `operator` otherwise, however
|
|
19
|
+
* customer-shaped it looks; a customer command with one operator subcommand or
|
|
20
|
+
* flag hides just that. Local commands need no control plane.
|
|
21
|
+
*
|
|
22
|
+
* **`permission` is the grant a token needs** — on the command, or per
|
|
23
|
+
* subcommand in `subcommandPermissions` (keyed by the path under the command,
|
|
24
|
+
* `subject set`) — read off the same table, so the site can say which
|
|
25
|
+
* `--permission` to ask for. A command that needs none has none.
|
|
17
26
|
*/
|
|
18
27
|
/** The reference's sections, in the order a person meets them. */
|
|
19
28
|
export const GROUPS = [
|
|
@@ -25,14 +34,15 @@ export const GROUPS = [
|
|
|
25
34
|
{ id: 'runners', title: 'Runners' },
|
|
26
35
|
{ id: 'billing', title: 'Usage and cost' },
|
|
27
36
|
];
|
|
28
|
-
/** Calls a control-plane route a customer token is refused on. */
|
|
29
|
-
const
|
|
37
|
+
/** Calls a control-plane route a customer token is refused on, whatever it holds. */
|
|
38
|
+
const OPERATOR = 'operator';
|
|
30
39
|
export const REFERENCE = {
|
|
31
40
|
login: {
|
|
32
41
|
group: 'setup',
|
|
33
42
|
audience: 'customer',
|
|
34
43
|
examples: [
|
|
35
|
-
{ command: 'apex login', description:
|
|
44
|
+
{ command: 'apex login', description: "Sign in through the browser; the token carries your Account's default permissions for your role, and is saved for your user only." },
|
|
45
|
+
{ command: 'apex login --account acc_4f2b9e', description: 'In more than one Account: name the one to sign in to.' },
|
|
36
46
|
{ command: 'apex login --no-browser', description: 'On a machine without a browser: print the approval URL to open elsewhere.' },
|
|
37
47
|
],
|
|
38
48
|
},
|
|
@@ -45,7 +55,7 @@ export const REFERENCE = {
|
|
|
45
55
|
group: 'setup',
|
|
46
56
|
audience: 'customer',
|
|
47
57
|
examples: [
|
|
48
|
-
{ command: 'apex whoami', description:
|
|
58
|
+
{ command: 'apex whoami', description: "Who the CLI acts as, with which credential, and — for a token — what it may do and which Account it belongs to." },
|
|
49
59
|
{ command: 'apex whoami --json', description: 'The same, for a script.' },
|
|
50
60
|
],
|
|
51
61
|
},
|
|
@@ -54,10 +64,18 @@ export const REFERENCE = {
|
|
|
54
64
|
audience: 'customer',
|
|
55
65
|
examples: [
|
|
56
66
|
{
|
|
57
|
-
command: 'apex token create deploy-bot --repo acme/web --expires-in-days 30',
|
|
58
|
-
description: 'Create a token
|
|
67
|
+
command: 'apex token create deploy-bot --permission actions:write --permission secrets:write --repo acme/web --expires-in-days 30',
|
|
68
|
+
description: 'Create a token that may run workflows and set secrets on one repository, approved in the browser, and print it once.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
command: 'apex token edit 0b6f1c2e-9d4a-4f7e-8a31-5c2d9e7b1a40 --remove-permission secrets:write',
|
|
72
|
+
description: 'Narrow a token: applies at once, from the terminal.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
command: 'apex token edit 0b6f1c2e-9d4a-4f7e-8a31-5c2d9e7b1a40 --add-permission deployments:write',
|
|
76
|
+
description: 'Widen a token: the browser opens so you can approve it, within your Account\'s token policy.',
|
|
59
77
|
},
|
|
60
|
-
{ command: 'apex token list --all', description: 'Every token you hold, expired and revoked ones included.' },
|
|
78
|
+
{ command: 'apex token list --all', description: 'Every token you hold by Account, with its permissions — expired and revoked ones included.' },
|
|
61
79
|
{ command: 'apex token revoke 0b6f1c2e-9d4a-4f7e-8a31-5c2d9e7b1a40', description: 'Revoke one token by the id apex token list shows; it stops working at once.' },
|
|
62
80
|
],
|
|
63
81
|
},
|
|
@@ -86,11 +104,12 @@ export const REFERENCE = {
|
|
|
86
104
|
migrate: {
|
|
87
105
|
group: 'local',
|
|
88
106
|
audience: 'customer',
|
|
89
|
-
// `
|
|
90
|
-
|
|
107
|
+
// `scan` reads GitHub or a checkout; `coexist` changes the repository's settings on the control plane.
|
|
108
|
+
subcommandPermissions: { coexist: 'administration:write' },
|
|
91
109
|
examples: [
|
|
92
110
|
{ command: 'apex migrate scan acme', description: "Report what an organization's workflows would do on Apex Actions, read through the GitHub API." },
|
|
93
111
|
{ command: 'apex migrate scan --path .', description: 'Scan a checkout on this machine instead.' },
|
|
112
|
+
{ command: 'apex migrate coexist acme/web --on --add ci.yml', description: 'Run Apex beside GitHub Actions on one repository, starting with one workflow.' },
|
|
94
113
|
],
|
|
95
114
|
},
|
|
96
115
|
secret: {
|
|
@@ -98,6 +117,7 @@ export const REFERENCE = {
|
|
|
98
117
|
audience: 'customer',
|
|
99
118
|
// The organisation scope answers only for the operator's own organisation.
|
|
100
119
|
operatorOptions: ['--org'],
|
|
120
|
+
subcommandPermissions: { list: 'secrets:read', set: 'secrets:write', delete: 'secrets:write' },
|
|
101
121
|
examples: [
|
|
102
122
|
{ command: 'printf %s "$NPM_TOKEN" | apex secret set NPM_TOKEN --repo acme/web', description: 'Set a secret from a pipe; a value never goes on the command line.' },
|
|
103
123
|
{ command: 'apex secret set DEPLOY_KEY --repo acme/web --repo acme/api --from-file key.pem', description: 'Set one value on two repositories, from a file.' },
|
|
@@ -108,32 +128,171 @@ export const REFERENCE = {
|
|
|
108
128
|
group: 'config',
|
|
109
129
|
audience: 'customer',
|
|
110
130
|
operatorOptions: ['--org'],
|
|
131
|
+
subcommandPermissions: { list: 'variables:read', set: 'variables:write', delete: 'variables:write' },
|
|
111
132
|
examples: [
|
|
112
133
|
{ command: 'apex variable set NODE_VERSION --value 24 --repo acme/web', description: 'Set a repository variable; a variable is not secret, so --value is allowed.' },
|
|
113
134
|
{ command: 'apex variable list --repo acme/web --json', description: "A repository's variables and their values, as JSON." },
|
|
114
135
|
],
|
|
115
136
|
},
|
|
116
|
-
//
|
|
117
|
-
logs: {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
// Runs and logs: a run's repository decides, by the token's `actions` grant.
|
|
138
|
+
logs: {
|
|
139
|
+
group: 'runs',
|
|
140
|
+
audience: 'customer',
|
|
141
|
+
permission: 'actions:read',
|
|
142
|
+
subcommandPermissions: { delete: 'actions:write' },
|
|
143
|
+
examples: [
|
|
144
|
+
{ command: 'apex logs latest', description: 'The newest run you can see, every job, as it finished.' },
|
|
145
|
+
{ command: 'apex logs 4812 --job test --follow', description: 'Follow one job of a run until it completes.' },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
search: {
|
|
149
|
+
group: 'runs',
|
|
150
|
+
audience: 'customer',
|
|
151
|
+
permission: 'actions:read',
|
|
152
|
+
examples: [{ command: 'apex search "ECONNRESET" --repo acme/web --conclusion failure', description: "Find a string in the last 30 days of a repository's failed jobs." }],
|
|
153
|
+
},
|
|
154
|
+
why: {
|
|
155
|
+
group: 'runs',
|
|
156
|
+
audience: 'customer',
|
|
157
|
+
permission: 'actions:read',
|
|
158
|
+
examples: [
|
|
159
|
+
{ command: 'apex why 4812', description: 'Why a run happened: the event, the workflow and the rule that matched.' },
|
|
160
|
+
{ command: 'apex why --delivery 7c1e0a52-3b9f-11ef-8e6b-2f4d1c9a0b77', description: 'Why a webhook produced no run at all.' },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
reproduce: {
|
|
164
|
+
group: 'runs',
|
|
165
|
+
audience: 'customer',
|
|
166
|
+
permission: 'actions:read',
|
|
167
|
+
examples: [{ command: 'apex reproduce 4812', description: 'Print the apex run command that replays a past run on this machine; secrets are named, never copied.' }],
|
|
168
|
+
},
|
|
169
|
+
timing: {
|
|
170
|
+
group: 'runs',
|
|
171
|
+
audience: 'customer',
|
|
172
|
+
permission: 'actions:read',
|
|
173
|
+
examples: [{ command: 'apex timing 4812 --steps 3', description: 'Where a run spent its time, with the three slowest steps of each job.' }],
|
|
174
|
+
},
|
|
175
|
+
tests: {
|
|
176
|
+
group: 'runs',
|
|
177
|
+
audience: 'customer',
|
|
178
|
+
permission: 'actions:read',
|
|
179
|
+
examples: [
|
|
180
|
+
{ command: 'apex tests 4812', description: "A run's failing tests." },
|
|
181
|
+
{ command: 'apex tests --flaky --repo acme/web', description: 'Tests that passed and failed at the same commit.' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
rerun: {
|
|
185
|
+
group: 'runs',
|
|
186
|
+
audience: 'customer',
|
|
187
|
+
permission: 'actions:write',
|
|
188
|
+
examples: [
|
|
189
|
+
{ command: 'apex rerun 4812 --failed', description: 'Re-run only what failed, and everything downstream of it.' },
|
|
190
|
+
{ command: 'apex rerun 4812 --failed --resume', description: 'Resume each failed job at the step it failed at, where its snapshot survives.' },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
dispatch: {
|
|
194
|
+
group: 'runs',
|
|
195
|
+
audience: 'customer',
|
|
196
|
+
permission: 'actions:write',
|
|
197
|
+
examples: [{ command: 'apex dispatch deploy.yml --repo acme/web --ref main --input environment=staging', description: 'Trigger a workflow_dispatch run with a typed input.' }],
|
|
198
|
+
},
|
|
199
|
+
// Repository configuration: each resource its own grant.
|
|
200
|
+
environment: {
|
|
201
|
+
group: 'config',
|
|
202
|
+
audience: 'customer',
|
|
203
|
+
subcommandPermissions: { list: 'environments:read', set: 'environments:write', delete: 'environments:write' },
|
|
204
|
+
examples: [
|
|
205
|
+
{ command: 'apex environment list acme/web', description: 'The environments a repository protects, and how.' },
|
|
206
|
+
{ command: 'apex environment set acme/web production --reviewer user:octocat --branch-pattern main', description: 'Require a named reviewer, and deploy only from main.' },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
deployment: {
|
|
210
|
+
group: 'config',
|
|
211
|
+
audience: 'customer',
|
|
212
|
+
// Deciding also needs the person to be one of the environment's named reviewers.
|
|
213
|
+
subcommandPermissions: { list: 'deployments:read', approve: 'deployments:write', reject: 'deployments:write' },
|
|
214
|
+
examples: [
|
|
215
|
+
{ command: 'apex deployment list', description: 'Deployments waiting for an approval, across the repositories you reach.' },
|
|
216
|
+
{ command: 'apex deployment approve 91 --comment "staging looks good"', description: 'Approve one, with a comment recorded beside the decision.' },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
approval: {
|
|
220
|
+
group: 'config',
|
|
221
|
+
audience: 'customer',
|
|
222
|
+
subcommandPermissions: { list: 'approvals:read', approve: 'approvals:write', reject: 'approvals:write' },
|
|
223
|
+
examples: [
|
|
224
|
+
{ command: 'apex approval list', description: 'Runs from forks waiting for somebody to approve them.' },
|
|
225
|
+
{ command: 'apex approval approve 4812', description: 'Approve one: it runs without secrets, on a runner of its own.' },
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
schedule: {
|
|
229
|
+
group: 'config',
|
|
230
|
+
audience: 'customer',
|
|
231
|
+
subcommandPermissions: { list: 'actions:read' },
|
|
232
|
+
examples: [{ command: 'apex schedule list acme/web', description: 'What a repository has scheduled, and when each fires next.' }],
|
|
233
|
+
},
|
|
234
|
+
notify: {
|
|
235
|
+
group: 'config',
|
|
236
|
+
audience: 'customer',
|
|
237
|
+
subcommandPermissions: {
|
|
238
|
+
list: 'notifications:read',
|
|
239
|
+
log: 'notifications:read',
|
|
240
|
+
add: 'notifications:write',
|
|
241
|
+
rm: 'notifications:write',
|
|
242
|
+
test: 'notifications:write',
|
|
243
|
+
},
|
|
244
|
+
examples: [
|
|
245
|
+
{ command: 'apex notify add acme/web ops --kind slack --on broke,recovered', description: 'Tell a Slack channel when the default branch breaks and recovers; the URL is prompted for.' },
|
|
246
|
+
{ command: 'apex notify log acme/web', description: "The last attempts to reach a repository's destinations." },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
actions: {
|
|
250
|
+
group: 'config',
|
|
251
|
+
audience: 'customer',
|
|
252
|
+
// An organisation's action policy is still the operator's.
|
|
253
|
+
operatorSubcommands: ['policy'],
|
|
254
|
+
subcommandPermissions: { list: 'actions:read', pin: 'administration:write' },
|
|
255
|
+
examples: [
|
|
256
|
+
{ command: 'apex actions list acme/web', description: 'Every action a repository pulls in, least fixed first.' },
|
|
257
|
+
{ command: 'apex actions pin acme/web --apply', description: 'Open a pull request pinning each action to the commit its version points at.' },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
oidc: {
|
|
261
|
+
group: 'config',
|
|
262
|
+
audience: 'customer',
|
|
263
|
+
subcommandPermissions: { subject: 'actions:read', 'subject set': 'actions:write' },
|
|
264
|
+
examples: [
|
|
265
|
+
{ command: 'apex oidc discovery', description: 'The discovery document a cloud provider fetches to trust these tokens.' },
|
|
266
|
+
{ command: 'apex oidc subject set acme/web --claim repo context job_workflow_ref', description: "Fold the calling workflow into a repository's subject claim." },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
cache: {
|
|
270
|
+
group: 'storage',
|
|
271
|
+
audience: 'customer',
|
|
272
|
+
subcommandPermissions: { list: 'actions:read', delete: 'actions:write' },
|
|
273
|
+
examples: [
|
|
274
|
+
{ command: 'apex cache list acme/web', description: 'The caches a repository holds, most recently used first.' },
|
|
275
|
+
{ command: 'apex cache delete acme/web 318', description: 'Drop one entry by id.' },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
artifact: {
|
|
279
|
+
group: 'storage',
|
|
280
|
+
audience: 'customer',
|
|
281
|
+
subcommandPermissions: { list: 'actions:read', download: 'actions:read', delete: 'actions:write' },
|
|
282
|
+
examples: [
|
|
283
|
+
{ command: 'apex artifact list 4812', description: 'What a run produced.' },
|
|
284
|
+
{ command: 'apex artifact download 4812 coverage -o coverage.zip', description: "Download one of a run's artifacts as a zip." },
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
spend: {
|
|
288
|
+
group: 'billing',
|
|
289
|
+
audience: 'customer',
|
|
290
|
+
// With a token, one repository at a time; the fleet-wide report is the operator's.
|
|
291
|
+
permission: 'actions:read',
|
|
292
|
+
examples: [{ command: 'apex spend --repo acme/web --days 7', description: "A repository's last week of cost, next to what GitHub would have charged." }],
|
|
293
|
+
},
|
|
294
|
+
// The operator's: the fleet, the customer agreement's escalation path, and departed tenants' data.
|
|
295
|
+
runner: { group: 'runners', audience: OPERATOR },
|
|
296
|
+
terms: { group: 'billing', audience: OPERATOR },
|
|
297
|
+
tenant: { group: 'billing', audience: OPERATOR },
|
|
139
298
|
};
|
package/dist/cli/spend/main.js
CHANGED
|
@@ -74,7 +74,7 @@ const handler = async (options) => {
|
|
|
74
74
|
};
|
|
75
75
|
export default new Command('spend')
|
|
76
76
|
.description('Show what the fleet cost, next to what the same work would have cost on GitHub')
|
|
77
|
-
.option('--repo <owner/repo>', 'one repository (default: every repository)')
|
|
77
|
+
.option('--repo <owner/repo>', 'one repository — required with an API token (default for the operator: every repository)')
|
|
78
78
|
.option('--days <n>', 'window in days, clamped to 1–365 (default: 30)')
|
|
79
79
|
.option('--workflows <n>', `workflows to list (default: ${DEFAULT_ROWS})`)
|
|
80
80
|
.option('--json', 'print the report as JSON')
|