@ansible-flow/node 0.0.0-stage → 0.85.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/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/ai/agents.d.ts +34 -0
- package/dist/ai/agents.js +16 -0
- package/dist/ai/agents.js.map +7 -0
- package/dist/ai/hostConfig.d.ts +44 -0
- package/dist/ai/hostConfig.js +120 -0
- package/dist/ai/hostConfig.js.map +7 -0
- package/dist/ai/hosts.d.ts +28 -0
- package/dist/ai/hosts.js +9 -0
- package/dist/ai/hosts.js.map +7 -0
- package/dist/ai/writers.d.ts +124 -0
- package/dist/ai/writers.js +75 -0
- package/dist/ai/writers.js.map +7 -0
- package/dist/autoLayout.d.ts +38 -0
- package/dist/autoLayout.js +145 -0
- package/dist/autoLayout.js.map +7 -0
- package/dist/awx/awxWorkflow.d.ts +47 -0
- package/dist/awx/awxWorkflow.js +17 -0
- package/dist/awx/awxWorkflow.js.map +7 -0
- package/dist/awx/client.d.ts +77 -0
- package/dist/awx/client.js +22 -0
- package/dist/awx/client.js.map +7 -0
- package/dist/catalog/bindingCheck.d.ts +4 -0
- package/dist/catalog/bindingCheck.js +69 -0
- package/dist/catalog/bindingCheck.js.map +7 -0
- package/dist/catalog/catalogIndex.d.ts +52 -0
- package/dist/catalog/catalogIndex.js +13 -0
- package/dist/catalog/catalogIndex.js.map +7 -0
- package/dist/catalog/catalogWorker.d.ts +17 -0
- package/dist/catalog/catalogWorker.js +38 -0
- package/dist/catalog/catalogWorker.js.map +7 -0
- package/dist/catalog/dumpFiles.d.ts +3 -0
- package/dist/catalog/dumpFiles.js +19 -0
- package/dist/catalog/dumpFiles.js.map +7 -0
- package/dist/chunks/chunk-2MZQHIXP.js +614 -0
- package/dist/chunks/chunk-2MZQHIXP.js.map +7 -0
- package/dist/chunks/chunk-4FHPTGI4.js +52 -0
- package/dist/chunks/chunk-4FHPTGI4.js.map +7 -0
- package/dist/chunks/chunk-A2NMQGQE.js +189 -0
- package/dist/chunks/chunk-A2NMQGQE.js.map +7 -0
- package/dist/chunks/chunk-EPJKJBNW.js +181 -0
- package/dist/chunks/chunk-EPJKJBNW.js.map +7 -0
- package/dist/chunks/chunk-FG53MLUX.js +136 -0
- package/dist/chunks/chunk-FG53MLUX.js.map +7 -0
- package/dist/chunks/chunk-GAL5LW6Y.js +269 -0
- package/dist/chunks/chunk-GAL5LW6Y.js.map +7 -0
- package/dist/chunks/chunk-HPVT6I36.js +371 -0
- package/dist/chunks/chunk-HPVT6I36.js.map +7 -0
- package/dist/chunks/chunk-I6FIOP7G.js +48 -0
- package/dist/chunks/chunk-I6FIOP7G.js.map +7 -0
- package/dist/chunks/chunk-IIFP6UQR.js +44 -0
- package/dist/chunks/chunk-IIFP6UQR.js.map +7 -0
- package/dist/chunks/chunk-JDPN3LTT.js +18 -0
- package/dist/chunks/chunk-JDPN3LTT.js.map +7 -0
- package/dist/chunks/chunk-LGUG2Q7Z.js +191 -0
- package/dist/chunks/chunk-LGUG2Q7Z.js.map +7 -0
- package/dist/chunks/chunk-NTZRNEZM.js +25 -0
- package/dist/chunks/chunk-NTZRNEZM.js.map +7 -0
- package/dist/chunks/chunk-RETJPSKL.js +123 -0
- package/dist/chunks/chunk-RETJPSKL.js.map +7 -0
- package/dist/chunks/chunk-SSZNBJZQ.js +52 -0
- package/dist/chunks/chunk-SSZNBJZQ.js.map +7 -0
- package/dist/chunks/chunk-SUV3VHKR.js +52 -0
- package/dist/chunks/chunk-SUV3VHKR.js.map +7 -0
- package/dist/chunks/chunk-THNSGAKN.js +133 -0
- package/dist/chunks/chunk-THNSGAKN.js.map +7 -0
- package/dist/chunks/chunk-UNCR5JOZ.js +85 -0
- package/dist/chunks/chunk-UNCR5JOZ.js.map +7 -0
- package/dist/env/probeClient.d.ts +77 -0
- package/dist/env/probeClient.js +244 -0
- package/dist/env/probeClient.js.map +7 -0
- package/dist/env/userSettings.d.ts +33 -0
- package/dist/env/userSettings.js +101 -0
- package/dist/env/userSettings.js.map +7 -0
- package/dist/run/audit.d.ts +2 -0
- package/dist/run/audit.js +10 -0
- package/dist/run/audit.js.map +7 -0
- package/dist/run/exec.d.ts +14 -0
- package/dist/run/exec.js +7 -0
- package/dist/run/exec.js.map +7 -0
- package/dist/run/facts.d.ts +39 -0
- package/dist/run/facts.js +96 -0
- package/dist/run/facts.js.map +7 -0
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +8 -0
- package/dist/run/ids.js.map +7 -0
- package/dist/run/pastRuns.d.ts +9 -0
- package/dist/run/pastRuns.js +13 -0
- package/dist/run/pastRuns.js.map +7 -0
- package/dist/run/runFlow.d.ts +78 -0
- package/dist/run/runFlow.js +20 -0
- package/dist/run/runFlow.js.map +7 -0
- package/dist/run/segments.d.ts +43 -0
- package/dist/run/segments.js +157 -0
- package/dist/run/segments.js.map +7 -0
- package/dist/run/stop.d.ts +3 -0
- package/dist/run/stop.js +7 -0
- package/dist/run/stop.js.map +7 -0
- package/dist/run/storage.d.ts +16 -0
- package/dist/run/storage.js +15 -0
- package/dist/run/storage.js.map +7 -0
- package/dist/run/tail.d.ts +5 -0
- package/dist/run/tail.js +7 -0
- package/dist/run/tail.js.map +7 -0
- package/dist/runner/client.d.ts +149 -0
- package/dist/runner/client.js +168 -0
- package/dist/runner/client.js.map +7 -0
- package/dist/runner/devBundle.d.ts +14 -0
- package/dist/runner/devBundle.js +78 -0
- package/dist/runner/devBundle.js.map +7 -0
- package/dist/scenario/cli.d.ts +39 -0
- package/dist/scenario/cli.js +125 -0
- package/dist/scenario/cli.js.map +7 -0
- package/dist/scenario/runner.d.ts +50 -0
- package/dist/scenario/runner.js +185 -0
- package/dist/scenario/runner.js.map +7 -0
- package/dist/sharedRefs.d.ts +22 -0
- package/dist/sharedRefs.js +114 -0
- package/dist/sharedRefs.js.map +7 -0
- package/dist/ssh/exec.d.ts +39 -0
- package/dist/ssh/exec.js +13 -0
- package/dist/ssh/exec.js.map +7 -0
- package/dist/ssh/remote.d.ts +40 -0
- package/dist/ssh/remote.js +25 -0
- package/dist/ssh/remote.js.map +7 -0
- package/dist/ssh/sshRun.d.ts +1 -0
- package/dist/ssh/sshRun.js +24 -0
- package/dist/ssh/sshRun.js.map +7 -0
- package/dist/ssh/sync.d.ts +22 -0
- package/dist/ssh/sync.js +12 -0
- package/dist/ssh/sync.js.map +7 -0
- package/dist/timeouts.d.ts +38 -0
- package/dist/timeouts.js +7 -0
- package/dist/timeouts.js.map +7 -0
- package/package.json +35 -4
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/node/runner/devBundle.ts
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
var REF = "refs/afd-dev/run";
|
|
7
|
+
function git(cwd, args, env = {}) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
execFile(
|
|
10
|
+
"git",
|
|
11
|
+
args,
|
|
12
|
+
{
|
|
13
|
+
cwd,
|
|
14
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", ...env },
|
|
15
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
16
|
+
timeout: 12e4,
|
|
17
|
+
windowsHide: true
|
|
18
|
+
},
|
|
19
|
+
(error, stdout, stderr) => error ? reject(new Error(stderr.trim().split("\n").at(-1) || error.message)) : resolve(stdout.trim())
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function devBundle(root, serverCommit) {
|
|
24
|
+
const dir = await mkdtemp(join(tmpdir(), "afd-dev-"));
|
|
25
|
+
try {
|
|
26
|
+
const head = await git(root, ["rev-parse", "HEAD"]);
|
|
27
|
+
const dirty = await git(root, ["status", "--porcelain", "--untracked-files=all"]) !== "";
|
|
28
|
+
let commit = head;
|
|
29
|
+
if (dirty) {
|
|
30
|
+
const env = { GIT_INDEX_FILE: join(dir, "index") };
|
|
31
|
+
await git(root, ["read-tree", "HEAD"], env);
|
|
32
|
+
await git(root, ["add", "-A"], env);
|
|
33
|
+
const tree = await git(root, ["write-tree"], env);
|
|
34
|
+
commit = await git(
|
|
35
|
+
root,
|
|
36
|
+
["commit-tree", tree, "-p", head, "-m", "Work in progress: a dev run from VS Code"],
|
|
37
|
+
// The commit is the extension's, whoever has no git identity set up.
|
|
38
|
+
{
|
|
39
|
+
GIT_AUTHOR_NAME: "Ansible Flow Designer",
|
|
40
|
+
GIT_AUTHOR_EMAIL: "dev-run@ansible-flow.invalid",
|
|
41
|
+
GIT_COMMITTER_NAME: "Ansible Flow Designer",
|
|
42
|
+
GIT_COMMITTER_EMAIL: "dev-run@ansible-flow.invalid"
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (commit === serverCommit) return void 0;
|
|
47
|
+
const fromServer = await git(root, ["cat-file", "-e", `${serverCommit}^{commit}`]).then(
|
|
48
|
+
() => true,
|
|
49
|
+
() => false
|
|
50
|
+
);
|
|
51
|
+
const file = join(dir, "work.bundle");
|
|
52
|
+
await git(root, ["update-ref", REF, commit]);
|
|
53
|
+
try {
|
|
54
|
+
try {
|
|
55
|
+
await git(root, [
|
|
56
|
+
"bundle",
|
|
57
|
+
"create",
|
|
58
|
+
"--quiet",
|
|
59
|
+
file,
|
|
60
|
+
REF,
|
|
61
|
+
...fromServer ? [`^${serverCommit}`] : []
|
|
62
|
+
]);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
if (!fromServer || !/empty bundle/i.test(e.message)) throw e;
|
|
65
|
+
await git(root, ["bundle", "create", "--quiet", file, REF]);
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
await git(root, ["update-ref", "-d", REF]).catch(() => void 0);
|
|
69
|
+
}
|
|
70
|
+
return { commit, uncommitted: dirty, fromServer, bundle: await readFile(file) };
|
|
71
|
+
} finally {
|
|
72
|
+
await rm(dir, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
devBundle
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=devBundle.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/runner/devBundle.ts"],
|
|
4
|
+
"sourcesContent": ["// AFD-282: someone's unpushed work as a git bundle, for a dev run on Ansible Flow Server\n// (AFS-26). The work is HEAD, or, with uncommitted changes, a commit of the working tree made with\n// a temporary index: the user's index, working tree and branches are not touched. It is bundled\n// from the server's commit when this repository has it (the server has everything before it),\n// else with its whole history.\nimport { execFile } from 'node:child_process';\nimport { mkdtemp, readFile, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nconst REF = 'refs/afd-dev/run';\n\nfunction git(cwd: string, args: string[], env: Record<string, string> = {}): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(\n 'git',\n args,\n {\n cwd,\n env: { ...process.env, GIT_TERMINAL_PROMPT: '0', ...env },\n maxBuffer: 16 * 1024 * 1024,\n timeout: 120_000,\n windowsHide: true,\n },\n (error, stdout, stderr) =>\n error\n ? reject(new Error(stderr.trim().split('\\n').at(-1) || error.message))\n : resolve(stdout.trim()),\n );\n });\n}\n\nexport interface DevWork {\n /** The commit the bundle holds: HEAD, or the commit of the working tree. */\n commit: string;\n /** It has uncommitted changes in it. */\n uncommitted: boolean;\n /** Bundled from the server's commit (else the whole history). */\n fromServer: boolean;\n bundle: Buffer;\n}\n\n/**\n * The work in the repository at `root` as a bundle, or undefined when it is the server's commit\n * (nothing to send).\n */\nexport async function devBundle(root: string, serverCommit: string): Promise<DevWork | undefined> {\n const dir = await mkdtemp(join(tmpdir(), 'afd-dev-'));\n try {\n const head = await git(root, ['rev-parse', 'HEAD']);\n const dirty = (await git(root, ['status', '--porcelain', '--untracked-files=all'])) !== '';\n let commit = head;\n if (dirty) {\n // A commit of everything as it is now (what `git add -A` would stage, ignored files left\n // out), made with an index of its own.\n const env = { GIT_INDEX_FILE: join(dir, 'index') };\n await git(root, ['read-tree', 'HEAD'], env);\n await git(root, ['add', '-A'], env);\n const tree = await git(root, ['write-tree'], env);\n commit = await git(\n root,\n ['commit-tree', tree, '-p', head, '-m', 'Work in progress: a dev run from VS Code'],\n // The commit is the extension's, whoever has no git identity set up.\n {\n GIT_AUTHOR_NAME: 'Ansible Flow Designer',\n GIT_AUTHOR_EMAIL: 'dev-run@ansible-flow.invalid',\n GIT_COMMITTER_NAME: 'Ansible Flow Designer',\n GIT_COMMITTER_EMAIL: 'dev-run@ansible-flow.invalid',\n },\n );\n }\n if (commit === serverCommit) return undefined;\n const fromServer = await git(root, ['cat-file', '-e', `${serverCommit}^{commit}`]).then(\n () => true,\n () => false,\n );\n const file = join(dir, 'work.bundle');\n await git(root, ['update-ref', REF, commit]);\n try {\n try {\n await git(root, [\n 'bundle',\n 'create',\n '--quiet',\n file,\n REF,\n ...(fromServer ? [`^${serverCommit}`] : []),\n ]);\n } catch (e) {\n // Nothing new since the server's commit (HEAD is behind it): the whole history, then.\n if (!fromServer || !/empty bundle/i.test((e as Error).message)) throw e;\n await git(root, ['bundle', 'create', '--quiet', file, REF]);\n }\n } finally {\n await git(root, ['update-ref', '-d', REF]).catch(() => undefined);\n }\n return { commit, uncommitted: dirty, fromServer, bundle: await readFile(file) };\n } finally {\n await rm(dir, { recursive: true, force: true });\n }\n}\n"],
|
|
5
|
+
"mappings": ";AAKA,SAAS,gBAAgB;AACzB,SAAS,SAAS,UAAU,UAAU;AACtC,SAAS,cAAc;AACvB,SAAS,YAAY;AAErB,IAAM,MAAM;AAEZ,SAAS,IAAI,KAAa,MAAgB,MAA8B,CAAC,GAAoB;AAC3F,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA,KAAK,EAAE,GAAG,QAAQ,KAAK,qBAAqB,KAAK,GAAG,IAAI;AAAA,QACxD,WAAW,KAAK,OAAO;AAAA,QACvB,SAAS;AAAA,QACT,aAAa;AAAA,MACf;AAAA,MACA,CAAC,OAAO,QAAQ,WACd,QACI,OAAO,IAAI,MAAM,OAAO,KAAK,EAAE,MAAM,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,OAAO,CAAC,IACnE,QAAQ,OAAO,KAAK,CAAC;AAAA,IAC7B;AAAA,EACF,CAAC;AACH;AAgBA,eAAsB,UAAU,MAAc,cAAoD;AAChG,QAAM,MAAM,MAAM,QAAQ,KAAK,OAAO,GAAG,UAAU,CAAC;AACpD,MAAI;AACF,UAAM,OAAO,MAAM,IAAI,MAAM,CAAC,aAAa,MAAM,CAAC;AAClD,UAAM,QAAS,MAAM,IAAI,MAAM,CAAC,UAAU,eAAe,uBAAuB,CAAC,MAAO;AACxF,QAAI,SAAS;AACb,QAAI,OAAO;AAGT,YAAM,MAAM,EAAE,gBAAgB,KAAK,KAAK,OAAO,EAAE;AACjD,YAAM,IAAI,MAAM,CAAC,aAAa,MAAM,GAAG,GAAG;AAC1C,YAAM,IAAI,MAAM,CAAC,OAAO,IAAI,GAAG,GAAG;AAClC,YAAM,OAAO,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG,GAAG;AAChD,eAAS,MAAM;AAAA,QACb;AAAA,QACA,CAAC,eAAe,MAAM,MAAM,MAAM,MAAM,0CAA0C;AAAA;AAAA,QAElF;AAAA,UACE,iBAAiB;AAAA,UACjB,kBAAkB;AAAA,UAClB,oBAAoB;AAAA,UACpB,qBAAqB;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,aAAc,QAAO;AACpC,UAAM,aAAa,MAAM,IAAI,MAAM,CAAC,YAAY,MAAM,GAAG,YAAY,WAAW,CAAC,EAAE;AAAA,MACjF,MAAM;AAAA,MACN,MAAM;AAAA,IACR;AACA,UAAM,OAAO,KAAK,KAAK,aAAa;AACpC,UAAM,IAAI,MAAM,CAAC,cAAc,KAAK,MAAM,CAAC;AAC3C,QAAI;AACF,UAAI;AACF,cAAM,IAAI,MAAM;AAAA,UACd;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,GAAI,aAAa,CAAC,IAAI,YAAY,EAAE,IAAI,CAAC;AAAA,QAC3C,CAAC;AAAA,MACH,SAAS,GAAG;AAEV,YAAI,CAAC,cAAc,CAAC,gBAAgB,KAAM,EAAY,OAAO,EAAG,OAAM;AACtE,cAAM,IAAI,MAAM,CAAC,UAAU,UAAU,WAAW,MAAM,GAAG,CAAC;AAAA,MAC5D;AAAA,IACF,UAAE;AACA,YAAM,IAAI,MAAM,CAAC,cAAc,MAAM,GAAG,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,IAClE;AACA,WAAO,EAAE,QAAQ,aAAa,OAAO,YAAY,QAAQ,MAAM,SAAS,IAAI,EAAE;AAAA,EAChF,UAAE;AACA,UAAM,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAChD;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FlowDocument } from '@ansible-flow/core/flow/types';
|
|
2
|
+
import { type ScenarioFile, type ScenarioProblem } from '@ansible-flow/core/scenario/scenarios';
|
|
3
|
+
export declare const USAGE: string;
|
|
4
|
+
export interface CliOptions {
|
|
5
|
+
targets: string[];
|
|
6
|
+
image?: string;
|
|
7
|
+
engine?: 'docker' | 'podman';
|
|
8
|
+
python?: string;
|
|
9
|
+
junit?: string;
|
|
10
|
+
/** AFD-118: run in WSL from Windows; the distribution, or '' for the default one. */
|
|
11
|
+
wsl?: string;
|
|
12
|
+
only?: string[];
|
|
13
|
+
keep: boolean;
|
|
14
|
+
verbose: boolean;
|
|
15
|
+
simulate: boolean;
|
|
16
|
+
check: boolean;
|
|
17
|
+
help: boolean;
|
|
18
|
+
/** An option this command does not know, or one without its value. */
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseCliArgs(args: readonly string[]): CliOptions;
|
|
22
|
+
/** Flow files: the ones named, and every *.flow.yaml under the folders named. */
|
|
23
|
+
export declare function flowsOf(target: string): string[];
|
|
24
|
+
export interface FlowScenarios {
|
|
25
|
+
flowFile: string;
|
|
26
|
+
scenariosFile: string;
|
|
27
|
+
doc: FlowDocument;
|
|
28
|
+
file?: ScenarioFile;
|
|
29
|
+
/** The file does not read as scenarios. */
|
|
30
|
+
error?: string;
|
|
31
|
+
problems: ScenarioProblem[];
|
|
32
|
+
}
|
|
33
|
+
/** The flows under the targets that have a scenarios file, read and checked. */
|
|
34
|
+
export declare function loadScenarioFlows(targets: readonly string[]): FlowScenarios[];
|
|
35
|
+
/** One line per problem: FAIL for errors, WARN for warnings (they do not stop a run). */
|
|
36
|
+
export declare function problemLines(f: FlowScenarios): string[];
|
|
37
|
+
/** The scenarios of one flow, computed by the simulator. */
|
|
38
|
+
export declare function simulateFlow(f: FlowScenarios, only?: readonly string[]): import("@ansible-flow/core/scenario/scenarios").ScenarioResult[];
|
|
39
|
+
export declare const NO_SCENARIOS = "No scenarios found: a flow has them in <flow key>.scenarios.yaml next to its .flow.yaml";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/node/scenario/cli.ts
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { parseFlow } from "@ansible-flow/core/flow/document";
|
|
5
|
+
import { isFileKey } from "@ansible-flow/core/flow/ids";
|
|
6
|
+
import {
|
|
7
|
+
checkScenarios,
|
|
8
|
+
parseScenarios,
|
|
9
|
+
scenariosFileName
|
|
10
|
+
} from "@ansible-flow/core/scenario/scenarios";
|
|
11
|
+
import { simulateScenario } from "@ansible-flow/core/scenario/simulate";
|
|
12
|
+
var USAGE = [
|
|
13
|
+
"Usage: node afd-scenarios.js [options] <flow.yaml|folder>...",
|
|
14
|
+
"",
|
|
15
|
+
"Runs the scenarios in <flow key>.scenarios.yaml next to each flow; exits 1 when one fails.",
|
|
16
|
+
"",
|
|
17
|
+
" --simulate compute where each host goes, without Ansible (instant); a scenario",
|
|
18
|
+
" the simulator cannot model is skipped: verify it with a run",
|
|
19
|
+
" --check only check the scenario files against their flows",
|
|
20
|
+
" --image IMAGE run in this execution environment image (Docker or Podman)",
|
|
21
|
+
" --engine docker|podman",
|
|
22
|
+
" --python PATH run with ansible-playbook on this machine (default: python3)",
|
|
23
|
+
" --wsl run in WSL from Windows (the default distribution)",
|
|
24
|
+
" --wsl-distro NAME run in this WSL distribution",
|
|
25
|
+
" --scenario NAME run only this scenario (repeatable)",
|
|
26
|
+
" --junit FILE write JUnit XML for CI dashboards",
|
|
27
|
+
" --keep keep each run folder",
|
|
28
|
+
" --verbose print Ansible output"
|
|
29
|
+
].join("\n");
|
|
30
|
+
var WITH_VALUE = /* @__PURE__ */ new Set([
|
|
31
|
+
"--image",
|
|
32
|
+
"--engine",
|
|
33
|
+
"--python",
|
|
34
|
+
"--junit",
|
|
35
|
+
"--scenario",
|
|
36
|
+
"--wsl-distro"
|
|
37
|
+
]);
|
|
38
|
+
var SWITCHES = /* @__PURE__ */ new Set(["--keep", "--verbose", "--simulate", "--check", "--help", "--wsl"]);
|
|
39
|
+
function parseCliArgs(args) {
|
|
40
|
+
const o = {
|
|
41
|
+
targets: [],
|
|
42
|
+
keep: false,
|
|
43
|
+
verbose: false,
|
|
44
|
+
simulate: false,
|
|
45
|
+
check: false,
|
|
46
|
+
help: false
|
|
47
|
+
};
|
|
48
|
+
const only = [];
|
|
49
|
+
for (let i = 0; i < args.length; i++) {
|
|
50
|
+
const a = args[i];
|
|
51
|
+
if (WITH_VALUE.has(a)) {
|
|
52
|
+
const v = args[++i];
|
|
53
|
+
if (v === void 0 || v.startsWith("--")) o.error ??= `${a} needs a value`;
|
|
54
|
+
else if (a === "--image") o.image = v;
|
|
55
|
+
else if (a === "--engine") {
|
|
56
|
+
if (v === "docker" || v === "podman") o.engine = v;
|
|
57
|
+
else o.error ??= `--engine takes docker or podman, not ${v}`;
|
|
58
|
+
} else if (a === "--python") o.python = v;
|
|
59
|
+
else if (a === "--junit") o.junit = v;
|
|
60
|
+
else if (a === "--wsl-distro") o.wsl = v;
|
|
61
|
+
else only.push(v);
|
|
62
|
+
} else if (SWITCHES.has(a)) {
|
|
63
|
+
if (a === "--keep") o.keep = true;
|
|
64
|
+
if (a === "--verbose") o.verbose = true;
|
|
65
|
+
if (a === "--simulate") o.simulate = true;
|
|
66
|
+
if (a === "--check") o.check = true;
|
|
67
|
+
if (a === "--help") o.help = true;
|
|
68
|
+
if (a === "--wsl") o.wsl ??= "";
|
|
69
|
+
} else if (a.startsWith("--")) o.error ??= `Unknown option ${a}`;
|
|
70
|
+
else o.targets.push(a);
|
|
71
|
+
}
|
|
72
|
+
if (only.length) o.only = only;
|
|
73
|
+
if (o.simulate && o.check) o.error ??= "Give --simulate or --check, not both";
|
|
74
|
+
if (o.wsl !== void 0 && o.image) o.error ??= "Give --wsl or --image, not both";
|
|
75
|
+
return o;
|
|
76
|
+
}
|
|
77
|
+
function flowsOf(target) {
|
|
78
|
+
if (!statSync(target).isDirectory()) return [target];
|
|
79
|
+
return readdirSync(target, { recursive: true, encoding: "utf8" }).filter((f) => f.endsWith(".flow.yaml") && !f.includes("node_modules")).map((f) => join(target, f));
|
|
80
|
+
}
|
|
81
|
+
function loadScenarioFlows(targets) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const flowFile of targets.flatMap(flowsOf)) {
|
|
84
|
+
const doc = parseFlow(readFileSync(flowFile, "utf8")).doc;
|
|
85
|
+
if (!doc) continue;
|
|
86
|
+
if (!isFileKey(doc?.flow?.key)) continue;
|
|
87
|
+
const scenariosFile = join(dirname(flowFile), scenariosFileName(doc.flow.key));
|
|
88
|
+
if (!existsSync(scenariosFile)) continue;
|
|
89
|
+
try {
|
|
90
|
+
const parsed = parseScenarios(readFileSync(scenariosFile, "utf8"));
|
|
91
|
+
out.push({
|
|
92
|
+
flowFile,
|
|
93
|
+
scenariosFile,
|
|
94
|
+
doc,
|
|
95
|
+
file: parsed.file,
|
|
96
|
+
error: parsed.error,
|
|
97
|
+
problems: parsed.file ? checkScenarios(doc, parsed.file) : []
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
out.push({ flowFile, scenariosFile, doc, error: e.message, problems: [] });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function problemLines(f) {
|
|
106
|
+
if (f.error) return [`FAIL ${f.scenariosFile}: ${f.error}`];
|
|
107
|
+
return f.problems.map((p) => {
|
|
108
|
+
const where = p.scenario >= 0 ? ` [${f.file?.scenarios[p.scenario]?.name ?? p.scenario}]` : "";
|
|
109
|
+
return `${p.severity === "warning" ? "WARN" : "FAIL"} ${f.scenariosFile}${where}: ${p.message}`;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function simulateFlow(f, only) {
|
|
113
|
+
return (f.file?.scenarios ?? []).filter((s) => !only || only.includes(s.name)).map((s) => simulateScenario(f.doc, s));
|
|
114
|
+
}
|
|
115
|
+
var NO_SCENARIOS = "No scenarios found: a flow has them in <flow key>.scenarios.yaml next to its .flow.yaml";
|
|
116
|
+
export {
|
|
117
|
+
NO_SCENARIOS,
|
|
118
|
+
USAGE,
|
|
119
|
+
flowsOf,
|
|
120
|
+
loadScenarioFlows,
|
|
121
|
+
parseCliArgs,
|
|
122
|
+
problemLines,
|
|
123
|
+
simulateFlow
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/scenario/cli.ts"],
|
|
4
|
+
"sourcesContent": ["// The scenario command line (AFD-58, AFD-114), apart from its entry file so it can be tested:\n// arguments, the flows with scenarios under the paths named, and the two modes that need no Ansible,\n// --check (the files against their flows) and --simulate (where each host goes, computed).\nimport { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { parseFlow } from '../../core/flow/document';\nimport { isFileKey } from '../../core/flow/ids';\nimport type { FlowDocument } from '../../core/flow/types';\nimport {\n checkScenarios,\n parseScenarios,\n type ScenarioFile,\n type ScenarioProblem,\n scenariosFileName,\n} from '../../core/scenario/scenarios';\nimport { simulateScenario } from '../../core/scenario/simulate';\n\nexport const USAGE = [\n 'Usage: node afd-scenarios.js [options] <flow.yaml|folder>...',\n '',\n 'Runs the scenarios in <flow key>.scenarios.yaml next to each flow; exits 1 when one fails.',\n '',\n ' --simulate compute where each host goes, without Ansible (instant); a scenario',\n ' the simulator cannot model is skipped: verify it with a run',\n ' --check only check the scenario files against their flows',\n ' --image IMAGE run in this execution environment image (Docker or Podman)',\n ' --engine docker|podman',\n ' --python PATH run with ansible-playbook on this machine (default: python3)',\n ' --wsl run in WSL from Windows (the default distribution)',\n ' --wsl-distro NAME run in this WSL distribution',\n ' --scenario NAME run only this scenario (repeatable)',\n ' --junit FILE write JUnit XML for CI dashboards',\n ' --keep keep each run folder',\n ' --verbose print Ansible output',\n].join('\\n');\n\nexport interface CliOptions {\n targets: string[];\n image?: string;\n engine?: 'docker' | 'podman';\n python?: string;\n junit?: string;\n /** AFD-118: run in WSL from Windows; the distribution, or '' for the default one. */\n wsl?: string;\n only?: string[];\n keep: boolean;\n verbose: boolean;\n simulate: boolean;\n check: boolean;\n help: boolean;\n /** An option this command does not know, or one without its value. */\n error?: string;\n}\n\nconst WITH_VALUE = new Set([\n '--image',\n '--engine',\n '--python',\n '--junit',\n '--scenario',\n '--wsl-distro',\n]);\nconst SWITCHES = new Set(['--keep', '--verbose', '--simulate', '--check', '--help', '--wsl']);\n\nexport function parseCliArgs(args: readonly string[]): CliOptions {\n const o: CliOptions = {\n targets: [],\n keep: false,\n verbose: false,\n simulate: false,\n check: false,\n help: false,\n };\n const only: string[] = [];\n for (let i = 0; i < args.length; i++) {\n const a = args[i] as string;\n if (WITH_VALUE.has(a)) {\n const v = args[++i];\n if (v === undefined || v.startsWith('--')) o.error ??= `${a} needs a value`;\n else if (a === '--image') o.image = v;\n else if (a === '--engine') {\n if (v === 'docker' || v === 'podman') o.engine = v;\n else o.error ??= `--engine takes docker or podman, not ${v}`;\n } else if (a === '--python') o.python = v;\n else if (a === '--junit') o.junit = v;\n else if (a === '--wsl-distro') o.wsl = v;\n else only.push(v);\n } else if (SWITCHES.has(a)) {\n if (a === '--keep') o.keep = true;\n if (a === '--verbose') o.verbose = true;\n if (a === '--simulate') o.simulate = true;\n if (a === '--check') o.check = true;\n if (a === '--help') o.help = true;\n if (a === '--wsl') o.wsl ??= '';\n } else if (a.startsWith('--')) o.error ??= `Unknown option ${a}`;\n else o.targets.push(a);\n }\n if (only.length) o.only = only;\n if (o.simulate && o.check) o.error ??= 'Give --simulate or --check, not both';\n if (o.wsl !== undefined && o.image) o.error ??= 'Give --wsl or --image, not both';\n return o;\n}\n\n/** Flow files: the ones named, and every *.flow.yaml under the folders named. */\nexport function flowsOf(target: string): string[] {\n if (!statSync(target).isDirectory()) return [target];\n return readdirSync(target, { recursive: true, encoding: 'utf8' })\n .filter((f) => f.endsWith('.flow.yaml') && !f.includes('node_modules'))\n .map((f) => join(target, f));\n}\n\nexport interface FlowScenarios {\n flowFile: string;\n scenariosFile: string;\n doc: FlowDocument;\n file?: ScenarioFile;\n /** The file does not read as scenarios. */\n error?: string;\n problems: ScenarioProblem[];\n}\n\n/** The flows under the targets that have a scenarios file, read and checked. */\nexport function loadScenarioFlows(targets: readonly string[]): FlowScenarios[] {\n const out: FlowScenarios[] = [];\n for (const flowFile of targets.flatMap(flowsOf)) {\n // AFD-147: a flow that does not read (an alias inside what it names, too) has no scenarios.\n const doc = parseFlow(readFileSync(flowFile, 'utf8')).doc;\n if (!doc) continue;\n // AFD-133: a key that cannot name a file names no scenarios file either.\n if (!isFileKey(doc?.flow?.key)) continue;\n const scenariosFile = join(dirname(flowFile), scenariosFileName(doc.flow.key));\n if (!existsSync(scenariosFile)) continue;\n // AFD-159: a scenarios file that does not read or check fails alone, as a FAIL line.\n try {\n const parsed = parseScenarios(readFileSync(scenariosFile, 'utf8'));\n out.push({\n flowFile,\n scenariosFile,\n doc,\n file: parsed.file,\n error: parsed.error,\n problems: parsed.file ? checkScenarios(doc, parsed.file) : [],\n });\n } catch (e) {\n out.push({ flowFile, scenariosFile, doc, error: (e as Error).message, problems: [] });\n }\n }\n return out;\n}\n\n/** One line per problem: FAIL for errors, WARN for warnings (they do not stop a run). */\nexport function problemLines(f: FlowScenarios): string[] {\n if (f.error) return [`FAIL ${f.scenariosFile}: ${f.error}`];\n return f.problems.map((p) => {\n const where = p.scenario >= 0 ? ` [${f.file?.scenarios[p.scenario]?.name ?? p.scenario}]` : '';\n return `${p.severity === 'warning' ? 'WARN' : 'FAIL'} ${f.scenariosFile}${where}: ${p.message}`;\n });\n}\n\n/** The scenarios of one flow, computed by the simulator. */\nexport function simulateFlow(f: FlowScenarios, only?: readonly string[]) {\n return (f.file?.scenarios ?? [])\n .filter((s) => !only || only.includes(s.name))\n .map((s) => simulateScenario(f.doc, s));\n}\n\nexport const NO_SCENARIOS =\n 'No scenarios found: a flow has them in <flow key>.scenarios.yaml next to its .flow.yaml';\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { GenerateResult } from '@ansible-flow/core/engine/types';
|
|
2
|
+
import type { FlowDocument } from '@ansible-flow/core/flow/types';
|
|
3
|
+
import type { Environment } from '@ansible-flow/core/run/environment';
|
|
4
|
+
import { type Scenario, type ScenarioFile, type ScenarioResult } from '@ansible-flow/core/scenario/scenarios';
|
|
5
|
+
export interface ScenarioRunOptions {
|
|
6
|
+
env: Environment;
|
|
7
|
+
/** Repository root on this machine (mounted read-only in a container). */
|
|
8
|
+
root: string;
|
|
9
|
+
/** Folder with callback_plugins/afd_events.py. */
|
|
10
|
+
probeDir: string;
|
|
11
|
+
flowFile: string;
|
|
12
|
+
doc: FlowDocument;
|
|
13
|
+
file: ScenarioFile;
|
|
14
|
+
/** Where each scenario's folder is made; removed afterwards unless `keep`. */
|
|
15
|
+
workDir: string;
|
|
16
|
+
only?: readonly string[];
|
|
17
|
+
keep?: boolean;
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
log?: (line: string) => void;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A scenario's folder: the scenario build of the flow, its extra vars and inventory (AFD-114: the
|
|
24
|
+
* simulator's parity suite stages many into one container). Undefined when the flow does not
|
|
25
|
+
* generate.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stageScenario(doc: FlowDocument, s: Scenario, dir: string, source: string): GenerateResult | undefined;
|
|
28
|
+
export declare function runScenarios(o: ScenarioRunOptions): Promise<ScenarioResult[]>;
|
|
29
|
+
/**
|
|
30
|
+
* A one-line verdict per scenario, and the hosts that ended elsewhere. With the flow at hand, the
|
|
31
|
+
* ends by their titles with the keys a scenario file uses in brackets (AFD-94). AFD-105: in the
|
|
32
|
+
* panel's wording, with why when the scenario planted the host's outcome.
|
|
33
|
+
*/
|
|
34
|
+
export declare function describeResults(results: readonly ScenarioResult[], doc?: FlowDocument): string[];
|
|
35
|
+
/**
|
|
36
|
+
* AFD-114: a simulated scenario that has what the simulator does not model is not judged on the
|
|
37
|
+
* command line: its verdict is a best effort, so CI skips it rather than pass or fail on a guess.
|
|
38
|
+
*/
|
|
39
|
+
export declare const skipped: (r: ScenarioResult) => boolean;
|
|
40
|
+
/** Scenarios that failed, leaving out the skipped. */
|
|
41
|
+
export declare const failedCount: (results: readonly ScenarioResult[]) => number;
|
|
42
|
+
/** One flow's scenarios as a JUnit `<testsuite>`. */
|
|
43
|
+
export declare function junitSuite(flow: string, results: readonly ScenarioResult[], doc?: FlowDocument): string;
|
|
44
|
+
/**
|
|
45
|
+
* A JUnit file of several flows' suites under one root (AFD-158: one document per flow, joined,
|
|
46
|
+
* was not XML once there were two flows).
|
|
47
|
+
*/
|
|
48
|
+
export declare function junitDocument(suites: readonly string[]): string;
|
|
49
|
+
/** One flow's scenarios as a whole JUnit file. */
|
|
50
|
+
export declare function junitXml(flow: string, results: readonly ScenarioResult[], doc?: FlowDocument): string;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchRunFile,
|
|
3
|
+
putRunFolder,
|
|
4
|
+
removeRunDir,
|
|
5
|
+
syncWorkspace
|
|
6
|
+
} from "../chunks/chunk-RETJPSKL.js";
|
|
7
|
+
import "../chunks/chunk-THNSGAKN.js";
|
|
8
|
+
import "../chunks/chunk-FG53MLUX.js";
|
|
9
|
+
import {
|
|
10
|
+
stopStarted
|
|
11
|
+
} from "../chunks/chunk-JDPN3LTT.js";
|
|
12
|
+
import {
|
|
13
|
+
startProcess
|
|
14
|
+
} from "../chunks/chunk-I6FIOP7G.js";
|
|
15
|
+
|
|
16
|
+
// src/node/scenario/runner.ts
|
|
17
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { basename, join } from "node:path";
|
|
19
|
+
import { engineFor } from "@ansible-flow/core/engine/engines";
|
|
20
|
+
import { insidePaths, runProcess } from "@ansible-flow/core/run/command";
|
|
21
|
+
import { summarizeRun } from "@ansible-flow/core/run/events";
|
|
22
|
+
import {
|
|
23
|
+
judge,
|
|
24
|
+
scenarioInventory,
|
|
25
|
+
scenarioVars
|
|
26
|
+
} from "@ansible-flow/core/scenario/scenarios";
|
|
27
|
+
import { missWhy } from "@ansible-flow/core/scenario/view";
|
|
28
|
+
var EVENTS = "events";
|
|
29
|
+
var slug = (name) => String(name ?? "").toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-|-$/g, "").slice(0, 40) || "scenario";
|
|
30
|
+
async function exec(spec, log, timeoutMs, signal) {
|
|
31
|
+
const started = startProcess(spec, spec.env, log);
|
|
32
|
+
const stop = () => {
|
|
33
|
+
started.terminate();
|
|
34
|
+
void stopStarted(spec);
|
|
35
|
+
};
|
|
36
|
+
const timer = setTimeout(stop, timeoutMs);
|
|
37
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
38
|
+
const code = await started.exit;
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
signal?.removeEventListener("abort", stop);
|
|
41
|
+
return code ?? null;
|
|
42
|
+
}
|
|
43
|
+
function stageScenario(doc, s, dir, source) {
|
|
44
|
+
const generated = engineFor(doc).generate(doc, { source, simulate: true, stubAll: true });
|
|
45
|
+
if (!generated.ok || generated.files.length === 0) return void 0;
|
|
46
|
+
for (const f of generated.files) writeFileSync(join(dir, f.name), f.text);
|
|
47
|
+
writeFileSync(join(dir, "vars.json"), JSON.stringify(scenarioVars(s), null, 2));
|
|
48
|
+
writeFileSync(join(dir, "inventory.ini"), scenarioInventory(doc, s));
|
|
49
|
+
writeFileSync(join(dir, "ansible.cfg"), "");
|
|
50
|
+
return generated;
|
|
51
|
+
}
|
|
52
|
+
async function runOne(o, s, index, log) {
|
|
53
|
+
const dir = join(o.workDir, `${index + 1}-${slug(s.name)}`);
|
|
54
|
+
rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
const shared = o.env.kind === "container";
|
|
57
|
+
const ssh = o.env.kind === "ssh" ? o.env : void 0;
|
|
58
|
+
const eventsDir = shared || ssh?.container ? EVENTS : void 0;
|
|
59
|
+
chmodSync(dir, shared ? 493 : 448);
|
|
60
|
+
if (shared) {
|
|
61
|
+
mkdirSync(join(dir, EVENTS));
|
|
62
|
+
chmodSync(join(dir, EVENTS), 1023);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const generated = stageScenario(o.doc, s, dir, basename(o.flowFile));
|
|
66
|
+
if (!generated)
|
|
67
|
+
return judge(o.doc, s, void 0, "The flow does not generate: fix its errors first");
|
|
68
|
+
if (ssh) {
|
|
69
|
+
const failed = await syncWorkspace(ssh, { root: o.root, probe: o.probeDir }) ?? await putRunFolder(ssh, dir, eventsDir);
|
|
70
|
+
if (failed) return judge(o.doc, s, void 0, failed);
|
|
71
|
+
}
|
|
72
|
+
const inside = o.env.kind === "local" ? dir.replaceAll("\\", "/") : insidePaths(o.env, { root: o.root, probe: o.probeDir, run: dir }).run ?? dir;
|
|
73
|
+
const at = (name) => `${inside}/${name}`;
|
|
74
|
+
const spec = runProcess(
|
|
75
|
+
o.env,
|
|
76
|
+
{ root: o.root, probe: o.probeDir },
|
|
77
|
+
{
|
|
78
|
+
playbook: at(generated.files[0]?.name),
|
|
79
|
+
inventory: at("inventory.ini"),
|
|
80
|
+
check: false,
|
|
81
|
+
runDir: dir,
|
|
82
|
+
vault: false,
|
|
83
|
+
// The scenario build runs no binding: the repository's ansible.cfg has nothing to say,
|
|
84
|
+
// and the empty one written above keeps it from loading from the working directory.
|
|
85
|
+
hasConfig: false,
|
|
86
|
+
emptyConfig: true,
|
|
87
|
+
eventsDir
|
|
88
|
+
},
|
|
89
|
+
`afd-scenario-${Date.now().toString(36)}-${index}`
|
|
90
|
+
);
|
|
91
|
+
const rc = await exec(spec, log, o.timeoutMs ?? 3e5, o.signal);
|
|
92
|
+
const events = join(dir, eventsDir ?? "", "events.jsonl");
|
|
93
|
+
if (ssh) {
|
|
94
|
+
await fetchRunFile(ssh, dir, `${eventsDir ? `${eventsDir}/` : ""}events.jsonl`, events);
|
|
95
|
+
await removeRunDir(ssh, dir);
|
|
96
|
+
}
|
|
97
|
+
const text = existsSync(events) ? readFileSync(events, "utf8") : "";
|
|
98
|
+
if (o.signal?.aborted) return judge(o.doc, s, void 0, "Cancelled");
|
|
99
|
+
if (!text.trim() || rc === null || ![0, 2, 4].includes(rc))
|
|
100
|
+
return {
|
|
101
|
+
...judge(
|
|
102
|
+
o.doc,
|
|
103
|
+
s,
|
|
104
|
+
text.trim() ? summarizeRun(text, generated.routes) : void 0,
|
|
105
|
+
`ansible-playbook ended with ${rc ?? "an error"}${text.trim() ? "" : " before any routing event"}`
|
|
106
|
+
),
|
|
107
|
+
engine: "ansible"
|
|
108
|
+
};
|
|
109
|
+
return { ...judge(o.doc, s, summarizeRun(text, generated.routes)), engine: "ansible" };
|
|
110
|
+
} catch (e) {
|
|
111
|
+
return judge(o.doc, s, void 0, e.message);
|
|
112
|
+
} finally {
|
|
113
|
+
if (!o.keep) rmSync(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function runScenarios(o) {
|
|
117
|
+
const log = o.log ?? (() => void 0);
|
|
118
|
+
const results = [];
|
|
119
|
+
for (const [i, s] of o.file.scenarios.entries()) {
|
|
120
|
+
if (!s || typeof s !== "object") continue;
|
|
121
|
+
if (o.only && !o.only.includes(s.name)) continue;
|
|
122
|
+
if (o.signal?.aborted) break;
|
|
123
|
+
log(`# Scenario: ${s.name}`);
|
|
124
|
+
results.push(await runOne(o, s, i, log));
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
function describeResults(results, doc) {
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const r of results) {
|
|
131
|
+
if (skipped(r)) {
|
|
132
|
+
out.push(`SKIP ${r.name}: the simulator does not model this; verify it with a run`);
|
|
133
|
+
for (const u of r.needsAnsible ?? []) out.push(` ${u.message}`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const how = r.engine === "simulated" ? " (simulated)" : "";
|
|
137
|
+
out.push(`${r.passed ? "PASS" : "FAIL"} ${r.name}${how}${r.error ? `: ${r.error}` : ""}`);
|
|
138
|
+
for (const h of r.hosts)
|
|
139
|
+
if (h.expected !== h.actual) out.push(` ${missWhy(doc, h, r.forced, true)}`);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
var skipped = (r) => r.engine === "simulated" && (r.needsAnsible?.length ?? 0) > 0;
|
|
144
|
+
var failedCount = (results) => results.filter((r) => !r.passed && !skipped(r)).length;
|
|
145
|
+
var XML_FORBIDDEN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff]/g;
|
|
146
|
+
var esc = (t) => t.replaceAll(XML_FORBIDDEN, "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
147
|
+
function junitSuite(flow, results, doc) {
|
|
148
|
+
const failures = failedCount(results);
|
|
149
|
+
const skips = results.filter(skipped).length;
|
|
150
|
+
const cases = results.map((r) => {
|
|
151
|
+
if (skipped(r))
|
|
152
|
+
return ` <testcase classname="${esc(flow)}" name="${esc(r.name)}"><skipped message="${esc(`Verify with a run: ${(r.needsAnsible ?? []).map((u) => u.message).join("; ")}`)}"/></testcase>`;
|
|
153
|
+
if (r.passed) return ` <testcase classname="${esc(flow)}" name="${esc(r.name)}"/>`;
|
|
154
|
+
const text = describeResults([r], doc).join("\n");
|
|
155
|
+
return ` <testcase classname="${esc(flow)}" name="${esc(r.name)}"><failure message="${esc(r.error ?? "hosts ended elsewhere")}">${esc(text)}</failure></testcase>`;
|
|
156
|
+
});
|
|
157
|
+
return [
|
|
158
|
+
` <testsuite name="${esc(flow)}" tests="${results.length}" failures="${failures}"${skips ? ` skipped="${skips}"` : ""}>`,
|
|
159
|
+
...cases,
|
|
160
|
+
" </testsuite>"
|
|
161
|
+
].join("\n");
|
|
162
|
+
}
|
|
163
|
+
function junitDocument(suites) {
|
|
164
|
+
return [
|
|
165
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
166
|
+
"<testsuites>",
|
|
167
|
+
...suites,
|
|
168
|
+
"</testsuites>",
|
|
169
|
+
""
|
|
170
|
+
].join("\n");
|
|
171
|
+
}
|
|
172
|
+
function junitXml(flow, results, doc) {
|
|
173
|
+
return junitDocument([junitSuite(flow, results, doc)]);
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
describeResults,
|
|
177
|
+
failedCount,
|
|
178
|
+
junitDocument,
|
|
179
|
+
junitSuite,
|
|
180
|
+
junitXml,
|
|
181
|
+
runScenarios,
|
|
182
|
+
skipped,
|
|
183
|
+
stageScenario
|
|
184
|
+
};
|
|
185
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/scenario/runner.ts"],
|
|
4
|
+
"sourcesContent": ["// Runs a flow's scenarios (AFD-58) for the designer and the CI command line alike: per scenario, the\n// SCENARIO build of the flow (every step as its stub), a local-connection inventory and the extra\n// vars go into a folder of their own; ansible-playbook runs in the environment as any run does\n// (runProcess), and its routing events say where each host ended.\nimport { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { basename, join } from 'node:path';\nimport { engineFor } from '../../core/engine/engines';\nimport type { GenerateResult } from '../../core/engine/types';\nimport type { FlowDocument } from '../../core/flow/types';\nimport { insidePaths, type ProcessSpec, runProcess } from '../../core/run/command';\nimport type { Environment } from '../../core/run/environment';\nimport { summarizeRun } from '../../core/run/events';\nimport {\n judge,\n type Scenario,\n type ScenarioFile,\n type ScenarioResult,\n scenarioInventory,\n scenarioVars,\n} from '../../core/scenario/scenarios';\nimport { missWhy } from '../../core/scenario/view';\nimport { startProcess } from '../run/exec';\nimport { stopStarted } from '../run/stop';\nimport { fetchRunFile, putRunFolder, removeRunDir, syncWorkspace } from '../ssh/remote';\n\nexport interface ScenarioRunOptions {\n env: Environment;\n /** Repository root on this machine (mounted read-only in a container). */\n root: string;\n /** Folder with callback_plugins/afd_events.py. */\n probeDir: string;\n flowFile: string;\n doc: FlowDocument;\n file: ScenarioFile;\n /** Where each scenario's folder is made; removed afterwards unless `keep`. */\n workDir: string;\n only?: readonly string[];\n keep?: boolean;\n timeoutMs?: number;\n log?: (line: string) => void;\n signal?: AbortSignal;\n}\n\n/** The folder an EE image writes the events into, inside a scenario's folder (AFD-159). */\nconst EVENTS = 'events';\n\n/** AFD-159: a name that is not text (a number, nothing) still names a folder. */\nconst slug = (name: unknown) =>\n String(name ?? '')\n .toLowerCase()\n .replaceAll(/[^a-z0-9]+/g, '-')\n .replaceAll(/^-|-$/g, '')\n .slice(0, 40) || 'scenario';\n\n/** Runs one process to its end, feeding its output to `log`; resolves with the exit code. */\nasync function exec(\n spec: ProcessSpec,\n log: (line: string) => void,\n timeoutMs: number,\n signal?: AbortSignal,\n): Promise<number | null> {\n const started = startProcess(spec, spec.env, log);\n // Killing the client leaves a container or a WSL process running; spec.stop ends those.\n const stop = () => {\n started.terminate();\n void stopStarted(spec);\n };\n const timer = setTimeout(stop, timeoutMs);\n signal?.addEventListener('abort', stop, { once: true });\n const code = await started.exit;\n clearTimeout(timer);\n signal?.removeEventListener('abort', stop);\n return code ?? null;\n}\n\n/**\n * A scenario's folder: the scenario build of the flow, its extra vars and inventory (AFD-114: the\n * simulator's parity suite stages many into one container). Undefined when the flow does not\n * generate.\n */\nexport function stageScenario(\n doc: FlowDocument,\n s: Scenario,\n dir: string,\n source: string,\n): GenerateResult | undefined {\n const generated = engineFor(doc).generate(doc, { source, simulate: true, stubAll: true });\n if (!generated.ok || generated.files.length === 0) return undefined;\n for (const f of generated.files) writeFileSync(join(dir, f.name), f.text);\n writeFileSync(join(dir, 'vars.json'), JSON.stringify(scenarioVars(s), null, 2));\n writeFileSync(join(dir, 'inventory.ini'), scenarioInventory(doc, s));\n // AFD-126: ANSIBLE_CONFIG points here, so no ansible.cfg of the repository or the user applies.\n writeFileSync(join(dir, 'ansible.cfg'), '');\n return generated;\n}\n\nasync function runOne(\n o: ScenarioRunOptions,\n s: Scenario,\n index: number,\n log: (line: string) => void,\n): Promise<ScenarioResult> {\n const dir = join(o.workDir, `${index + 1}-${slug(s.name)}`);\n rmSync(dir, { recursive: true, force: true });\n mkdirSync(dir, { recursive: true });\n // AFD-159: Ansible loads plugins, group_vars and host_vars from beside the playbook and the\n // inventory, so no other user may write here (0777 let one swap them in on a shared CI host).\n // An EE image runs as a user of its own, which only reads here and writes the events into\n // EVENTS, open to all and sticky: nothing in it is loaded.\n const shared = o.env.kind === 'container';\n // AFD-247: an SSH host runs a copy of this folder; in an image there, the events folder too.\n const ssh = o.env.kind === 'ssh' ? o.env : undefined;\n const eventsDir = shared || ssh?.container ? EVENTS : undefined;\n chmodSync(dir, shared ? 0o755 : 0o700);\n if (shared) {\n mkdirSync(join(dir, EVENTS));\n chmodSync(join(dir, EVENTS), 0o1777);\n }\n try {\n const generated = stageScenario(o.doc, s, dir, basename(o.flowFile));\n if (!generated)\n return judge(o.doc, s, undefined, 'The flow does not generate: fix its errors first');\n if (ssh) {\n const failed =\n (await syncWorkspace(ssh, { root: o.root, probe: o.probeDir })) ??\n (await putRunFolder(ssh, dir, eventsDir));\n if (failed) return judge(o.doc, s, undefined, failed);\n }\n const inside =\n o.env.kind === 'local'\n ? dir.replaceAll('\\\\', '/')\n : (insidePaths(o.env, { root: o.root, probe: o.probeDir, run: dir }).run ?? dir);\n const at = (name: string) => `${inside}/${name}`;\n const spec = runProcess(\n o.env,\n { root: o.root, probe: o.probeDir },\n {\n playbook: at(generated.files[0]?.name as string),\n inventory: at('inventory.ini'),\n check: false,\n runDir: dir,\n vault: false,\n // The scenario build runs no binding: the repository's ansible.cfg has nothing to say,\n // and the empty one written above keeps it from loading from the working directory.\n hasConfig: false,\n emptyConfig: true,\n eventsDir,\n },\n `afd-scenario-${Date.now().toString(36)}-${index}`,\n );\n const rc = await exec(spec, log, o.timeoutMs ?? 300_000, o.signal);\n const events = join(dir, eventsDir ?? '', 'events.jsonl');\n if (ssh) {\n await fetchRunFile(ssh, dir, `${eventsDir ? `${eventsDir}/` : ''}events.jsonl`, events);\n await removeRunDir(ssh, dir);\n }\n const text = existsSync(events) ? readFileSync(events, 'utf8') : '';\n if (o.signal?.aborted) return judge(o.doc, s, undefined, 'Cancelled');\n // 2: hosts failed in the outcome play, as a scenario with failures expects.\n if (!text.trim() || rc === null || ![0, 2, 4].includes(rc))\n return {\n ...judge(\n o.doc,\n s,\n text.trim() ? summarizeRun(text, generated.routes) : undefined,\n `ansible-playbook ended with ${rc ?? 'an error'}${text.trim() ? '' : ' before any routing event'}`,\n ),\n engine: 'ansible',\n };\n return { ...judge(o.doc, s, summarizeRun(text, generated.routes)), engine: 'ansible' };\n } catch (e) {\n // AFD-159: a scenario that cannot be staged fails alone; the others still run.\n return judge(o.doc, s, undefined, (e as Error).message);\n } finally {\n if (!o.keep) rmSync(dir, { recursive: true, force: true });\n }\n}\n\nexport async function runScenarios(o: ScenarioRunOptions): Promise<ScenarioResult[]> {\n const log = o.log ?? (() => undefined);\n const results: ScenarioResult[] = [];\n for (const [i, s] of o.file.scenarios.entries()) {\n // AFD-159: checkScenarios reports an entry that is no map; a run without the check skips it.\n if (!s || typeof s !== 'object') continue;\n if (o.only && !o.only.includes(s.name)) continue;\n if (o.signal?.aborted) break;\n log(`# Scenario: ${s.name}`);\n results.push(await runOne(o, s, i, log));\n }\n return results;\n}\n\n/**\n * A one-line verdict per scenario, and the hosts that ended elsewhere. With the flow at hand, the\n * ends by their titles with the keys a scenario file uses in brackets (AFD-94). AFD-105: in the\n * panel's wording, with why when the scenario planted the host's outcome.\n */\nexport function describeResults(results: readonly ScenarioResult[], doc?: FlowDocument): string[] {\n const out: string[] = [];\n for (const r of results) {\n if (skipped(r)) {\n out.push(`SKIP ${r.name}: the simulator does not model this; verify it with a run`);\n for (const u of r.needsAnsible ?? []) out.push(` ${u.message}`);\n continue;\n }\n const how = r.engine === 'simulated' ? ' (simulated)' : '';\n out.push(`${r.passed ? 'PASS' : 'FAIL'} ${r.name}${how}${r.error ? `: ${r.error}` : ''}`);\n for (const h of r.hosts)\n if (h.expected !== h.actual) out.push(` ${missWhy(doc, h, r.forced, true)}`);\n }\n return out;\n}\n\n/**\n * AFD-114: a simulated scenario that has what the simulator does not model is not judged on the\n * command line: its verdict is a best effort, so CI skips it rather than pass or fail on a guess.\n */\nexport const skipped = (r: ScenarioResult) =>\n r.engine === 'simulated' && (r.needsAnsible?.length ?? 0) > 0;\n\n/** Scenarios that failed, leaving out the skipped. */\nexport const failedCount = (results: readonly ScenarioResult[]) =>\n results.filter((r) => !r.passed && !skipped(r)).length;\n\n/** JUnit XML for CI dashboards: one test case per scenario. */\n/**\n * Text for XML: escaped, and without the control characters XML 1.0 forbids even as references\n * (AFD-158: Ansible output can carry terminal colour codes).\n */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: removing control characters is the point.\nconst XML_FORBIDDEN = /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f\\ufffe\\uffff]/g;\nconst esc = (t: string) =>\n t\n .replaceAll(XML_FORBIDDEN, '')\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"');\n\n/** One flow's scenarios as a JUnit `<testsuite>`. */\nexport function junitSuite(\n flow: string,\n results: readonly ScenarioResult[],\n doc?: FlowDocument,\n): string {\n const failures = failedCount(results);\n const skips = results.filter(skipped).length;\n const cases = results.map((r) => {\n if (skipped(r))\n return ` <testcase classname=\"${esc(flow)}\" name=\"${esc(r.name)}\"><skipped message=\"${esc(`Verify with a run: ${(r.needsAnsible ?? []).map((u) => u.message).join('; ')}`)}\"/></testcase>`;\n if (r.passed) return ` <testcase classname=\"${esc(flow)}\" name=\"${esc(r.name)}\"/>`;\n const text = describeResults([r], doc).join('\\n');\n return ` <testcase classname=\"${esc(flow)}\" name=\"${esc(r.name)}\"><failure message=\"${esc(r.error ?? 'hosts ended elsewhere')}\">${esc(text)}</failure></testcase>`;\n });\n return [\n ` <testsuite name=\"${esc(flow)}\" tests=\"${results.length}\" failures=\"${failures}\"${skips ? ` skipped=\"${skips}\"` : ''}>`,\n ...cases,\n ' </testsuite>',\n ].join('\\n');\n}\n\n/**\n * A JUnit file of several flows' suites under one root (AFD-158: one document per flow, joined,\n * was not XML once there were two flows).\n */\nexport function junitDocument(suites: readonly string[]): string {\n return [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<testsuites>',\n ...suites,\n '</testsuites>',\n '',\n ].join('\\n');\n}\n\n/** One flow's scenarios as a whole JUnit file. */\nexport function junitXml(\n flow: string,\n results: readonly ScenarioResult[],\n doc?: FlowDocument,\n): string {\n return junitDocument([junitSuite(flow, results, doc)]);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FlowDocument, FlowNode } from '@ansible-flow/core/flow/types';
|
|
2
|
+
import type { Finding } from '@ansible-flow/core/flow/validate';
|
|
3
|
+
export interface SharedSource {
|
|
4
|
+
/** The source flow's version and the subflow at the key path. */
|
|
5
|
+
version?: number;
|
|
6
|
+
node?: FlowNode;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The file a shared subflow's `ref.from` names, checked before anything on disk is touched
|
|
11
|
+
* (AFD-128): a UNC path would make Windows connect to that host and offer the user's credentials,
|
|
12
|
+
* and a device or a huge file would be read whole. It must be a relative path to a .flow.yaml that
|
|
13
|
+
* stays, links resolved, inside `root`: the workspace folder, else the folder above the flow's own.
|
|
14
|
+
*/
|
|
15
|
+
export declare function sharedFile(flowFile: string, path: string, root?: string): {
|
|
16
|
+
file?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
};
|
|
19
|
+
/** The source of `from`, for the flow at `flowFile` in the workspace folder `root`. */
|
|
20
|
+
export declare function sharedSource(flowFile: string, from: string, root?: string): SharedSource;
|
|
21
|
+
/** Problems for the flow's shared subflows: a source that is gone, or a newer version of it. */
|
|
22
|
+
export declare function sharedFindings(flowFile: string, doc: FlowDocument, root?: string): Finding[];
|