@boboddy/sdk 0.2.10-alpha → 0.2.13-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boboddy-config-parser.d.ts +0 -5
- package/dist/boboddy-config-parser.js +0 -6
- package/dist/definitions/steps/define-step.d.ts +22 -1
- package/dist/definitions/steps/index.js +2 -1
- package/dist/definitions/steps/step-definitions-client.d.ts +30 -0
- package/dist/definitions/validation/index.d.ts +2 -0
- package/dist/definitions/validation/index.js +440 -0
- package/dist/definitions/validation/json-schema-paths.d.ts +45 -0
- package/dist/definitions/validation/validate-definition-specs.d.ts +25 -0
- package/dist/generated/types.gen.d.ts +110 -5
- package/dist/health-checks.d.ts +45 -0
- package/dist/health-checks.js +14308 -0
- package/dist/index.js +2 -7
- package/dist/push/collect-definitions.d.ts +21 -0
- package/dist/push/index.d.ts +2 -0
- package/dist/push/index.js +471 -68
- package/dist/push/push-from-directory.d.ts +7 -1
- package/dist/step-execution-plane-client.d.ts +10 -5
- package/package.json +9 -1
package/dist/index.js
CHANGED
|
@@ -16057,7 +16057,8 @@ ${feature._promptAddition}` : feature._promptAddition;
|
|
|
16057
16057
|
}))
|
|
16058
16058
|
],
|
|
16059
16059
|
opencodeMcpJson: config2.mcpServers ?? null,
|
|
16060
|
-
opencodePluginJson: config2.plugins ?? null
|
|
16060
|
+
opencodePluginJson: config2.plugins ?? null,
|
|
16061
|
+
healthChecksJson: config2.healthChecks ?? null
|
|
16061
16062
|
};
|
|
16062
16063
|
return spec;
|
|
16063
16064
|
}
|
|
@@ -17186,7 +17187,6 @@ var parseServices = (raw) => {
|
|
|
17186
17187
|
return [];
|
|
17187
17188
|
return Object.entries(raw).map(([name, entry]) => {
|
|
17188
17189
|
const expose = entry.expose ?? {};
|
|
17189
|
-
const healthcheck = entry.healthcheck ?? {};
|
|
17190
17190
|
const dependsOn = Array.isArray(entry.dependsOn) ? entry.dependsOn.filter((d) => typeof d === "string") : [];
|
|
17191
17191
|
return {
|
|
17192
17192
|
name,
|
|
@@ -17197,11 +17197,6 @@ var parseServices = (raw) => {
|
|
|
17197
17197
|
expose: {
|
|
17198
17198
|
targetPort: asNumber(expose.targetPort) ?? 0,
|
|
17199
17199
|
protocol: asString(expose.protocol) ?? "http"
|
|
17200
|
-
},
|
|
17201
|
-
healthcheck: {
|
|
17202
|
-
protocol: asString(healthcheck.protocol) ?? "http",
|
|
17203
|
-
path: asString(healthcheck.path) ?? null,
|
|
17204
|
-
expectedStatus: asNumber(healthcheck.expectedStatus) ?? null
|
|
17205
17200
|
}
|
|
17206
17201
|
};
|
|
17207
17202
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PipelineDefinitionSpec } from "../definitions/pipelines";
|
|
2
|
+
import { type DefaultPipelineAssignmentSpec } from "../definitions/pipelines/define-default-pipeline-assignment";
|
|
3
|
+
import type { StepDefinitionSpec } from "../definitions/steps";
|
|
4
|
+
export type CollectedDefinitions = {
|
|
5
|
+
readonly pipelines: readonly PipelineDefinitionSpec[];
|
|
6
|
+
/** Deduped by `key@vN`; named exports take precedence over embedded steps. */
|
|
7
|
+
readonly steps: readonly StepDefinitionSpec[];
|
|
8
|
+
/** Present only when `default-pipeline-assignment.ts` exists in the directory. */
|
|
9
|
+
readonly defaultPipelineAssignment: DefaultPipelineAssignmentSpec | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Imports every `.ts`/`.js` file in `dir` (except the push script itself and
|
|
13
|
+
* `default-pipeline-assignment.ts`) and collects the pipeline and step
|
|
14
|
+
* definitions they export. The assignment file, when present, is imported and
|
|
15
|
+
* validated too but returned separately — syncing it needs the server.
|
|
16
|
+
*
|
|
17
|
+
* Designed to run on the user's native runtime (bun, node-with-tsx, deno), NOT
|
|
18
|
+
* inside a `bun --compile`'d binary — that runtime can't resolve scoped package
|
|
19
|
+
* `exports` field remappings from external user files.
|
|
20
|
+
*/
|
|
21
|
+
export declare function collectDefinitionsFromDirectory(dir: string): Promise<CollectedDefinitions>;
|
package/dist/push/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
export { collectDefinitionsFromDirectory } from "./collect-definitions";
|
|
2
|
+
export type { CollectedDefinitions } from "./collect-definitions";
|
|
1
3
|
export { pushFromDirectory } from "./push-from-directory";
|
|
2
4
|
export type { PushFromDirectoryOptions, PushFromDirectoryResult, } from "./push-from-directory";
|
package/dist/push/index.js
CHANGED
|
@@ -12075,7 +12075,8 @@ ${feature._promptAddition}` : feature._promptAddition;
|
|
|
12075
12075
|
}))
|
|
12076
12076
|
],
|
|
12077
12077
|
opencodeMcpJson: config2.mcpServers ?? null,
|
|
12078
|
-
opencodePluginJson: config2.plugins ?? null
|
|
12078
|
+
opencodePluginJson: config2.plugins ?? null,
|
|
12079
|
+
healthChecksJson: config2.healthChecks ?? null
|
|
12079
12080
|
};
|
|
12080
12081
|
return spec;
|
|
12081
12082
|
}
|
|
@@ -16784,10 +16785,428 @@ function isDefaultPipelineAssignmentSpec(value) {
|
|
|
16784
16785
|
return false;
|
|
16785
16786
|
return value["_tag"] === "default_pipeline_assignment";
|
|
16786
16787
|
}
|
|
16787
|
-
// src/
|
|
16788
|
+
// src/definitions/validation/json-schema-paths.ts
|
|
16789
|
+
var SEGMENT_PATTERN = /([^.[\]]+)|(\[(\d+)\])/g;
|
|
16790
|
+
var NUMERIC_SEGMENT = /^\d+$/;
|
|
16791
|
+
var SCALAR_TYPES = new Set([
|
|
16792
|
+
"string",
|
|
16793
|
+
"number",
|
|
16794
|
+
"integer",
|
|
16795
|
+
"boolean",
|
|
16796
|
+
"null"
|
|
16797
|
+
]);
|
|
16798
|
+
var MAX_REF_HOPS = 16;
|
|
16799
|
+
var MAX_CANDIDATES = 64;
|
|
16800
|
+
function parseSourcePath(sourcePath) {
|
|
16801
|
+
const trimmed = sourcePath.trim();
|
|
16802
|
+
const normalized = trimmed === "$" ? "" : trimmed.startsWith("$.") ? trimmed.slice(2) : trimmed;
|
|
16803
|
+
if (!normalized)
|
|
16804
|
+
return [];
|
|
16805
|
+
return [...normalized.matchAll(SEGMENT_PATTERN)].map((match) => match[1] ?? match[3]).filter((segment) => Boolean(segment));
|
|
16806
|
+
}
|
|
16807
|
+
function isSchemaNode(value) {
|
|
16808
|
+
return typeof value === "boolean" || typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16809
|
+
}
|
|
16810
|
+
function asRecord(node) {
|
|
16811
|
+
return typeof node === "boolean" ? null : node;
|
|
16812
|
+
}
|
|
16813
|
+
function resolveRef2(root, ref) {
|
|
16814
|
+
if (!ref.startsWith("#"))
|
|
16815
|
+
return null;
|
|
16816
|
+
const pointer = ref.slice(1);
|
|
16817
|
+
if (pointer === "" || pointer === "/")
|
|
16818
|
+
return root;
|
|
16819
|
+
if (!pointer.startsWith("/"))
|
|
16820
|
+
return null;
|
|
16821
|
+
let current = root;
|
|
16822
|
+
for (const rawToken of pointer.slice(1).split("/")) {
|
|
16823
|
+
const token = rawToken.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
16824
|
+
if (typeof current !== "object" || current === null)
|
|
16825
|
+
return null;
|
|
16826
|
+
current = current[token];
|
|
16827
|
+
}
|
|
16828
|
+
return isSchemaNode(current) ? current : null;
|
|
16829
|
+
}
|
|
16830
|
+
function flatten(node, root) {
|
|
16831
|
+
const out = [];
|
|
16832
|
+
const queue = [
|
|
16833
|
+
{ node, hops: 0 }
|
|
16834
|
+
];
|
|
16835
|
+
while (queue.length > 0) {
|
|
16836
|
+
const entry = queue.shift();
|
|
16837
|
+
if (!entry)
|
|
16838
|
+
break;
|
|
16839
|
+
if (out.length >= MAX_CANDIDATES)
|
|
16840
|
+
return null;
|
|
16841
|
+
if (entry.hops > MAX_REF_HOPS)
|
|
16842
|
+
return null;
|
|
16843
|
+
const record2 = asRecord(entry.node);
|
|
16844
|
+
if (!record2) {
|
|
16845
|
+
out.push(entry.node);
|
|
16846
|
+
continue;
|
|
16847
|
+
}
|
|
16848
|
+
const ref = record2["$ref"];
|
|
16849
|
+
if (typeof ref === "string") {
|
|
16850
|
+
const target = resolveRef2(root, ref);
|
|
16851
|
+
if (!target)
|
|
16852
|
+
return null;
|
|
16853
|
+
queue.push({ node: target, hops: entry.hops + 1 });
|
|
16854
|
+
continue;
|
|
16855
|
+
}
|
|
16856
|
+
const branches = ["anyOf", "oneOf", "allOf"].flatMap((keyword) => {
|
|
16857
|
+
const value = record2[keyword];
|
|
16858
|
+
return Array.isArray(value) ? value.filter(isSchemaNode) : [];
|
|
16859
|
+
});
|
|
16860
|
+
if (branches.length > 0) {
|
|
16861
|
+
for (const branch of branches) {
|
|
16862
|
+
queue.push({ node: branch, hops: entry.hops + 1 });
|
|
16863
|
+
}
|
|
16864
|
+
continue;
|
|
16865
|
+
}
|
|
16866
|
+
out.push(entry.node);
|
|
16867
|
+
}
|
|
16868
|
+
return out;
|
|
16869
|
+
}
|
|
16870
|
+
function typeNames(record2) {
|
|
16871
|
+
const raw = record2["type"];
|
|
16872
|
+
if (typeof raw === "string")
|
|
16873
|
+
return new Set([raw]);
|
|
16874
|
+
if (Array.isArray(raw)) {
|
|
16875
|
+
return new Set(raw.filter((entry) => typeof entry === "string"));
|
|
16876
|
+
}
|
|
16877
|
+
return new Set;
|
|
16878
|
+
}
|
|
16879
|
+
var INDETERMINATE = { kind: "indeterminate" };
|
|
16880
|
+
function stepIntoObject(record2, segment) {
|
|
16881
|
+
const properties = asRecord(isSchemaNode(record2["properties"]) ? record2["properties"] : {});
|
|
16882
|
+
const declared = properties ?? {};
|
|
16883
|
+
const child = declared[segment];
|
|
16884
|
+
if (isSchemaNode(child))
|
|
16885
|
+
return { kind: "child", node: child };
|
|
16886
|
+
if (record2["patternProperties"] !== undefined)
|
|
16887
|
+
return INDETERMINATE;
|
|
16888
|
+
if (record2["additionalProperties"] !== false)
|
|
16889
|
+
return INDETERMINATE;
|
|
16890
|
+
return {
|
|
16891
|
+
kind: "invalid",
|
|
16892
|
+
reason: "unknown-property",
|
|
16893
|
+
availablePaths: Object.keys(declared).sort()
|
|
16894
|
+
};
|
|
16895
|
+
}
|
|
16896
|
+
function stepIntoArray(record2, segment) {
|
|
16897
|
+
if (!NUMERIC_SEGMENT.test(segment)) {
|
|
16898
|
+
return {
|
|
16899
|
+
kind: "invalid",
|
|
16900
|
+
reason: "not-an-array-index",
|
|
16901
|
+
availablePaths: []
|
|
16902
|
+
};
|
|
16903
|
+
}
|
|
16904
|
+
const prefixItems = record2["prefixItems"];
|
|
16905
|
+
if (Array.isArray(prefixItems)) {
|
|
16906
|
+
const positional = prefixItems[Number(segment)];
|
|
16907
|
+
if (isSchemaNode(positional))
|
|
16908
|
+
return { kind: "child", node: positional };
|
|
16909
|
+
}
|
|
16910
|
+
const items = record2["items"];
|
|
16911
|
+
if (isSchemaNode(items))
|
|
16912
|
+
return { kind: "child", node: items };
|
|
16913
|
+
return INDETERMINATE;
|
|
16914
|
+
}
|
|
16915
|
+
function stepInto(node, segment) {
|
|
16916
|
+
const record2 = asRecord(node);
|
|
16917
|
+
if (!record2 || Object.keys(record2).length === 0)
|
|
16918
|
+
return INDETERMINATE;
|
|
16919
|
+
const types = typeNames(record2);
|
|
16920
|
+
const objectLike = types.has("object") || record2["properties"] !== undefined || record2["patternProperties"] !== undefined || record2["additionalProperties"] !== undefined;
|
|
16921
|
+
const arrayLike = types.has("array") || record2["items"] !== undefined || record2["prefixItems"] !== undefined;
|
|
16922
|
+
const outcomes = [];
|
|
16923
|
+
if (objectLike)
|
|
16924
|
+
outcomes.push(stepIntoObject(record2, segment));
|
|
16925
|
+
if (arrayLike)
|
|
16926
|
+
outcomes.push(stepIntoArray(record2, segment));
|
|
16927
|
+
if (outcomes.length === 0) {
|
|
16928
|
+
if (types.size > 0 && [...types].every((name) => SCALAR_TYPES.has(name))) {
|
|
16929
|
+
return {
|
|
16930
|
+
kind: "invalid",
|
|
16931
|
+
reason: "scalar-has-no-members",
|
|
16932
|
+
availablePaths: []
|
|
16933
|
+
};
|
|
16934
|
+
}
|
|
16935
|
+
return INDETERMINATE;
|
|
16936
|
+
}
|
|
16937
|
+
return combine(outcomes);
|
|
16938
|
+
}
|
|
16939
|
+
function combine(outcomes) {
|
|
16940
|
+
const children = outcomes.filter((outcome) => outcome.kind === "child");
|
|
16941
|
+
if (children.length > 0)
|
|
16942
|
+
return children[0] ?? INDETERMINATE;
|
|
16943
|
+
if (outcomes.some((outcome) => outcome.kind === "indeterminate")) {
|
|
16944
|
+
return INDETERMINATE;
|
|
16945
|
+
}
|
|
16946
|
+
const invalid = outcomes.filter((outcome) => outcome.kind === "invalid");
|
|
16947
|
+
const first = invalid[0];
|
|
16948
|
+
if (!first)
|
|
16949
|
+
return INDETERMINATE;
|
|
16950
|
+
return {
|
|
16951
|
+
kind: "invalid",
|
|
16952
|
+
reason: first.reason,
|
|
16953
|
+
availablePaths: [
|
|
16954
|
+
...new Set(invalid.flatMap((outcome) => outcome.availablePaths))
|
|
16955
|
+
].sort()
|
|
16956
|
+
};
|
|
16957
|
+
}
|
|
16958
|
+
function enumeratePaths(node, root, maxDepth = 3, limit = 40) {
|
|
16959
|
+
const out = [];
|
|
16960
|
+
const visit = (current, prefix, depth) => {
|
|
16961
|
+
if (out.length >= limit || depth > maxDepth)
|
|
16962
|
+
return;
|
|
16963
|
+
for (const branch of flatten(current, root) ?? []) {
|
|
16964
|
+
const record2 = asRecord(branch);
|
|
16965
|
+
const properties = record2 ? asRecord(isSchemaNode(record2["properties"]) ? record2["properties"] : {}) : null;
|
|
16966
|
+
if (!properties)
|
|
16967
|
+
continue;
|
|
16968
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
16969
|
+
if (out.length >= limit)
|
|
16970
|
+
return;
|
|
16971
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
16972
|
+
out.push(path);
|
|
16973
|
+
if (isSchemaNode(child))
|
|
16974
|
+
visit(child, path, depth + 1);
|
|
16975
|
+
}
|
|
16976
|
+
}
|
|
16977
|
+
};
|
|
16978
|
+
visit(node, "", 1);
|
|
16979
|
+
return [...new Set(out)].sort();
|
|
16980
|
+
}
|
|
16981
|
+
function resolveSourcePath(schema, sourcePath) {
|
|
16982
|
+
const segments = parseSourcePath(sourcePath);
|
|
16983
|
+
if (segments.length === 0)
|
|
16984
|
+
return { kind: "resolved" };
|
|
16985
|
+
let candidates = [schema];
|
|
16986
|
+
let resolvedPrefix = "";
|
|
16987
|
+
for (const segment of segments) {
|
|
16988
|
+
const expanded = candidates.flatMap((node) => flatten(node, schema) ?? []);
|
|
16989
|
+
if (expanded.length === 0)
|
|
16990
|
+
return { kind: "indeterminate" };
|
|
16991
|
+
const outcome = combine(expanded.map((node) => stepInto(node, segment)));
|
|
16992
|
+
if (outcome.kind === "indeterminate")
|
|
16993
|
+
return { kind: "indeterminate" };
|
|
16994
|
+
if (outcome.kind === "invalid") {
|
|
16995
|
+
const availablePaths = [
|
|
16996
|
+
...new Set(expanded.flatMap((node) => enumeratePaths(node, schema)))
|
|
16997
|
+
].sort();
|
|
16998
|
+
return {
|
|
16999
|
+
kind: "invalid",
|
|
17000
|
+
resolvedPrefix,
|
|
17001
|
+
segment,
|
|
17002
|
+
reason: outcome.reason,
|
|
17003
|
+
availablePaths
|
|
17004
|
+
};
|
|
17005
|
+
}
|
|
17006
|
+
candidates = [outcome.node];
|
|
17007
|
+
resolvedPrefix = resolvedPrefix ? `${resolvedPrefix}.${segment}` : segment;
|
|
17008
|
+
}
|
|
17009
|
+
return { kind: "resolved" };
|
|
17010
|
+
}
|
|
17011
|
+
// src/definitions/validation/validate-definition-specs.ts
|
|
17012
|
+
function listPaths(paths, limit = 24) {
|
|
17013
|
+
if (paths.length === 0)
|
|
17014
|
+
return "";
|
|
17015
|
+
if (paths.length <= limit)
|
|
17016
|
+
return paths.join(", ");
|
|
17017
|
+
return `${paths.slice(0, limit).join(", ")}, \u2026 (${String(paths.length - limit)} more)`;
|
|
17018
|
+
}
|
|
17019
|
+
function quotedOrRoot(prefix) {
|
|
17020
|
+
return prefix ? `"${prefix}"` : "the result root";
|
|
17021
|
+
}
|
|
17022
|
+
function checkSignalSourcePaths(steps) {
|
|
17023
|
+
const issues = [];
|
|
17024
|
+
for (const step of steps) {
|
|
17025
|
+
const schema = step.resultSchemaJson ?? null;
|
|
17026
|
+
if (!schema)
|
|
17027
|
+
continue;
|
|
17028
|
+
for (const signal2 of step.signalExtractorDefinitions) {
|
|
17029
|
+
const resolution = resolveSourcePath(schema, signal2.sourcePath);
|
|
17030
|
+
if (resolution.kind !== "invalid")
|
|
17031
|
+
continue;
|
|
17032
|
+
const { resolvedPrefix, segment, reason, availablePaths } = resolution;
|
|
17033
|
+
const cause = reason === "not-an-array-index" ? `${quotedOrRoot(resolvedPrefix)} is an array, so "${segment}" can never index it \u2014 array segments must be numeric (e.g. "${resolvedPrefix}[0]")` : reason === "scalar-has-no-members" ? `${quotedOrRoot(resolvedPrefix)} is a scalar, so it has no property "${segment}"` : `${quotedOrRoot(resolvedPrefix)} has no property "${segment}"`;
|
|
17034
|
+
const suffix = availablePaths.length > 0 ? ` Valid sourcePaths ${resolvedPrefix ? `under "${resolvedPrefix}"` : "for this step"}: ${listPaths(availablePaths)}.` : "";
|
|
17035
|
+
issues.push({
|
|
17036
|
+
check: "signal-source-path",
|
|
17037
|
+
message: `Step "${step.key}" declares signal "${signal2.key}" with sourcePath ` + `"${signal2.sourcePath}", which can never resolve against the step's ` + `result schema: ${cause}.${suffix}`
|
|
17038
|
+
});
|
|
17039
|
+
}
|
|
17040
|
+
}
|
|
17041
|
+
return issues;
|
|
17042
|
+
}
|
|
17043
|
+
function checkHealthChecks(steps) {
|
|
17044
|
+
const issues = [];
|
|
17045
|
+
for (const step of steps) {
|
|
17046
|
+
const checks3 = step.healthChecksJson ?? [];
|
|
17047
|
+
if (checks3.length === 0)
|
|
17048
|
+
continue;
|
|
17049
|
+
const mcpServerKeys = Object.keys(step.opencodeMcpJson ?? {});
|
|
17050
|
+
const mcpServerKeySet = new Set(mcpServerKeys);
|
|
17051
|
+
checks3.forEach((check2, index) => {
|
|
17052
|
+
const label = check2.name ?? check2.tool;
|
|
17053
|
+
const where = `Step "${step.key}" health check #${String(index + 1)} ("${label}")`;
|
|
17054
|
+
if (!check2.mcp)
|
|
17055
|
+
return;
|
|
17056
|
+
if (!mcpServerKeySet.has(check2.mcp)) {
|
|
17057
|
+
issues.push({
|
|
17058
|
+
check: "health-check-mcp-server",
|
|
17059
|
+
message: `${where} names MCP server "${check2.mcp}", but the step declares no ` + `such server in mcpServers. Declared servers: ${mcpServerKeys.length > 0 ? listPaths([...mcpServerKeys].sort()) : "(none)"}.`
|
|
17060
|
+
});
|
|
17061
|
+
}
|
|
17062
|
+
const prefix = `${check2.mcp}_`;
|
|
17063
|
+
if (check2.tool.startsWith(prefix)) {
|
|
17064
|
+
issues.push({
|
|
17065
|
+
check: "health-check-double-qualified",
|
|
17066
|
+
message: `${where} sets mcp "${check2.mcp}" and tool "${check2.tool}", which already ` + `starts with "${prefix}". When "mcp" is set, "tool" should be the bare tool ` + `name \u2014 OpenCode resolves it to "${prefix}${check2.tool}". Did you mean ` + `tool: "${check2.tool.slice(prefix.length)}"?`
|
|
17067
|
+
});
|
|
17068
|
+
}
|
|
17069
|
+
});
|
|
17070
|
+
}
|
|
17071
|
+
return issues;
|
|
17072
|
+
}
|
|
17073
|
+
function routeTargets(policy) {
|
|
17074
|
+
const keys = [];
|
|
17075
|
+
if (policy.defaultEventType === "route" && typeof policy.defaultEventParamsJson?.["pipelineKey"] === "string") {
|
|
17076
|
+
keys.push(policy.defaultEventParamsJson["pipelineKey"]);
|
|
17077
|
+
}
|
|
17078
|
+
for (const rule of policy.rulesJson.rules) {
|
|
17079
|
+
if (rule.event.type === "route" && typeof rule.event.params?.["pipelineKey"] === "string") {
|
|
17080
|
+
keys.push(rule.event.params["pipelineKey"]);
|
|
17081
|
+
}
|
|
17082
|
+
}
|
|
17083
|
+
return keys;
|
|
17084
|
+
}
|
|
17085
|
+
function checkRouteTargets(pipelines, knownPipelineKeys) {
|
|
17086
|
+
const issues = [];
|
|
17087
|
+
const known = new Set([
|
|
17088
|
+
...pipelines.map((pipeline2) => pipeline2.key),
|
|
17089
|
+
...knownPipelineKeys
|
|
17090
|
+
]);
|
|
17091
|
+
for (const pipeline2 of pipelines) {
|
|
17092
|
+
for (const step of pipeline2.steps) {
|
|
17093
|
+
for (const target of routeTargets(step.advancementPolicyDefinition)) {
|
|
17094
|
+
if (known.has(target))
|
|
17095
|
+
continue;
|
|
17096
|
+
issues.push({
|
|
17097
|
+
check: "route-target",
|
|
17098
|
+
message: `Pipeline "${pipeline2.key}" step "${step.stepKey}" routes to pipeline ` + `"${target}", but no pipeline with that key was found on the server or ` + `in the current push batch. Push the target pipeline first.`
|
|
17099
|
+
});
|
|
17100
|
+
}
|
|
17101
|
+
}
|
|
17102
|
+
}
|
|
17103
|
+
return issues;
|
|
17104
|
+
}
|
|
17105
|
+
function executionRanks(steps) {
|
|
17106
|
+
const positions = steps.map((step) => step.position);
|
|
17107
|
+
const usable = positions.every((value) => Number.isInteger(value) && value > 0) && new Set(positions).size === positions.length;
|
|
17108
|
+
const indexes = steps.map((_, index) => index);
|
|
17109
|
+
const ordered = usable ? [...indexes].sort((left, right) => (positions[left] ?? 0) - (positions[right] ?? 0)) : indexes;
|
|
17110
|
+
return new Map(ordered.map((index, rank) => [index, rank]));
|
|
17111
|
+
}
|
|
17112
|
+
function bindingSource(binding) {
|
|
17113
|
+
if (binding.source === "step_signal") {
|
|
17114
|
+
return { stepKey: binding.stepKey, signalKey: binding.signalKey };
|
|
17115
|
+
}
|
|
17116
|
+
if (binding.source === "step_output") {
|
|
17117
|
+
return { stepKey: binding.stepKey, signalKey: null };
|
|
17118
|
+
}
|
|
17119
|
+
return null;
|
|
17120
|
+
}
|
|
17121
|
+
function declaredSignalKeys(stepKey, stepsByKey, pipelineSteps) {
|
|
17122
|
+
const specs = stepsByKey.get(stepKey);
|
|
17123
|
+
if (!specs || specs.length === 0)
|
|
17124
|
+
return null;
|
|
17125
|
+
const keys = new Set;
|
|
17126
|
+
for (const spec of specs) {
|
|
17127
|
+
for (const signal2 of spec.signalExtractorDefinitions)
|
|
17128
|
+
keys.add(signal2.key);
|
|
17129
|
+
}
|
|
17130
|
+
for (const step of pipelineSteps) {
|
|
17131
|
+
if (step.stepKey !== stepKey)
|
|
17132
|
+
continue;
|
|
17133
|
+
for (const computed of step.computedSignalDefinitions)
|
|
17134
|
+
keys.add(computed.key);
|
|
17135
|
+
}
|
|
17136
|
+
return [...keys];
|
|
17137
|
+
}
|
|
17138
|
+
function checkSignalBindings(pipelines, stepsByKey) {
|
|
17139
|
+
const issues = [];
|
|
17140
|
+
for (const pipeline2 of pipelines) {
|
|
17141
|
+
const ranks = executionRanks(pipeline2.steps);
|
|
17142
|
+
const order = [...pipeline2.steps.keys()].sort((left, right) => (ranks.get(left) ?? 0) - (ranks.get(right) ?? 0)).map((index) => pipeline2.steps[index]?.stepKey ?? "");
|
|
17143
|
+
const orderHint = `Steps in "${pipeline2.key}", in order: ${order.join(" \u2192 ")}.`;
|
|
17144
|
+
pipeline2.steps.forEach((step, index) => {
|
|
17145
|
+
const consumerRank = ranks.get(index) ?? index;
|
|
17146
|
+
const where = `Pipeline "${pipeline2.key}" step "${step.stepKey}"`;
|
|
17147
|
+
for (const [field, binding] of Object.entries(step.inputBindingsJson)) {
|
|
17148
|
+
const source = bindingSource(binding);
|
|
17149
|
+
if (!source)
|
|
17150
|
+
continue;
|
|
17151
|
+
const what = source.signalKey ? `binds input "${field}" to signal "${source.signalKey}" of step "${source.stepKey}"` : `binds input "${field}" to the output of step "${source.stepKey}"`;
|
|
17152
|
+
const producerRanks = pipeline2.steps.map((candidate, candidateIndex) => candidate.stepKey === source.stepKey ? ranks.get(candidateIndex) ?? candidateIndex : null).filter((rank) => rank !== null);
|
|
17153
|
+
if (producerRanks.length === 0) {
|
|
17154
|
+
issues.push({
|
|
17155
|
+
check: "signal-binding",
|
|
17156
|
+
message: `${where} ${what}, but no step with that key is in the pipeline. ${orderHint}`
|
|
17157
|
+
});
|
|
17158
|
+
continue;
|
|
17159
|
+
}
|
|
17160
|
+
if (!producerRanks.some((rank) => rank < consumerRank)) {
|
|
17161
|
+
issues.push({
|
|
17162
|
+
check: "signal-binding",
|
|
17163
|
+
message: `${where} ${what}, but that step does not run before it, so the ` + `value will never exist. ${orderHint}`
|
|
17164
|
+
});
|
|
17165
|
+
continue;
|
|
17166
|
+
}
|
|
17167
|
+
if (source.signalKey === null)
|
|
17168
|
+
continue;
|
|
17169
|
+
const available = declaredSignalKeys(source.stepKey, stepsByKey, pipeline2.steps);
|
|
17170
|
+
if (available === null || available.includes(source.signalKey))
|
|
17171
|
+
continue;
|
|
17172
|
+
issues.push({
|
|
17173
|
+
check: "signal-binding",
|
|
17174
|
+
message: `${where} ${what}, but "${source.stepKey}" declares no such signal. ` + `Signals on "${source.stepKey}": ${available.length > 0 ? listPaths([...available].sort()) : "(none)"}.`
|
|
17175
|
+
});
|
|
17176
|
+
}
|
|
17177
|
+
});
|
|
17178
|
+
}
|
|
17179
|
+
return issues;
|
|
17180
|
+
}
|
|
17181
|
+
function validateDefinitionSpecs(specs, options = {}) {
|
|
17182
|
+
const stepsByKey = new Map;
|
|
17183
|
+
for (const step of specs.steps) {
|
|
17184
|
+
const existing = stepsByKey.get(step.key);
|
|
17185
|
+
if (existing)
|
|
17186
|
+
existing.push(step);
|
|
17187
|
+
else
|
|
17188
|
+
stepsByKey.set(step.key, [step]);
|
|
17189
|
+
}
|
|
17190
|
+
return [
|
|
17191
|
+
...checkSignalSourcePaths(specs.steps),
|
|
17192
|
+
...checkHealthChecks(specs.steps),
|
|
17193
|
+
...checkRouteTargets(specs.pipelines, options.knownPipelineKeys ?? []),
|
|
17194
|
+
...checkSignalBindings(specs.pipelines, stepsByKey)
|
|
17195
|
+
];
|
|
17196
|
+
}
|
|
17197
|
+
function assertValidDefinitionSpecs(specs, options = {}) {
|
|
17198
|
+
const issues = validateDefinitionSpecs(specs, options);
|
|
17199
|
+
if (issues.length === 0)
|
|
17200
|
+
return;
|
|
17201
|
+
const header = issues.length === 1 ? "Definition validation failed:" : `Definition validation failed with ${String(issues.length)} problems:`;
|
|
17202
|
+
throw new Error([header, ...issues.map((issue2) => ` \u2022 ${issue2.message}`)].join(`
|
|
17203
|
+
`));
|
|
17204
|
+
}
|
|
17205
|
+
// src/push/collect-definitions.ts
|
|
16788
17206
|
import { existsSync, readdirSync } from "fs";
|
|
16789
17207
|
import { join, resolve } from "path";
|
|
16790
17208
|
import { pathToFileURL } from "url";
|
|
17209
|
+
var PUSH_SCRIPT_NAMES = new Set(["push.ts", "push.mjs", "push.js"]);
|
|
16791
17210
|
function isStepDefinitionSpec(value) {
|
|
16792
17211
|
if (typeof value !== "object" || value === null)
|
|
16793
17212
|
return false;
|
|
@@ -16800,38 +17219,21 @@ function isPipelineDefinitionSpec(value) {
|
|
|
16800
17219
|
const obj = value;
|
|
16801
17220
|
return typeof obj["key"] === "string" && typeof obj["name"] === "string" && typeof obj["version"] === "number" && Array.isArray(obj["steps"]);
|
|
16802
17221
|
}
|
|
16803
|
-
function
|
|
16804
|
-
|
|
16805
|
-
if (policy.defaultEventType === "route" && typeof policy.defaultEventParamsJson?.["pipelineKey"] === "string") {
|
|
16806
|
-
keys.push(policy.defaultEventParamsJson["pipelineKey"]);
|
|
16807
|
-
}
|
|
16808
|
-
for (const rule of policy.rulesJson.rules) {
|
|
16809
|
-
if (rule.event.type === "route" && typeof rule.event.params?.["pipelineKey"] === "string") {
|
|
16810
|
-
keys.push(rule.event.params["pipelineKey"]);
|
|
16811
|
-
}
|
|
16812
|
-
}
|
|
16813
|
-
return keys;
|
|
17222
|
+
async function importModule(path) {
|
|
17223
|
+
return await import(pathToFileURL(path).href);
|
|
16814
17224
|
}
|
|
16815
|
-
|
|
16816
|
-
async function pushFromDirectory(dir, opts) {
|
|
16817
|
-
const log = opts.log ?? ((msg) => {
|
|
16818
|
-
console.warn(msg);
|
|
16819
|
-
});
|
|
16820
|
-
const headers = { Authorization: `Bearer ${opts.accessToken}` };
|
|
17225
|
+
async function collectDefinitionsFromDirectory(dir) {
|
|
16821
17226
|
const absDir = resolve(dir);
|
|
16822
|
-
const allFiles = readdirSync(absDir).filter((
|
|
16823
|
-
const
|
|
16824
|
-
const
|
|
16825
|
-
const pipelineSpecs = [];
|
|
17227
|
+
const allFiles = readdirSync(absDir).filter((file2) => file2.endsWith(".ts") || file2.endsWith(".js"));
|
|
17228
|
+
const sourceFiles = allFiles.filter((file2) => !PUSH_SCRIPT_NAMES.has(file2) && file2 !== DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
|
|
17229
|
+
const pipelines = [];
|
|
16826
17230
|
const stepMap = new Map;
|
|
16827
17231
|
for (const file2 of sourceFiles) {
|
|
16828
|
-
const
|
|
16829
|
-
const mod = await import(pathToFileURL(absPath).href);
|
|
17232
|
+
const mod = await importModule(join(absDir, file2));
|
|
16830
17233
|
for (const [exportName, value] of Object.entries(mod)) {
|
|
16831
17234
|
if (exportName === "default") {
|
|
16832
|
-
if (isPipelineDefinitionSpec(value))
|
|
16833
|
-
|
|
16834
|
-
}
|
|
17235
|
+
if (isPipelineDefinitionSpec(value))
|
|
17236
|
+
pipelines.push(value);
|
|
16835
17237
|
continue;
|
|
16836
17238
|
}
|
|
16837
17239
|
if (isStepDefinitionSpec(value)) {
|
|
@@ -16839,38 +17241,48 @@ async function pushFromDirectory(dir, opts) {
|
|
|
16839
17241
|
}
|
|
16840
17242
|
}
|
|
16841
17243
|
}
|
|
16842
|
-
for (const spec of
|
|
17244
|
+
for (const spec of pipelines) {
|
|
16843
17245
|
for (const embedded of spec._stepDefinitions ?? []) {
|
|
16844
17246
|
const key = `${embedded.key}@v${String(embedded.version)}`;
|
|
16845
|
-
if (!stepMap.has(key))
|
|
17247
|
+
if (!stepMap.has(key))
|
|
16846
17248
|
stepMap.set(key, embedded);
|
|
16847
|
-
}
|
|
16848
17249
|
}
|
|
16849
17250
|
}
|
|
16850
|
-
|
|
17251
|
+
return {
|
|
17252
|
+
pipelines,
|
|
17253
|
+
steps: [...stepMap.values()],
|
|
17254
|
+
defaultPipelineAssignment: await collectDefaultPipelineAssignment(absDir)
|
|
17255
|
+
};
|
|
17256
|
+
}
|
|
17257
|
+
async function collectDefaultPipelineAssignment(absDir) {
|
|
17258
|
+
const path = join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
|
|
17259
|
+
if (!existsSync(path))
|
|
17260
|
+
return null;
|
|
17261
|
+
const mod = await importModule(path);
|
|
17262
|
+
const spec = mod["default"];
|
|
17263
|
+
if (!isDefaultPipelineAssignmentSpec(spec)) {
|
|
17264
|
+
throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} must have a default export produced by defaultPipelineAssignment(({ assign, skip, ... }) => ({ default: ..., rules: [...] })). Got: ${typeof spec}`);
|
|
17265
|
+
}
|
|
17266
|
+
return spec;
|
|
17267
|
+
}
|
|
17268
|
+
// src/push/push-from-directory.ts
|
|
17269
|
+
async function pushFromDirectory(dir, opts) {
|
|
17270
|
+
const log = opts.log ?? ((msg) => {
|
|
17271
|
+
console.warn(msg);
|
|
17272
|
+
});
|
|
17273
|
+
const headers = { Authorization: `Bearer ${opts.accessToken}` };
|
|
17274
|
+
const collected = await collectDefinitionsFromDirectory(dir);
|
|
17275
|
+
const { pipelines, steps } = collected;
|
|
17276
|
+
log(`Found ${String(pipelines.length)} pipeline(s) and ${String(steps.length)} step(s).`);
|
|
16851
17277
|
const stepsClient = createStepDefinitionsClient(opts.baseUrl);
|
|
16852
|
-
|
|
17278
|
+
const pipelinesClient = createPipelineDefinitionsClient(opts.baseUrl);
|
|
17279
|
+
const serverPipelineKeys = pipelines.length > 0 ? (await pipelinesClient.listByProjectId(opts.projectId, { headers })).map((pipeline2) => pipeline2.key) : [];
|
|
17280
|
+
assertValidDefinitionSpecs({ pipelines, steps }, { knownPipelineKeys: serverPipelineKeys });
|
|
17281
|
+
for (const spec of steps) {
|
|
16853
17282
|
await stepsClient.upsertFromSpec(opts.projectId, spec, { headers });
|
|
16854
17283
|
log(`\u2713 step ${spec.key} v${String(spec.version)} \u2192 upserted`);
|
|
16855
17284
|
}
|
|
16856
|
-
|
|
16857
|
-
let pushedPipelinesCount = 0;
|
|
16858
|
-
if (pipelineSpecs.length > 0) {
|
|
16859
|
-
const existingPipelines = await pipelinesClient.listByProjectId(opts.projectId, { headers });
|
|
16860
|
-
const knownPipelineKeys = new Set([
|
|
16861
|
-
...pipelineSpecs.map((s) => s.key),
|
|
16862
|
-
...existingPipelines.map((p) => p.key)
|
|
16863
|
-
]);
|
|
16864
|
-
for (const spec of pipelineSpecs) {
|
|
16865
|
-
for (const step of spec.steps) {
|
|
16866
|
-
const routeKeys = extractRoutePipelineKeys(step.advancementPolicyDefinition);
|
|
16867
|
-
for (const routeKey of routeKeys) {
|
|
16868
|
-
if (!knownPipelineKeys.has(routeKey)) {
|
|
16869
|
-
throw new Error(`Pipeline "${spec.key}" step "${step.stepKey}" routes to pipeline "${routeKey}", but no pipeline with that key was found on the server or in the current push batch. Push the target pipeline first.`);
|
|
16870
|
-
}
|
|
16871
|
-
}
|
|
16872
|
-
}
|
|
16873
|
-
}
|
|
17285
|
+
if (pipelines.length > 0) {
|
|
16874
17286
|
const serverSteps = await stepsClient.listByProjectId(opts.projectId, {
|
|
16875
17287
|
headers
|
|
16876
17288
|
});
|
|
@@ -16879,27 +17291,17 @@ async function pushFromDirectory(dir, opts) {
|
|
|
16879
17291
|
key: s.key,
|
|
16880
17292
|
version: s.version
|
|
16881
17293
|
}));
|
|
16882
|
-
for (const spec of
|
|
17294
|
+
for (const spec of pipelines) {
|
|
16883
17295
|
await pipelinesClient.upsertFromSpec(opts.projectId, spec, stepDefs, {
|
|
16884
17296
|
headers
|
|
16885
17297
|
});
|
|
16886
17298
|
log(`\u2713 pipeline ${spec.key} v${String(spec.version)} \u2192 upserted`);
|
|
16887
17299
|
}
|
|
16888
|
-
pushedPipelinesCount = pipelineSpecs.length;
|
|
16889
|
-
}
|
|
16890
|
-
let syncedDefaultPipelineAssignment = false;
|
|
16891
|
-
if (hasAssignmentFile) {
|
|
16892
|
-
const assignmentFilePath = join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
|
|
16893
|
-
const assignmentMod = await import(pathToFileURL(assignmentFilePath).href);
|
|
16894
|
-
const assignmentSpec = assignmentMod["default"];
|
|
16895
|
-
if (!isDefaultPipelineAssignmentSpec(assignmentSpec)) {
|
|
16896
|
-
throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} must have a default export produced by defaultPipelineAssignment(({ assign, skip, ... }) => ({ default: ..., rules: [...] })). Got: ${typeof assignmentSpec}`);
|
|
16897
|
-
}
|
|
16898
|
-
syncedDefaultPipelineAssignment = await syncDefaultPipelineAssignment(assignmentSpec, opts, headers, pipelinesClient, log);
|
|
16899
17300
|
}
|
|
17301
|
+
const syncedDefaultPipelineAssignment = collected.defaultPipelineAssignment ? await syncDefaultPipelineAssignment(collected.defaultPipelineAssignment, opts, headers, pipelinesClient, log) : false;
|
|
16900
17302
|
return {
|
|
16901
|
-
pushedSteps:
|
|
16902
|
-
pushedPipelines:
|
|
17303
|
+
pushedSteps: steps.length,
|
|
17304
|
+
pushedPipelines: pipelines.length,
|
|
16903
17305
|
syncedDefaultPipelineAssignment
|
|
16904
17306
|
};
|
|
16905
17307
|
}
|
|
@@ -16921,7 +17323,7 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
|
|
|
16921
17323
|
}
|
|
16922
17324
|
for (const key of referencedKeys) {
|
|
16923
17325
|
if (!pipelineKeyToId.has(key)) {
|
|
16924
|
-
throw new Error(
|
|
17326
|
+
throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} references pipeline "${key}", ` + `but no pipeline with that key was found on the server. ` + `Push the pipeline first with \`boboddy pipelines push\`.`);
|
|
16925
17327
|
}
|
|
16926
17328
|
}
|
|
16927
17329
|
const linearPipelineDefinitionId = pipelineKeyToId.get(serialized.linearPipelineDefinitionKey);
|
|
@@ -16933,7 +17335,7 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
|
|
|
16933
17335
|
const pKey = rule.event.params["pipelineKey"];
|
|
16934
17336
|
const pId = pipelineKeyToId.get(pKey);
|
|
16935
17337
|
if (!pId) {
|
|
16936
|
-
throw new Error(
|
|
17338
|
+
throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} assign() references pipeline "${pKey}" ` + `which was not found on the server.`);
|
|
16937
17339
|
}
|
|
16938
17340
|
return {
|
|
16939
17341
|
...rule,
|
|
@@ -16971,5 +17373,6 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
|
|
|
16971
17373
|
return true;
|
|
16972
17374
|
}
|
|
16973
17375
|
export {
|
|
16974
|
-
pushFromDirectory
|
|
17376
|
+
pushFromDirectory,
|
|
17377
|
+
collectDefinitionsFromDirectory
|
|
16975
17378
|
};
|