@apex-actions/cli 0.67.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.
Files changed (81) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +84 -0
  3. package/THIRD-PARTY-NOTICES.md +1226 -0
  4. package/dist/cli/actions/main.js +193 -0
  5. package/dist/cli/approval/main.js +76 -0
  6. package/dist/cli/artifact/main.js +115 -0
  7. package/dist/cli/cache/main.js +104 -0
  8. package/dist/cli/deployment/main.js +73 -0
  9. package/dist/cli/dispatch/main.js +47 -0
  10. package/dist/cli/environment/main.js +139 -0
  11. package/dist/cli/login/main.js +73 -0
  12. package/dist/cli/logout/main.js +43 -0
  13. package/dist/cli/logs/main.js +125 -0
  14. package/dist/cli/lsp/main.js +46 -0
  15. package/dist/cli/main.js +88 -0
  16. package/dist/cli/migrate/main.js +275 -0
  17. package/dist/cli/notify/main.js +257 -0
  18. package/dist/cli/oidc/main.js +103 -0
  19. package/dist/cli/reproduce/main.js +73 -0
  20. package/dist/cli/rerun/main.js +100 -0
  21. package/dist/cli/run/main.js +50 -0
  22. package/dist/cli/runner/main.js +238 -0
  23. package/dist/cli/schedule/main.js +54 -0
  24. package/dist/cli/search/main.js +123 -0
  25. package/dist/cli/secret/main.js +102 -0
  26. package/dist/cli/secret/shared.js +104 -0
  27. package/dist/cli/spend/main.js +81 -0
  28. package/dist/cli/terms/main.js +85 -0
  29. package/dist/cli/tests/main.js +122 -0
  30. package/dist/cli/timing/main.js +90 -0
  31. package/dist/cli/validate/main.js +48 -0
  32. package/dist/cli/variable/main.js +83 -0
  33. package/dist/cli/whoami/main.js +63 -0
  34. package/dist/cli/why/main.js +106 -0
  35. package/dist/src/application/authenticate.js +56 -0
  36. package/dist/src/application/dispatch-workflow.js +23 -0
  37. package/dist/src/application/explain-runs.js +40 -0
  38. package/dist/src/application/index.js +2 -0
  39. package/dist/src/application/inspect-runs.js +29 -0
  40. package/dist/src/application/manage-actions.js +26 -0
  41. package/dist/src/application/manage-approvals.js +24 -0
  42. package/dist/src/application/manage-artifacts.js +79 -0
  43. package/dist/src/application/manage-caches.js +41 -0
  44. package/dist/src/application/manage-environments.js +136 -0
  45. package/dist/src/application/manage-notifications.js +20 -0
  46. package/dist/src/application/manage-runners.js +89 -0
  47. package/dist/src/application/manage-secrets.js +83 -0
  48. package/dist/src/application/manage-terms.js +34 -0
  49. package/dist/src/application/read-logs.js +40 -0
  50. package/dist/src/application/report-spend.js +15 -0
  51. package/dist/src/application/rerun-runs.js +34 -0
  52. package/dist/src/application/resolve-run.js +19 -0
  53. package/dist/src/application/run-workflow.js +17 -0
  54. package/dist/src/application/scan-migration.js +76 -0
  55. package/dist/src/application/search-logs.js +24 -0
  56. package/dist/src/application/serve-language-server.js +15 -0
  57. package/dist/src/application/validate-workflows.js +12 -0
  58. package/dist/src/domain/credentials.js +82 -0
  59. package/dist/src/domain/fleet-history.js +136 -0
  60. package/dist/src/domain/index.js +2 -0
  61. package/dist/src/domain/insight.js +42 -0
  62. package/dist/src/domain/logsearch.js +40 -0
  63. package/dist/src/domain/migration.js +122 -0
  64. package/dist/src/domain/notify.js +64 -0
  65. package/dist/src/domain/outcomediff.js +40 -0
  66. package/dist/src/domain/provenance.js +63 -0
  67. package/dist/src/domain/runs.js +90 -0
  68. package/dist/src/domain/secrets.js +39 -0
  69. package/dist/src/domain/spend.js +50 -0
  70. package/dist/src/domain/supplychain.js +48 -0
  71. package/dist/src/domain/terms.js +16 -0
  72. package/dist/src/domain/validation.js +4 -0
  73. package/dist/src/infrastructure/control-plane-http.js +458 -0
  74. package/dist/src/infrastructure/credentials.js +134 -0
  75. package/dist/src/infrastructure/engine-binary.js +164 -0
  76. package/dist/src/infrastructure/github-api.js +154 -0
  77. package/dist/src/infrastructure/index.js +2 -0
  78. package/dist/src/infrastructure/loopback-login.js +136 -0
  79. package/dist/src/infrastructure/runner-binary.js +74 -0
  80. package/dist/src/infrastructure/secret-input.js +52 -0
  81. package/package.json +54 -0
