breakaway 2.0.0-main.61 → 2.0.0-main.63
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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/tasks/cli.js +1 -1
- package/scripts/tasks/infra-adopt.js +115 -0
- package/scripts/tasks.mjs +20 -0
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "2.0.0-main.
|
|
3
|
+
"version": "2.0.0-main.63",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "breakaway",
|
|
9
|
-
"version": "2.0.0-main.
|
|
9
|
+
"version": "2.0.0-main.63",
|
|
10
10
|
"license": "FSL-1.1-Apache-2.0",
|
|
11
11
|
"bin": {
|
|
12
12
|
"breakaway": "scripts/tasks.mjs"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "2.0.0-main.
|
|
3
|
+
"version": "2.0.0-main.63",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/scripts/tasks/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ export const SUBCOMMANDS = {
|
|
|
29
29
|
'listen',
|
|
30
30
|
],
|
|
31
31
|
specs: ['list', 'show'],
|
|
32
|
-
infra: ['init', 'runner', 'check', 'act', ...INFRA_READS],
|
|
32
|
+
infra: ['init', 'runner', 'check', 'adopt', 'act', ...INFRA_READS],
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
/** Commands that take nothing after their name, so a word there is a mistake (an old copy's missing subcommand, say). */
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npx breakaway infra adopt <environment>` (CLI-23, docs/specs/IDEA-19-architect.md, "Desired state"): writes the
|
|
3
|
+
* board's draft of an environment's desired state into the checkout, so nobody hand-writes
|
|
4
|
+
* `.github/breakaway-infra/<environment>.json`. The board drafts it from what runs there (BRK-240,
|
|
5
|
+
* GET /api/infra/environments/<environment>/draft, read only, so an agent's token may ask); this writes it where it
|
|
6
|
+
* goes, then checks it the way `infra check` does and says what to commit.
|
|
7
|
+
*
|
|
8
|
+
* Files only: it never makes a plan and never applies anything. It never overwrites a file without --force, and it
|
|
9
|
+
* writes nothing for an observe-only environment, which takes no desired state. The path is worked out here from the
|
|
10
|
+
* environment's name, never taken from the board's answer, so the file can't land anywhere else.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { checkDesiredFile, desiredPath, environmentOfFile } from '../../src/infra-desired.js';
|
|
15
|
+
import { infraCheck, readInfraFolder } from './infra-check.js';
|
|
16
|
+
|
|
17
|
+
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs `infra adopt`. `code` is what the CLI exits with: 1 when nothing was written, or the written file doesn't check.
|
|
21
|
+
* @param {string[]} args what follows `infra adopt`: the environment's name (or ID)
|
|
22
|
+
* @param {{ root: string, repo: string | null, force?: boolean, dryRun?: boolean,
|
|
23
|
+
* get: (path: string) => Promise<{ ok: boolean, status: number, data: any }>,
|
|
24
|
+
* post: (path: string, body: any) => Promise<{ ok: boolean, status: number, data: any }> }} ctx
|
|
25
|
+
* @returns {Promise<{ code: number, data: any, text: string }>}
|
|
26
|
+
*/
|
|
27
|
+
export async function infraAdopt(args, { root, repo, get, post, force = false, dryRun = false }) {
|
|
28
|
+
const refused = (error, text = `${error}.`) => ({ code: 1, data: { ok: false, written: false, error }, text });
|
|
29
|
+
const ref = args[0];
|
|
30
|
+
if (!ref)
|
|
31
|
+
return refused(
|
|
32
|
+
'name the environment',
|
|
33
|
+
'Name the environment: infra adopt <environment>. npx breakaway infra lists this repository’s.',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const res = await get(
|
|
37
|
+
`infra/environments/${encodeURIComponent(ref)}/draft${repo ? `?repo=${encodeURIComponent(repo)}` : ''}`,
|
|
38
|
+
);
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
if (res.status === 404 && /^no route for/u.test(String(res.data?.error ?? '')))
|
|
41
|
+
return refused(
|
|
42
|
+
'this board has no draft yet',
|
|
43
|
+
'This board can’t draft a desired state yet: its owner updates it to a release that can, then try again.',
|
|
44
|
+
);
|
|
45
|
+
const error = res.status === 0 ? `can’t reach the board (${res.data?.error ?? 'no answer'})` : res.data?.error;
|
|
46
|
+
return refused(error ?? `HTTP ${res.status}`, `Nothing was written: ${error ?? `HTTP ${res.status}`}.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const draft = res.data?.draft;
|
|
50
|
+
const named = environmentOfFile(`${draft?.environment ?? ''}.json`);
|
|
51
|
+
if (!draft || typeof draft.json !== 'string' || !named || !('environment' in named))
|
|
52
|
+
return refused('the board’s draft isn’t one this CLI can read: update npx breakaway, then try again');
|
|
53
|
+
const environment = named.environment;
|
|
54
|
+
const path = desiredPath(environment);
|
|
55
|
+
const notes = Array.isArray(draft.notes) ? draft.notes.map(String) : [];
|
|
56
|
+
const data = { environment, path, resources: draft.resources ?? 0, observeOnly: Boolean(draft.observeOnly), notes };
|
|
57
|
+
const withNotes = (lines) => [...lines, ...(notes.length ? ['', ...notes.map((n) => `- ${n}`)] : [])];
|
|
58
|
+
|
|
59
|
+
if (draft.observeOnly)
|
|
60
|
+
return {
|
|
61
|
+
code: 1,
|
|
62
|
+
data: { ...data, ok: false, written: false, error: `${environment} is observe only` },
|
|
63
|
+
text: withNotes([
|
|
64
|
+
`Nothing was written: ${environment} is observe only, so it takes no desired state and the board never changes it.`,
|
|
65
|
+
]).join('\n'),
|
|
66
|
+
};
|
|
67
|
+
// The board checks its draft before it answers, so this only fails when the two disagree: write nothing bad.
|
|
68
|
+
const checked = checkDesiredFile(draft.json, {});
|
|
69
|
+
if ('error' in checked)
|
|
70
|
+
return refused(
|
|
71
|
+
`the draft doesn’t check at ${checked.error.field ?? 'the top'}: ${checked.error.message}`,
|
|
72
|
+
`Nothing was written: the board’s draft for ${environment} doesn’t check here (${checked.error.field ?? 'the top'}: ${checked.error.message}). Update npx breakaway, then try again.`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const file = join(root, path);
|
|
76
|
+
const exists = existsSync(file);
|
|
77
|
+
if (exists && !force)
|
|
78
|
+
return {
|
|
79
|
+
code: 1,
|
|
80
|
+
data: { ...data, ok: false, written: false, error: `${path} is already there` },
|
|
81
|
+
text: `Nothing was written: ${path} is already there. See the draft with --dry-run, or replace the file with --force.`,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (dryRun)
|
|
85
|
+
return {
|
|
86
|
+
code: 0,
|
|
87
|
+
data: { ...data, ok: true, written: false, json: draft.json },
|
|
88
|
+
text: withNotes([
|
|
89
|
+
`Would ${exists ? 'replace' : 'write'} ${path}, ${plural(data.resources, 'resource')}:`,
|
|
90
|
+
'',
|
|
91
|
+
draft.json.trimEnd(),
|
|
92
|
+
]).join('\n'),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
96
|
+
writeFileSync(file, draft.json);
|
|
97
|
+
const check = await infraCheck([environment], { files: readInfraFolder(root), repo, post });
|
|
98
|
+
const next =
|
|
99
|
+
check.code === 0
|
|
100
|
+
? [
|
|
101
|
+
'',
|
|
102
|
+
`Next: commit ${path} and open a pull request. Nothing changes until it merges: then the board plans from it, and the plan waits for you.`,
|
|
103
|
+
]
|
|
104
|
+
: ['', `Fix what’s above, then npx breakaway infra check ${environment} before you commit ${path}.`];
|
|
105
|
+
return {
|
|
106
|
+
code: check.code,
|
|
107
|
+
data: { ...data, ok: check.code === 0, written: true, replaced: exists, check: check.data },
|
|
108
|
+
text: [
|
|
109
|
+
...withNotes([`${exists ? 'Replaced' : 'Wrote'} ${path}, ${plural(data.resources, 'resource')}.`]),
|
|
110
|
+
'',
|
|
111
|
+
check.text,
|
|
112
|
+
...next,
|
|
113
|
+
].join('\n'),
|
|
114
|
+
};
|
|
115
|
+
}
|
package/scripts/tasks.mjs
CHANGED
|
@@ -434,6 +434,10 @@ Infrastructure (init and add need no board: run them in the checkout of the re
|
|
|
434
434
|
run it holds (--task <ID>, else the one routine run it has claimed here); it says whether
|
|
435
435
|
the owner's envelope covers it or it waits for the owner, and names the plan. Only a
|
|
436
436
|
runbook's run may; it's the one change a run may ask for, and it never applies anything
|
|
437
|
+
infra adopt <environment> write the board's draft of the environment's desired state, drafted from what runs
|
|
438
|
+
there, to .github/breakaway-infra/<environment>.json, then check it like infra check and say
|
|
439
|
+
what to commit; it never plans or applies, and writes nothing for an observe-only environment
|
|
440
|
+
[--force] replaces a file that's there [--dry-run] prints the draft instead
|
|
437
441
|
infra add [<template>] [<environment>] [<input>=<value> …] a golden path: write the change one of the owner's
|
|
438
442
|
templates makes (.github/breakaway-infra/templates/<name>/, else breakaway's example of that
|
|
439
443
|
name) into the environment's file and the code it needs, for a pull request; it never
|
|
@@ -1441,6 +1445,22 @@ const commands = {
|
|
|
1441
1445
|
process.exitCode = result.code;
|
|
1442
1446
|
return;
|
|
1443
1447
|
}
|
|
1448
|
+
if (args[0] === 'adopt') {
|
|
1449
|
+
// infra adopt (CLI-23): the board's draft of an environment's desired state, written into the checkout and checked.
|
|
1450
|
+
const { infraAdopt } = await import('./tasks/infra-adopt.js');
|
|
1451
|
+
const { topOf } = await import('./tasks/pipeline.js');
|
|
1452
|
+
const result = await infraAdopt(args.slice(1), {
|
|
1453
|
+
root: topOf(process.cwd()),
|
|
1454
|
+
repo: slug,
|
|
1455
|
+
force: Boolean(opts.force),
|
|
1456
|
+
dryRun: Boolean(opts['dry-run']),
|
|
1457
|
+
get: (path) => call('GET', path, undefined, { raw: true }),
|
|
1458
|
+
post: (path, body) => call('POST', path, body, { raw: true }),
|
|
1459
|
+
});
|
|
1460
|
+
print(result.data, () => result.text);
|
|
1461
|
+
process.exitCode = result.code;
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1444
1464
|
if (args[0] === 'act') {
|
|
1445
1465
|
// infra act (CLI-24): a runbook's agent asks for one scale or restart, for the run it holds; the board decides.
|
|
1446
1466
|
const { infraAct, InfraActError } = await import('./tasks/infra-act.js');
|