@@ -0,0 +1,193 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { listActions, proposePins, readPolicy, writePolicy } from '../../src/application/manage-actions.js';
4
+ import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
5
+ import { inventorySummary, patternList, pinningLabel, policyLine, } from '../../src/domain/supplychain.js';
6
+ import { parseRepository } from '../../src/domain/runs.js';
7
+ /** Build the API client, or explain what is missing and stop. */
8
+ const api = () => {
9
+ try {
10
+ return new HttpControlPlane();
11
+ }
12
+ catch (err) {
13
+ console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
14
+ process.exitCode = 2;
15
+ return undefined;
16
+ }
17
+ };
18
+ const repository = (slug) => {
19
+ const parsed = parseRepository(slug);
20
+ if (parsed)
21
+ return parsed;
22
+ console.error(chalk.red(`expected owner/repo, got ${JSON.stringify(slug)}`));
23
+ process.exitCode = 2;
24
+ return undefined;
25
+ };
26
+ const printInventory = (inv) => {
27
+ console.log(chalk.bold(`${inv.repository} · ${inv.ref.slice(0, 7)}`));
28
+ console.log(chalk.dim(` policy ${policyLine(inv.policy)}`));
29
+ console.log('');
30
+ if (inv.references.length === 0) {
31
+ console.log(chalk.yellow(' This repository pulls in no third-party actions.'));
32
+ }
33
+ else {
34
+ for (const ref of inv.references) {
35
+ // Movable is the state worth noticing, so it is the one with a colour.
36
+ const label = ref.pinning === 'movable' ? chalk.yellow(pinningLabel(ref.pinning)) : chalk.dim(pinningLabel(ref.pinning));
37
+ const from = ref.pinnedFrom === undefined || ref.pinnedFrom === '' ? '' : chalk.dim(` (${ref.pinnedFrom})`);
38
+ console.log(` ${ref.ref.padEnd(60)} ${label}${from}`);
39
+ console.log(chalk.dim(` ${ref.uses.map((u) => `${u.workflowPath}${u.line === undefined ? '' : `:${u.line}`}`).join(', ')}`));
40
+ }
41
+ console.log(chalk.dim(`\n ${inventorySummary(inv)}`));
42
+ }
43
+ const findings = inv.workflows.flatMap((w) => w.findings);
44
+ if (findings.length > 0) {
45
+ console.log(chalk.bold('\n what the policy says'));
46
+ for (const f of findings) {
47
+ console.log(` ${chalk.red(f.code.padEnd(12))} ${f.workflowPath}${f.line === undefined ? '' : `:${f.line}`} ${f.reason}`);
48
+ }
49
+ }
50
+ // Never behind a flag: what the control does not cover arrives with what it
51
+ // does, so a clean list cannot be read as a safe repository (ADR-0025).
52
+ console.log('');
53
+ for (const caveat of inv.caveats)
54
+ console.log(chalk.dim(` note: ${caveat}`));
55
+ };
56
+ const printProposal = (proposal, applied) => {
57
+ if (proposal.files.length === 0 && proposal.unresolved.length === 0) {
58
+ console.log(chalk.green(`${proposal.repository} is already pinned to the commits its versions point at.`));
59
+ return;
60
+ }
61
+ console.log(chalk.bold(`${proposal.repository} · against ${proposal.base}`));
62
+ for (const file of proposal.files) {
63
+ console.log(`\n ${chalk.bold(file.path)}`);
64
+ for (const change of file.changes) {
65
+ console.log(` ${change.ref}`);
66
+ console.log(` → ${chalk.green(change.to)}${change.from === '' ? '' : chalk.dim(` # ${change.from}`)}`);
67
+ }
68
+ }
69
+ if (proposal.unresolved.length > 0) {
70
+ console.log(chalk.bold('\n left alone'));
71
+ for (const u of proposal.unresolved)
72
+ console.log(` ${u.ref} ${chalk.dim(`in ${u.workflowPath} — ${u.reason}`)}`);
73
+ }
74
+ console.log('');
75
+ if (proposal.pullRequest !== undefined) {
76
+ const verb = proposal.pullRequest.created ? 'opened' : 'updated';
77
+ console.log(`${chalk.green('✔')} pull request ${verb}: ${proposal.pullRequest.url}`);
78
+ }
79
+ else if (applied) {
80
+ console.log(chalk.yellow(' nothing to propose, so no pull request was opened'));
81
+ }
82
+ else {
83
+ console.log(chalk.dim(' preview only — rerun with --apply to open the pull request'));
84
+ }
85
+ console.log(chalk.dim('\n note: a new release and a moved tag produce the same diff here. Both change the code this repository runs.'));
86
+ };
87
+ const listCommand = new Command('list')
88
+ .description('Every action a repository pulls in, least fixed first')
89
+ .argument('<repository>', 'owner/repo')
90
+ .option('--json', 'machine-readable output')
91
+ .action(async (slug, opts) => {
92
+ const target = repository(slug);
93
+ if (!target)
94
+ return;
95
+ const client = api();
96
+ if (!client)
97
+ return;
98
+ const outcome = await listActions(client, target);
99
+ if (outcome.kind === 'refused') {
100
+ console.error(chalk.red(outcome.message));
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ if (opts.json === true)
105
+ console.log(JSON.stringify(outcome.value, null, 2));
106
+ else
107
+ printInventory(outcome.value);
108
+ });
109
+ const policyCommand = new Command('policy')
110
+ .description("Read or replace an organisation's action policy")
111
+ .argument('<owner>', 'the organisation')
112
+ .option('--mode <mode>', 'off · audit · enforce — omit to read the policy rather than change it')
113
+ .option('--allow <patterns...>', 'patterns that may be used; empty means everything not denied')
114
+ .option('--deny <patterns...>', 'patterns that may not be used; deny wins over allow')
115
+ // Deliberately no `--no-require-pinned`: commander gives a flag that has a
116
+ // `--no-` twin a default of `true`, which would silently turn on the
117
+ // strictest rule in the policy for anybody who omitted both. Setting a
118
+ // policy replaces it, so leaving this off is how you allow tags.
119
+ .option('--require-pinned', 'require a commit SHA (or an image digest); a version tag does not count. Omit to permit tags')
120
+ .option('--json', 'machine-readable output')
121
+ .action(async (owner, opts) => {
122
+ const client = api();
123
+ if (!client)
124
+ return;
125
+ // Reading is the default. Changing an organisation's policy is not
126
+ // something to fall into by leaving a flag off.
127
+ if (opts.mode === undefined) {
128
+ const current = await readPolicy(client, owner);
129
+ if (current.kind === 'refused') {
130
+ console.error(chalk.red(current.message));
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ if (opts.json === true)
135
+ console.log(JSON.stringify(current.value, null, 2));
136
+ else {
137
+ console.log(chalk.bold(owner));
138
+ console.log(` ${policyLine(current.value)}`);
139
+ if (current.value.updatedBy !== undefined)
140
+ console.log(chalk.dim(` last changed by ${current.value.updatedBy}`));
141
+ }
142
+ return;
143
+ }
144
+ if (!['off', 'audit', 'enforce'].includes(opts.mode)) {
145
+ console.error(chalk.red(`--mode must be off, audit or enforce, got ${opts.mode}`));
146
+ process.exitCode = 2;
147
+ return;
148
+ }
149
+ const outcome = await writePolicy(client, owner, {
150
+ mode: opts.mode,
151
+ allow: patternList(opts.allow),
152
+ deny: patternList(opts.deny),
153
+ requirePinned: opts.requirePinned === true,
154
+ });
155
+ if (outcome.kind === 'refused') {
156
+ console.error(chalk.red(outcome.message));
157
+ process.exitCode = 1;
158
+ return;
159
+ }
160
+ if (opts.json === true)
161
+ console.log(JSON.stringify(outcome.value, null, 2));
162
+ else
163
+ console.log(`${chalk.green('✔')} ${owner}: ${policyLine(outcome.value)}`);
164
+ });
165
+ const pinCommand = new Command('pin')
166
+ .description('Propose pinning every action to the commit its version points at')
167
+ .argument('<repository>', 'owner/repo')
168
+ .option('--apply', 'open the pull request; without this the change is only printed')
169
+ .option('--json', 'machine-readable output')
170
+ .action(async (slug, opts) => {
171
+ const target = repository(slug);
172
+ if (!target)
173
+ return;
174
+ const client = api();
175
+ if (!client)
176
+ return;
177
+ const apply = opts.apply === true;
178
+ const outcome = await proposePins(client, target, apply);
179
+ if (outcome.kind === 'refused') {
180
+ console.error(chalk.red(outcome.message));
181
+ process.exitCode = 1;
182
+ return;
183
+ }
184
+ if (opts.json === true)
185
+ console.log(JSON.stringify(outcome.value, null, 2));
186
+ else
187
+ printProposal(outcome.value, apply);
188
+ });
189
+ export default new Command('actions')
190
+ .description('The action supply chain: what a repository pulls in, the policy over it, and pinning')
191
+ .addCommand(listCommand)
192
+ .addCommand(policyCommand)
193
+ .addCommand(pinCommand);
@@ -0,0 +1,76 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { decideRun, listWaitingRuns, waitingSummary, } from '../../src/application/manage-approvals.js';
4
+ import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
5
+ const api = () => {
6
+ try {
7
+ return new HttpControlPlane();
8
+ }
9
+ catch (err) {
10
+ console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
11
+ process.exitCode = 2;
12
+ return undefined;
13
+ }
14
+ };
15
+ const printWaiting = (runs) => {
16
+ if (runs.length === 0) {
17
+ console.log(chalk.green('Nothing is waiting for approval.'));
18
+ return;
19
+ }
20
+ console.log(chalk.bold(['RUN', 'WHAT', 'WHY'].join(' ')));
21
+ for (const r of runs)
22
+ console.log([r.id, waitingSummary(r), r.why].join(' '));
23
+ };
24
+ const listCommand = new Command('list')
25
+ .description('Runs from a fork waiting for somebody with write access to approve them')
26
+ .option('--json', 'machine-readable output')
27
+ .action(async (opts) => {
28
+ const client = api();
29
+ if (!client)
30
+ return;
31
+ const outcome = await listWaitingRuns(client);
32
+ if (outcome.kind === 'refused') {
33
+ console.error(chalk.red(outcome.detail));
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ if (opts.json === true) {
38
+ console.log(JSON.stringify({ runs: outcome.runs }, null, 2));
39
+ return;
40
+ }
41
+ printWaiting(outcome.runs);
42
+ });
43
+ const decide = (verb) => new Command(verb)
44
+ .description(verb === 'approve'
45
+ ? 'Approve one run from a fork — it starts with no secrets and on a runner of its own'
46
+ : 'Reject one run from a fork — every job fails and the attempt is closed')
47
+ .argument('<run-id>', 'run id (apex approval list prints them)')
48
+ .option('--comment <text>', 'recorded with the decision')
49
+ .action(async (runId, opts) => {
50
+ const client = api();
51
+ if (!client)
52
+ return;
53
+ const outcome = await decideRun(client, runId, verb === 'approve' ? 'approved' : 'rejected', opts.comment);
54
+ switch (outcome.kind) {
55
+ case 'not-found':
56
+ console.error(chalk.red(`No run ${runId}.`));
57
+ process.exitCode = 1;
58
+ return;
59
+ case 'refused':
60
+ console.error(chalk.red(outcome.detail));
61
+ process.exitCode = 1;
62
+ return;
63
+ case 'decided':
64
+ // Say what approving *did*, not only that it happened: the next
65
+ // question is always "so does it have my secrets now", and the
66
+ // answer is no.
67
+ console.log(verb === 'approve'
68
+ ? chalk.green(`Approved ${outcome.decision.runId}. It runs with no secrets, a read-only token, and a runner of its own.`)
69
+ : chalk.yellow(`Rejected ${outcome.decision.runId}. Its jobs are closed as failed.`));
70
+ }
71
+ });
72
+ export default new Command('approval')
73
+ .description('Runs from forks: what is waiting, and deciding it (ADR-0024)')
74
+ .addCommand(listCommand)
75
+ .addCommand(decide('approve'))
76
+ .addCommand(decide('reject'));
@@ -0,0 +1,115 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { Readable } from 'node:stream';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { basename, resolve } from 'node:path';
5
+ import { Command } from 'commander';
6
+ import chalk from 'chalk';
7
+ import { deleteArtifact, downloadArtifact, listArtifacts, untilExpiry, } from '../../src/application/manage-artifacts.js';
8
+ import { humanBytes } from '../../src/application/manage-caches.js';
9
+ import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
10
+ /** Build the API client, or explain what is missing and stop. */
11
+ const api = () => {
12
+ try {
13
+ return new HttpControlPlane();
14
+ }
15
+ catch (err) {
16
+ console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
17
+ process.exitCode = 2;
18
+ return undefined;
19
+ }
20
+ };
21
+ const printArtifacts = (artifacts, totalSizeBytes) => {
22
+ if (artifacts.length === 0) {
23
+ console.log(chalk.yellow('No artifacts. The first workflow to run `actions/upload-artifact` will create one.'));
24
+ return;
25
+ }
26
+ console.log(chalk.bold(['ID', 'SIZE', 'EXPIRES', 'NAME'].join(' ')));
27
+ for (const a of artifacts) {
28
+ console.log([String(a.id), humanBytes(a.sizeBytes), untilExpiry(a.expiresAt), a.name].join(' '));
29
+ }
30
+ // Nothing renews retention, so the column above is a countdown rather than a
31
+ // property — say so instead of leaving it to be discovered.
32
+ console.log(chalk.dim(`\n${artifacts.length} artifact(s), ${humanBytes(totalSizeBytes)}. Retention is not renewed: an expired artifact is deleted with its bytes.`));
33
+ };
34
+ const listCommand = new Command('list')
35
+ .description('Artifacts a run or a repository holds, newest first')
36
+ .argument('<target>', 'run id, or owner/repo')
37
+ .option('--json', 'machine-readable output')
38
+ .action(async (target, opts) => {
39
+ const client = api();
40
+ if (!client)
41
+ return;
42
+ const outcome = await listArtifacts(client, target);
43
+ if (outcome.kind === 'refused') {
44
+ console.error(chalk.red(outcome.detail));
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+ if (opts.json === true) {
49
+ console.log(JSON.stringify({ artifacts: outcome.artifacts, totalSizeBytes: outcome.totalSizeBytes }, null, 2));
50
+ return;
51
+ }
52
+ printArtifacts(outcome.artifacts, outcome.totalSizeBytes);
53
+ });
54
+ const downloadCommand = new Command('download')
55
+ .description('Download one artifact of a run as a zip')
56
+ .argument('<run>', 'run id')
57
+ .argument('[name]', 'artifact name (optional when the run produced exactly one)')
58
+ .option('-o, --output <path>', 'where to write the zip (default: <name>.zip here)')
59
+ .action(async (runId, name, opts) => {
60
+ const client = api();
61
+ if (!client)
62
+ return;
63
+ const outcome = await downloadArtifact(client, runId, name);
64
+ switch (outcome.kind) {
65
+ case 'not-found':
66
+ console.error(chalk.red(`run ${runId} has no artifact named ${outcome.name}`));
67
+ process.exitCode = 1;
68
+ return;
69
+ case 'ambiguous':
70
+ // Guessing here would silently write the wrong zip to the right path.
71
+ console.error(chalk.red(`run ${runId} has ${outcome.names.length} artifacts; name one of: ${outcome.names.join(', ')}`));
72
+ process.exitCode = 2;
73
+ return;
74
+ case 'refused':
75
+ console.error(chalk.red(outcome.detail));
76
+ process.exitCode = 1;
77
+ return;
78
+ case 'downloaded': {
79
+ const target = resolve(opts.output ?? `${basename(outcome.artifact.name)}.zip`);
80
+ await pipeline(Readable.from(outcome.body), createWriteStream(target));
81
+ console.log(`${chalk.green('✔')} ${outcome.artifact.name} → ${target} (${humanBytes(outcome.artifact.sizeBytes)})`);
82
+ }
83
+ }
84
+ });
85
+ const deleteCommand = new Command('delete')
86
+ .description('Delete one artifact by id')
87
+ .argument('<run>', 'run id')
88
+ .argument('<id>', 'artifact id (apex artifact list prints them)')
89
+ .action(async (runId, id) => {
90
+ const client = api();
91
+ if (!client)
92
+ return;
93
+ const outcome = await deleteArtifact(client, runId, id);
94
+ switch (outcome.kind) {
95
+ case 'bad-id':
96
+ console.error(chalk.red(`artifact ids are positive whole numbers, got ${JSON.stringify(outcome.given)}`));
97
+ process.exitCode = 2;
98
+ return;
99
+ case 'not-found':
100
+ console.error(chalk.red(`no artifact ${outcome.id} in run ${runId}`));
101
+ process.exitCode = 1;
102
+ return;
103
+ case 'refused':
104
+ console.error(chalk.red(outcome.detail));
105
+ process.exitCode = 1;
106
+ return;
107
+ case 'deleted':
108
+ console.log(`${chalk.green('✔')} artifact ${id} deleted`);
109
+ }
110
+ });
111
+ export default new Command('artifact')
112
+ .description('Artifacts: what a run produced, getting it out, and dropping one')
113
+ .addCommand(listCommand)
114
+ .addCommand(downloadCommand)
115
+ .addCommand(deleteCommand);
@@ -0,0 +1,104 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { deleteCache, humanBytes, listCaches, splitRepository } from '../../src/application/manage-caches.js';
4
+ import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
5
+ /** Build the API client, or explain what is missing and stop. */
6
+ const api = () => {
7
+ try {
8
+ return new HttpControlPlane();
9
+ }
10
+ catch (err) {
11
+ console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
12
+ process.exitCode = 2;
13
+ return undefined;
14
+ }
15
+ };
16
+ const repository = (slug) => {
17
+ const parsed = splitRepository(slug);
18
+ if (parsed)
19
+ return parsed;
20
+ console.error(chalk.red(`expected owner/repo, got ${JSON.stringify(slug)}`));
21
+ process.exitCode = 2;
22
+ return undefined;
23
+ };
24
+ const age = (iso) => {
25
+ const seconds = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
26
+ if (seconds < 90)
27
+ return `${seconds}s`;
28
+ if (seconds < 5400)
29
+ return `${Math.round(seconds / 60)}m`;
30
+ if (seconds < 172_800)
31
+ return `${Math.round(seconds / 3600)}h`;
32
+ return `${Math.round(seconds / 86_400)}d`;
33
+ };
34
+ const printCaches = (caches, totalSizeBytes) => {
35
+ if (caches.length === 0) {
36
+ console.log(chalk.yellow('No caches. The first workflow to run `actions/cache` will create one.'));
37
+ return;
38
+ }
39
+ console.log(chalk.bold(['ID', 'SIZE', 'USED', 'REF', 'KEY'].join(' ')));
40
+ for (const c of caches) {
41
+ console.log([String(c.id), humanBytes(c.sizeBytes), age(c.lastAccessedAt), c.ref, c.key].join(' '));
42
+ }
43
+ // The list is newest-used first, so the bottom row is what eviction takes
44
+ // next. Say so rather than making people infer it from the column.
45
+ console.log(chalk.dim(`\n${caches.length} cache(s), ${humanBytes(totalSizeBytes)}. Least recently used is last, and goes first when the quota is reached.`));
46
+ };
47
+ const listCommand = new Command('list')
48
+ .description('Caches a repository holds, most recently used first')
49
+ .argument('<repository>', 'owner/repo')
50
+ .option('--json', 'machine-readable output')
51
+ .action(async (slug, opts) => {
52
+ const target = repository(slug);
53
+ if (!target)
54
+ return;
55
+ const client = api();
56
+ if (!client)
57
+ return;
58
+ const outcome = await listCaches(client, target.owner, target.repo);
59
+ if (outcome.kind === 'refused') {
60
+ console.error(chalk.red(outcome.detail));
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+ if (opts.json === true) {
65
+ console.log(JSON.stringify({ caches: outcome.caches, totalSizeBytes: outcome.totalSizeBytes }, null, 2));
66
+ return;
67
+ }
68
+ printCaches(outcome.caches, outcome.totalSizeBytes);
69
+ });
70
+ const deleteCommand = new Command('delete')
71
+ .description('Delete one cache entry by id')
72
+ .argument('<repository>', 'owner/repo')
73
+ .argument('<id>', 'cache id (apex cache list prints them)')
74
+ .action(async (slug, id) => {
75
+ const target = repository(slug);
76
+ if (!target)
77
+ return;
78
+ const client = api();
79
+ if (!client)
80
+ return;
81
+ const outcome = await deleteCache(client, target.owner, target.repo, id);
82
+ switch (outcome.kind) {
83
+ case 'bad-id':
84
+ console.error(chalk.red(`cache ids are positive whole numbers, got ${JSON.stringify(outcome.given)}`));
85
+ process.exitCode = 2;
86
+ return;
87
+ case 'not-found':
88
+ console.error(chalk.red(`no cache ${outcome.id} in ${slug}`));
89
+ process.exitCode = 1;
90
+ return;
91
+ case 'refused':
92
+ console.error(chalk.red(outcome.detail));
93
+ process.exitCode = 1;
94
+ return;
95
+ case 'deleted':
96
+ // The stored bytes may survive: another entry can point at the same
97
+ // content. Saying "deleted" of the entry is the honest claim.
98
+ console.log(`${chalk.green('✔')} cache ${id} deleted`);
99
+ }
100
+ });
101
+ export default new Command('cache')
102
+ .description('Caches: what a repository holds, and dropping an entry')
103
+ .addCommand(listCommand)
104
+ .addCommand(deleteCommand);
@@ -0,0 +1,73 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { decideDeployment, deploymentSummary, listDeployments, } from '../../src/application/manage-environments.js';
4
+ import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
5
+ const api = () => {
6
+ try {
7
+ return new HttpControlPlane();
8
+ }
9
+ catch (err) {
10
+ console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
11
+ process.exitCode = 2;
12
+ return undefined;
13
+ }
14
+ };
15
+ const printDeployments = (deployments, scoped) => {
16
+ if (deployments.length === 0) {
17
+ console.log(chalk.yellow(scoped ? 'This run deployed nowhere.' : 'Nothing is waiting for approval.'));
18
+ return;
19
+ }
20
+ console.log(chalk.bold(['ID', 'ENVIRONMENT', 'STATUS', 'WHY'].join(' ')));
21
+ for (const d of deployments)
22
+ console.log([d.id, d.environment, d.status, deploymentSummary(d)].join(' '));
23
+ };
24
+ const listCommand = new Command('list')
25
+ .description('Deployments waiting for approval, or every deployment of one run')
26
+ .option('--run <id>', 'one run, in the order it deployed')
27
+ .option('--json', 'machine-readable output')
28
+ .action(async (opts) => {
29
+ const client = api();
30
+ if (!client)
31
+ return;
32
+ const outcome = await listDeployments(client, opts.run);
33
+ if (outcome.kind === 'refused') {
34
+ console.error(chalk.red(outcome.detail));
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+ if (opts.json === true) {
39
+ console.log(JSON.stringify({ deployments: outcome.deployments }, null, 2));
40
+ return;
41
+ }
42
+ printDeployments(outcome.deployments, opts.run !== undefined);
43
+ });
44
+ const decide = (verb) => new Command(verb)
45
+ .description(verb === 'approve' ? 'Approve one waiting deployment' : 'Reject one waiting deployment')
46
+ .argument('<id>', 'deployment id (apex deployment list prints them)')
47
+ .option('--comment <text>', 'recorded with the decision')
48
+ .action(async (id, opts) => {
49
+ const client = api();
50
+ if (!client)
51
+ return;
52
+ const outcome = await decideDeployment(client, id, verb === 'approve' ? 'approved' : 'rejected', opts.comment);
53
+ switch (outcome.kind) {
54
+ case 'not-found':
55
+ console.error(chalk.red(`no deployment ${id}`));
56
+ process.exitCode = 1;
57
+ return;
58
+ case 'refused':
59
+ // Whether this caller may decide is the environment's question, and
60
+ // the answer names the environment. Print it as it came.
61
+ console.error(chalk.red(outcome.detail));
62
+ process.exitCode = 1;
63
+ return;
64
+ case 'decided':
65
+ console.log(`${chalk.green('✔')} ${outcome.deployment.environment} ${verb === 'approve' ? 'approved' : 'rejected'}`);
66
+ console.log(chalk.dim(`run ${outcome.deployment.runId}`));
67
+ }
68
+ });
69
+ export default new Command('deployment')
70
+ .description('Deployments: what is waiting for an approval, and giving one')
71
+ .addCommand(listCommand)
72
+ .addCommand(decide('approve'))
73
+ .addCommand(decide('reject'));
@@ -0,0 +1,47 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { dispatchWorkflow } from '../../src/application/dispatch-workflow.js';
4
+ import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
5
+ const handler = async (workflow, options) => {
6
+ let api;
7
+ try {
8
+ api = new HttpControlPlane();
9
+ }
10
+ catch (err) {
11
+ console.error(chalk.red(err instanceof NotConfiguredError ? err.message : String(err)));
12
+ process.exitCode = 2;
13
+ return;
14
+ }
15
+ const outcome = await dispatchWorkflow(api, {
16
+ repository: options.repo,
17
+ workflow,
18
+ ref: options.ref ?? 'main',
19
+ inputs: options.input ?? [],
20
+ });
21
+ switch (outcome.kind) {
22
+ case 'bad-repository':
23
+ console.error(chalk.red(`--repo must be owner/repo, got ${outcome.given}`));
24
+ process.exitCode = 2;
25
+ return;
26
+ case 'bad-inputs':
27
+ console.error(chalk.red(`--input must be name=value: ${outcome.invalid.join(', ')}`));
28
+ process.exitCode = 2;
29
+ return;
30
+ case 'refused':
31
+ // The API's own words: unknown repository, disabled workflow, a required
32
+ // input left out. Nothing is re-worded here.
33
+ console.error(chalk.red(outcome.detail));
34
+ process.exitCode = 1;
35
+ return;
36
+ case 'dispatched':
37
+ console.log(`${chalk.green('✔')} dispatched ${workflow} on ${options.ref ?? 'main'}`);
38
+ console.log(chalk.dim(`delivery ${outcome.deliveryId} — follow it with: apex logs latest --repo ${options.repo} --follow`));
39
+ }
40
+ };
41
+ export default new Command('dispatch')
42
+ .description('Trigger workflow_dispatch with typed inputs')
43
+ .argument('<workflow>', 'workflow file path or name, e.g. .github/workflows/deploy.yml')
44
+ .requiredOption('--repo <owner/repo>', 'repository to dispatch in')
45
+ .option('--ref <ref>', 'branch or tag to run against, as on GitHub (a commit SHA is refused)', 'main')
46
+ .option('--input <name=value...>', 'workflow_dispatch input, repeatable')
47
+ .action(handler);