@boboddy/sdk 0.2.9-alpha → 0.2.12-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/client.js +11 -8
- package/dist/definitions/pipelines/index.js +11 -8
- package/dist/definitions/steps/define-step.d.ts +1 -1
- package/dist/definitions/steps/index.js +11 -8
- package/dist/definitions/validation/index.d.ts +2 -0
- package/dist/definitions/validation/index.js +409 -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/index.d.ts +1 -1
- package/dist/generated/sdk.gen.d.ts +5 -4
- package/dist/generated/types.gen.d.ts +370 -355
- package/dist/index.js +11 -8
- package/dist/push/collect-definitions.d.ts +21 -0
- package/dist/push/index.d.ts +2 -0
- package/dist/push/index.js +449 -75
- package/dist/push/push-from-directory.d.ts +7 -1
- package/package.json +5 -1
package/dist/push/index.js
CHANGED
|
@@ -12951,12 +12951,12 @@ class Projects extends HeyApiClient {
|
|
|
12951
12951
|
listProjectWorkItems(options) {
|
|
12952
12952
|
return (options.client ?? this.client).get({ url: "/api/projects/{projectId}/work-items", ...options });
|
|
12953
12953
|
}
|
|
12954
|
+
listProjectWorkItemFieldOptions(options) {
|
|
12955
|
+
return (options.client ?? this.client).get({ url: "/api/projects/{projectId}/work-item-field-options", ...options });
|
|
12956
|
+
}
|
|
12954
12957
|
getProject(options) {
|
|
12955
12958
|
return (options.client ?? this.client).get({ url: "/api/projects/{projectId}", ...options });
|
|
12956
12959
|
}
|
|
12957
|
-
getProjectWorkItemSignalScores(options) {
|
|
12958
|
-
return (options.client ?? this.client).get({ url: "/api/projects/{projectId}/work-item-signal-scores", ...options });
|
|
12959
|
-
}
|
|
12960
12960
|
updateProjectDefaultPipelineAssignment(options) {
|
|
12961
12961
|
return (options.client ?? this.client).put({
|
|
12962
12962
|
url: "/api/projects/{projectId}/default-pipeline-assignment",
|
|
@@ -13248,8 +13248,14 @@ class PipelineExecutions extends HeyApiClient {
|
|
|
13248
13248
|
getPipelineExecution(options) {
|
|
13249
13249
|
return (options.client ?? this.client).get({ url: "/api/linear-pipeline-executions/{linearPipelineExecutionId}", ...options });
|
|
13250
13250
|
}
|
|
13251
|
-
|
|
13252
|
-
return (options.client ?? this.client).get({ url: "/api/linear-pipeline-executions/by-
|
|
13251
|
+
countPipelineExecutionsByPipeline(options) {
|
|
13252
|
+
return (options.client ?? this.client).get({ url: "/api/linear-pipeline-executions/counts/by-pipeline", ...options });
|
|
13253
|
+
}
|
|
13254
|
+
listPipelineDefinitionStepRollups(options) {
|
|
13255
|
+
return (options.client ?? this.client).get({ url: "/api/linear-pipeline-executions/step-rollups/by-definition/{linearPipelineDefinitionId}", ...options });
|
|
13256
|
+
}
|
|
13257
|
+
listPipelineDefinitionStepRuns(options) {
|
|
13258
|
+
return (options.client ?? this.client).get({ url: "/api/linear-pipeline-executions/step-runs/by-definition/{linearPipelineDefinitionId}", ...options });
|
|
13253
13259
|
}
|
|
13254
13260
|
}
|
|
13255
13261
|
|
|
@@ -13284,9 +13290,6 @@ class WorkItems extends HeyApiClient {
|
|
|
13284
13290
|
}
|
|
13285
13291
|
});
|
|
13286
13292
|
}
|
|
13287
|
-
getWorkItems(options) {
|
|
13288
|
-
return (options.client ?? this.client).get({ url: "/api/work-items/batch", ...options });
|
|
13289
|
-
}
|
|
13290
13293
|
createWorkItems(options) {
|
|
13291
13294
|
return (options.client ?? this.client).post({
|
|
13292
13295
|
url: "/api/work-items/batch",
|
|
@@ -16781,10 +16784,397 @@ function isDefaultPipelineAssignmentSpec(value) {
|
|
|
16781
16784
|
return false;
|
|
16782
16785
|
return value["_tag"] === "default_pipeline_assignment";
|
|
16783
16786
|
}
|
|
16784
|
-
// src/
|
|
16787
|
+
// src/definitions/validation/json-schema-paths.ts
|
|
16788
|
+
var SEGMENT_PATTERN = /([^.[\]]+)|(\[(\d+)\])/g;
|
|
16789
|
+
var NUMERIC_SEGMENT = /^\d+$/;
|
|
16790
|
+
var SCALAR_TYPES = new Set([
|
|
16791
|
+
"string",
|
|
16792
|
+
"number",
|
|
16793
|
+
"integer",
|
|
16794
|
+
"boolean",
|
|
16795
|
+
"null"
|
|
16796
|
+
]);
|
|
16797
|
+
var MAX_REF_HOPS = 16;
|
|
16798
|
+
var MAX_CANDIDATES = 64;
|
|
16799
|
+
function parseSourcePath(sourcePath) {
|
|
16800
|
+
const trimmed = sourcePath.trim();
|
|
16801
|
+
const normalized = trimmed === "$" ? "" : trimmed.startsWith("$.") ? trimmed.slice(2) : trimmed;
|
|
16802
|
+
if (!normalized)
|
|
16803
|
+
return [];
|
|
16804
|
+
return [...normalized.matchAll(SEGMENT_PATTERN)].map((match) => match[1] ?? match[3]).filter((segment) => Boolean(segment));
|
|
16805
|
+
}
|
|
16806
|
+
function isSchemaNode(value) {
|
|
16807
|
+
return typeof value === "boolean" || typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16808
|
+
}
|
|
16809
|
+
function asRecord(node) {
|
|
16810
|
+
return typeof node === "boolean" ? null : node;
|
|
16811
|
+
}
|
|
16812
|
+
function resolveRef2(root, ref) {
|
|
16813
|
+
if (!ref.startsWith("#"))
|
|
16814
|
+
return null;
|
|
16815
|
+
const pointer = ref.slice(1);
|
|
16816
|
+
if (pointer === "" || pointer === "/")
|
|
16817
|
+
return root;
|
|
16818
|
+
if (!pointer.startsWith("/"))
|
|
16819
|
+
return null;
|
|
16820
|
+
let current = root;
|
|
16821
|
+
for (const rawToken of pointer.slice(1).split("/")) {
|
|
16822
|
+
const token = rawToken.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
16823
|
+
if (typeof current !== "object" || current === null)
|
|
16824
|
+
return null;
|
|
16825
|
+
current = current[token];
|
|
16826
|
+
}
|
|
16827
|
+
return isSchemaNode(current) ? current : null;
|
|
16828
|
+
}
|
|
16829
|
+
function flatten(node, root) {
|
|
16830
|
+
const out = [];
|
|
16831
|
+
const queue = [
|
|
16832
|
+
{ node, hops: 0 }
|
|
16833
|
+
];
|
|
16834
|
+
while (queue.length > 0) {
|
|
16835
|
+
const entry = queue.shift();
|
|
16836
|
+
if (!entry)
|
|
16837
|
+
break;
|
|
16838
|
+
if (out.length >= MAX_CANDIDATES)
|
|
16839
|
+
return null;
|
|
16840
|
+
if (entry.hops > MAX_REF_HOPS)
|
|
16841
|
+
return null;
|
|
16842
|
+
const record2 = asRecord(entry.node);
|
|
16843
|
+
if (!record2) {
|
|
16844
|
+
out.push(entry.node);
|
|
16845
|
+
continue;
|
|
16846
|
+
}
|
|
16847
|
+
const ref = record2["$ref"];
|
|
16848
|
+
if (typeof ref === "string") {
|
|
16849
|
+
const target = resolveRef2(root, ref);
|
|
16850
|
+
if (!target)
|
|
16851
|
+
return null;
|
|
16852
|
+
queue.push({ node: target, hops: entry.hops + 1 });
|
|
16853
|
+
continue;
|
|
16854
|
+
}
|
|
16855
|
+
const branches = ["anyOf", "oneOf", "allOf"].flatMap((keyword) => {
|
|
16856
|
+
const value = record2[keyword];
|
|
16857
|
+
return Array.isArray(value) ? value.filter(isSchemaNode) : [];
|
|
16858
|
+
});
|
|
16859
|
+
if (branches.length > 0) {
|
|
16860
|
+
for (const branch of branches) {
|
|
16861
|
+
queue.push({ node: branch, hops: entry.hops + 1 });
|
|
16862
|
+
}
|
|
16863
|
+
continue;
|
|
16864
|
+
}
|
|
16865
|
+
out.push(entry.node);
|
|
16866
|
+
}
|
|
16867
|
+
return out;
|
|
16868
|
+
}
|
|
16869
|
+
function typeNames(record2) {
|
|
16870
|
+
const raw = record2["type"];
|
|
16871
|
+
if (typeof raw === "string")
|
|
16872
|
+
return new Set([raw]);
|
|
16873
|
+
if (Array.isArray(raw)) {
|
|
16874
|
+
return new Set(raw.filter((entry) => typeof entry === "string"));
|
|
16875
|
+
}
|
|
16876
|
+
return new Set;
|
|
16877
|
+
}
|
|
16878
|
+
var INDETERMINATE = { kind: "indeterminate" };
|
|
16879
|
+
function stepIntoObject(record2, segment) {
|
|
16880
|
+
const properties = asRecord(isSchemaNode(record2["properties"]) ? record2["properties"] : {});
|
|
16881
|
+
const declared = properties ?? {};
|
|
16882
|
+
const child = declared[segment];
|
|
16883
|
+
if (isSchemaNode(child))
|
|
16884
|
+
return { kind: "child", node: child };
|
|
16885
|
+
if (record2["patternProperties"] !== undefined)
|
|
16886
|
+
return INDETERMINATE;
|
|
16887
|
+
if (record2["additionalProperties"] !== false)
|
|
16888
|
+
return INDETERMINATE;
|
|
16889
|
+
return {
|
|
16890
|
+
kind: "invalid",
|
|
16891
|
+
reason: "unknown-property",
|
|
16892
|
+
availablePaths: Object.keys(declared).sort()
|
|
16893
|
+
};
|
|
16894
|
+
}
|
|
16895
|
+
function stepIntoArray(record2, segment) {
|
|
16896
|
+
if (!NUMERIC_SEGMENT.test(segment)) {
|
|
16897
|
+
return {
|
|
16898
|
+
kind: "invalid",
|
|
16899
|
+
reason: "not-an-array-index",
|
|
16900
|
+
availablePaths: []
|
|
16901
|
+
};
|
|
16902
|
+
}
|
|
16903
|
+
const prefixItems = record2["prefixItems"];
|
|
16904
|
+
if (Array.isArray(prefixItems)) {
|
|
16905
|
+
const positional = prefixItems[Number(segment)];
|
|
16906
|
+
if (isSchemaNode(positional))
|
|
16907
|
+
return { kind: "child", node: positional };
|
|
16908
|
+
}
|
|
16909
|
+
const items = record2["items"];
|
|
16910
|
+
if (isSchemaNode(items))
|
|
16911
|
+
return { kind: "child", node: items };
|
|
16912
|
+
return INDETERMINATE;
|
|
16913
|
+
}
|
|
16914
|
+
function stepInto(node, segment) {
|
|
16915
|
+
const record2 = asRecord(node);
|
|
16916
|
+
if (!record2 || Object.keys(record2).length === 0)
|
|
16917
|
+
return INDETERMINATE;
|
|
16918
|
+
const types = typeNames(record2);
|
|
16919
|
+
const objectLike = types.has("object") || record2["properties"] !== undefined || record2["patternProperties"] !== undefined || record2["additionalProperties"] !== undefined;
|
|
16920
|
+
const arrayLike = types.has("array") || record2["items"] !== undefined || record2["prefixItems"] !== undefined;
|
|
16921
|
+
const outcomes = [];
|
|
16922
|
+
if (objectLike)
|
|
16923
|
+
outcomes.push(stepIntoObject(record2, segment));
|
|
16924
|
+
if (arrayLike)
|
|
16925
|
+
outcomes.push(stepIntoArray(record2, segment));
|
|
16926
|
+
if (outcomes.length === 0) {
|
|
16927
|
+
if (types.size > 0 && [...types].every((name) => SCALAR_TYPES.has(name))) {
|
|
16928
|
+
return {
|
|
16929
|
+
kind: "invalid",
|
|
16930
|
+
reason: "scalar-has-no-members",
|
|
16931
|
+
availablePaths: []
|
|
16932
|
+
};
|
|
16933
|
+
}
|
|
16934
|
+
return INDETERMINATE;
|
|
16935
|
+
}
|
|
16936
|
+
return combine(outcomes);
|
|
16937
|
+
}
|
|
16938
|
+
function combine(outcomes) {
|
|
16939
|
+
const children = outcomes.filter((outcome) => outcome.kind === "child");
|
|
16940
|
+
if (children.length > 0)
|
|
16941
|
+
return children[0] ?? INDETERMINATE;
|
|
16942
|
+
if (outcomes.some((outcome) => outcome.kind === "indeterminate")) {
|
|
16943
|
+
return INDETERMINATE;
|
|
16944
|
+
}
|
|
16945
|
+
const invalid = outcomes.filter((outcome) => outcome.kind === "invalid");
|
|
16946
|
+
const first = invalid[0];
|
|
16947
|
+
if (!first)
|
|
16948
|
+
return INDETERMINATE;
|
|
16949
|
+
return {
|
|
16950
|
+
kind: "invalid",
|
|
16951
|
+
reason: first.reason,
|
|
16952
|
+
availablePaths: [
|
|
16953
|
+
...new Set(invalid.flatMap((outcome) => outcome.availablePaths))
|
|
16954
|
+
].sort()
|
|
16955
|
+
};
|
|
16956
|
+
}
|
|
16957
|
+
function enumeratePaths(node, root, maxDepth = 3, limit = 40) {
|
|
16958
|
+
const out = [];
|
|
16959
|
+
const visit = (current, prefix, depth) => {
|
|
16960
|
+
if (out.length >= limit || depth > maxDepth)
|
|
16961
|
+
return;
|
|
16962
|
+
for (const branch of flatten(current, root) ?? []) {
|
|
16963
|
+
const record2 = asRecord(branch);
|
|
16964
|
+
const properties = record2 ? asRecord(isSchemaNode(record2["properties"]) ? record2["properties"] : {}) : null;
|
|
16965
|
+
if (!properties)
|
|
16966
|
+
continue;
|
|
16967
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
16968
|
+
if (out.length >= limit)
|
|
16969
|
+
return;
|
|
16970
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
16971
|
+
out.push(path);
|
|
16972
|
+
if (isSchemaNode(child))
|
|
16973
|
+
visit(child, path, depth + 1);
|
|
16974
|
+
}
|
|
16975
|
+
}
|
|
16976
|
+
};
|
|
16977
|
+
visit(node, "", 1);
|
|
16978
|
+
return [...new Set(out)].sort();
|
|
16979
|
+
}
|
|
16980
|
+
function resolveSourcePath(schema, sourcePath) {
|
|
16981
|
+
const segments = parseSourcePath(sourcePath);
|
|
16982
|
+
if (segments.length === 0)
|
|
16983
|
+
return { kind: "resolved" };
|
|
16984
|
+
let candidates = [schema];
|
|
16985
|
+
let resolvedPrefix = "";
|
|
16986
|
+
for (const segment of segments) {
|
|
16987
|
+
const expanded = candidates.flatMap((node) => flatten(node, schema) ?? []);
|
|
16988
|
+
if (expanded.length === 0)
|
|
16989
|
+
return { kind: "indeterminate" };
|
|
16990
|
+
const outcome = combine(expanded.map((node) => stepInto(node, segment)));
|
|
16991
|
+
if (outcome.kind === "indeterminate")
|
|
16992
|
+
return { kind: "indeterminate" };
|
|
16993
|
+
if (outcome.kind === "invalid") {
|
|
16994
|
+
const availablePaths = [
|
|
16995
|
+
...new Set(expanded.flatMap((node) => enumeratePaths(node, schema)))
|
|
16996
|
+
].sort();
|
|
16997
|
+
return {
|
|
16998
|
+
kind: "invalid",
|
|
16999
|
+
resolvedPrefix,
|
|
17000
|
+
segment,
|
|
17001
|
+
reason: outcome.reason,
|
|
17002
|
+
availablePaths
|
|
17003
|
+
};
|
|
17004
|
+
}
|
|
17005
|
+
candidates = [outcome.node];
|
|
17006
|
+
resolvedPrefix = resolvedPrefix ? `${resolvedPrefix}.${segment}` : segment;
|
|
17007
|
+
}
|
|
17008
|
+
return { kind: "resolved" };
|
|
17009
|
+
}
|
|
17010
|
+
// src/definitions/validation/validate-definition-specs.ts
|
|
17011
|
+
function listPaths(paths, limit = 24) {
|
|
17012
|
+
if (paths.length === 0)
|
|
17013
|
+
return "";
|
|
17014
|
+
if (paths.length <= limit)
|
|
17015
|
+
return paths.join(", ");
|
|
17016
|
+
return `${paths.slice(0, limit).join(", ")}, \u2026 (${String(paths.length - limit)} more)`;
|
|
17017
|
+
}
|
|
17018
|
+
function quotedOrRoot(prefix) {
|
|
17019
|
+
return prefix ? `"${prefix}"` : "the result root";
|
|
17020
|
+
}
|
|
17021
|
+
function checkSignalSourcePaths(steps) {
|
|
17022
|
+
const issues = [];
|
|
17023
|
+
for (const step of steps) {
|
|
17024
|
+
const schema = step.resultSchemaJson ?? null;
|
|
17025
|
+
if (!schema)
|
|
17026
|
+
continue;
|
|
17027
|
+
for (const signal2 of step.signalExtractorDefinitions) {
|
|
17028
|
+
const resolution = resolveSourcePath(schema, signal2.sourcePath);
|
|
17029
|
+
if (resolution.kind !== "invalid")
|
|
17030
|
+
continue;
|
|
17031
|
+
const { resolvedPrefix, segment, reason, availablePaths } = resolution;
|
|
17032
|
+
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}"`;
|
|
17033
|
+
const suffix = availablePaths.length > 0 ? ` Valid sourcePaths ${resolvedPrefix ? `under "${resolvedPrefix}"` : "for this step"}: ${listPaths(availablePaths)}.` : "";
|
|
17034
|
+
issues.push({
|
|
17035
|
+
check: "signal-source-path",
|
|
17036
|
+
message: `Step "${step.key}" declares signal "${signal2.key}" with sourcePath ` + `"${signal2.sourcePath}", which can never resolve against the step's ` + `result schema: ${cause}.${suffix}`
|
|
17037
|
+
});
|
|
17038
|
+
}
|
|
17039
|
+
}
|
|
17040
|
+
return issues;
|
|
17041
|
+
}
|
|
17042
|
+
function routeTargets(policy) {
|
|
17043
|
+
const keys = [];
|
|
17044
|
+
if (policy.defaultEventType === "route" && typeof policy.defaultEventParamsJson?.["pipelineKey"] === "string") {
|
|
17045
|
+
keys.push(policy.defaultEventParamsJson["pipelineKey"]);
|
|
17046
|
+
}
|
|
17047
|
+
for (const rule of policy.rulesJson.rules) {
|
|
17048
|
+
if (rule.event.type === "route" && typeof rule.event.params?.["pipelineKey"] === "string") {
|
|
17049
|
+
keys.push(rule.event.params["pipelineKey"]);
|
|
17050
|
+
}
|
|
17051
|
+
}
|
|
17052
|
+
return keys;
|
|
17053
|
+
}
|
|
17054
|
+
function checkRouteTargets(pipelines, knownPipelineKeys) {
|
|
17055
|
+
const issues = [];
|
|
17056
|
+
const known = new Set([
|
|
17057
|
+
...pipelines.map((pipeline2) => pipeline2.key),
|
|
17058
|
+
...knownPipelineKeys
|
|
17059
|
+
]);
|
|
17060
|
+
for (const pipeline2 of pipelines) {
|
|
17061
|
+
for (const step of pipeline2.steps) {
|
|
17062
|
+
for (const target of routeTargets(step.advancementPolicyDefinition)) {
|
|
17063
|
+
if (known.has(target))
|
|
17064
|
+
continue;
|
|
17065
|
+
issues.push({
|
|
17066
|
+
check: "route-target",
|
|
17067
|
+
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.`
|
|
17068
|
+
});
|
|
17069
|
+
}
|
|
17070
|
+
}
|
|
17071
|
+
}
|
|
17072
|
+
return issues;
|
|
17073
|
+
}
|
|
17074
|
+
function executionRanks(steps) {
|
|
17075
|
+
const positions = steps.map((step) => step.position);
|
|
17076
|
+
const usable = positions.every((value) => Number.isInteger(value) && value > 0) && new Set(positions).size === positions.length;
|
|
17077
|
+
const indexes = steps.map((_, index) => index);
|
|
17078
|
+
const ordered = usable ? [...indexes].sort((left, right) => (positions[left] ?? 0) - (positions[right] ?? 0)) : indexes;
|
|
17079
|
+
return new Map(ordered.map((index, rank) => [index, rank]));
|
|
17080
|
+
}
|
|
17081
|
+
function bindingSource(binding) {
|
|
17082
|
+
if (binding.source === "step_signal") {
|
|
17083
|
+
return { stepKey: binding.stepKey, signalKey: binding.signalKey };
|
|
17084
|
+
}
|
|
17085
|
+
if (binding.source === "step_output") {
|
|
17086
|
+
return { stepKey: binding.stepKey, signalKey: null };
|
|
17087
|
+
}
|
|
17088
|
+
return null;
|
|
17089
|
+
}
|
|
17090
|
+
function declaredSignalKeys(stepKey, stepsByKey, pipelineSteps) {
|
|
17091
|
+
const specs = stepsByKey.get(stepKey);
|
|
17092
|
+
if (!specs || specs.length === 0)
|
|
17093
|
+
return null;
|
|
17094
|
+
const keys = new Set;
|
|
17095
|
+
for (const spec of specs) {
|
|
17096
|
+
for (const signal2 of spec.signalExtractorDefinitions)
|
|
17097
|
+
keys.add(signal2.key);
|
|
17098
|
+
}
|
|
17099
|
+
for (const step of pipelineSteps) {
|
|
17100
|
+
if (step.stepKey !== stepKey)
|
|
17101
|
+
continue;
|
|
17102
|
+
for (const computed of step.computedSignalDefinitions)
|
|
17103
|
+
keys.add(computed.key);
|
|
17104
|
+
}
|
|
17105
|
+
return [...keys];
|
|
17106
|
+
}
|
|
17107
|
+
function checkSignalBindings(pipelines, stepsByKey) {
|
|
17108
|
+
const issues = [];
|
|
17109
|
+
for (const pipeline2 of pipelines) {
|
|
17110
|
+
const ranks = executionRanks(pipeline2.steps);
|
|
17111
|
+
const order = [...pipeline2.steps.keys()].sort((left, right) => (ranks.get(left) ?? 0) - (ranks.get(right) ?? 0)).map((index) => pipeline2.steps[index]?.stepKey ?? "");
|
|
17112
|
+
const orderHint = `Steps in "${pipeline2.key}", in order: ${order.join(" \u2192 ")}.`;
|
|
17113
|
+
pipeline2.steps.forEach((step, index) => {
|
|
17114
|
+
const consumerRank = ranks.get(index) ?? index;
|
|
17115
|
+
const where = `Pipeline "${pipeline2.key}" step "${step.stepKey}"`;
|
|
17116
|
+
for (const [field, binding] of Object.entries(step.inputBindingsJson)) {
|
|
17117
|
+
const source = bindingSource(binding);
|
|
17118
|
+
if (!source)
|
|
17119
|
+
continue;
|
|
17120
|
+
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}"`;
|
|
17121
|
+
const producerRanks = pipeline2.steps.map((candidate, candidateIndex) => candidate.stepKey === source.stepKey ? ranks.get(candidateIndex) ?? candidateIndex : null).filter((rank) => rank !== null);
|
|
17122
|
+
if (producerRanks.length === 0) {
|
|
17123
|
+
issues.push({
|
|
17124
|
+
check: "signal-binding",
|
|
17125
|
+
message: `${where} ${what}, but no step with that key is in the pipeline. ${orderHint}`
|
|
17126
|
+
});
|
|
17127
|
+
continue;
|
|
17128
|
+
}
|
|
17129
|
+
if (!producerRanks.some((rank) => rank < consumerRank)) {
|
|
17130
|
+
issues.push({
|
|
17131
|
+
check: "signal-binding",
|
|
17132
|
+
message: `${where} ${what}, but that step does not run before it, so the ` + `value will never exist. ${orderHint}`
|
|
17133
|
+
});
|
|
17134
|
+
continue;
|
|
17135
|
+
}
|
|
17136
|
+
if (source.signalKey === null)
|
|
17137
|
+
continue;
|
|
17138
|
+
const available = declaredSignalKeys(source.stepKey, stepsByKey, pipeline2.steps);
|
|
17139
|
+
if (available === null || available.includes(source.signalKey))
|
|
17140
|
+
continue;
|
|
17141
|
+
issues.push({
|
|
17142
|
+
check: "signal-binding",
|
|
17143
|
+
message: `${where} ${what}, but "${source.stepKey}" declares no such signal. ` + `Signals on "${source.stepKey}": ${available.length > 0 ? listPaths([...available].sort()) : "(none)"}.`
|
|
17144
|
+
});
|
|
17145
|
+
}
|
|
17146
|
+
});
|
|
17147
|
+
}
|
|
17148
|
+
return issues;
|
|
17149
|
+
}
|
|
17150
|
+
function validateDefinitionSpecs(specs, options = {}) {
|
|
17151
|
+
const stepsByKey = new Map;
|
|
17152
|
+
for (const step of specs.steps) {
|
|
17153
|
+
const existing = stepsByKey.get(step.key);
|
|
17154
|
+
if (existing)
|
|
17155
|
+
existing.push(step);
|
|
17156
|
+
else
|
|
17157
|
+
stepsByKey.set(step.key, [step]);
|
|
17158
|
+
}
|
|
17159
|
+
return [
|
|
17160
|
+
...checkSignalSourcePaths(specs.steps),
|
|
17161
|
+
...checkRouteTargets(specs.pipelines, options.knownPipelineKeys ?? []),
|
|
17162
|
+
...checkSignalBindings(specs.pipelines, stepsByKey)
|
|
17163
|
+
];
|
|
17164
|
+
}
|
|
17165
|
+
function assertValidDefinitionSpecs(specs, options = {}) {
|
|
17166
|
+
const issues = validateDefinitionSpecs(specs, options);
|
|
17167
|
+
if (issues.length === 0)
|
|
17168
|
+
return;
|
|
17169
|
+
const header = issues.length === 1 ? "Definition validation failed:" : `Definition validation failed with ${String(issues.length)} problems:`;
|
|
17170
|
+
throw new Error([header, ...issues.map((issue2) => ` \u2022 ${issue2.message}`)].join(`
|
|
17171
|
+
`));
|
|
17172
|
+
}
|
|
17173
|
+
// src/push/collect-definitions.ts
|
|
16785
17174
|
import { existsSync, readdirSync } from "fs";
|
|
16786
17175
|
import { join, resolve } from "path";
|
|
16787
17176
|
import { pathToFileURL } from "url";
|
|
17177
|
+
var PUSH_SCRIPT_NAMES = new Set(["push.ts", "push.mjs", "push.js"]);
|
|
16788
17178
|
function isStepDefinitionSpec(value) {
|
|
16789
17179
|
if (typeof value !== "object" || value === null)
|
|
16790
17180
|
return false;
|
|
@@ -16797,38 +17187,21 @@ function isPipelineDefinitionSpec(value) {
|
|
|
16797
17187
|
const obj = value;
|
|
16798
17188
|
return typeof obj["key"] === "string" && typeof obj["name"] === "string" && typeof obj["version"] === "number" && Array.isArray(obj["steps"]);
|
|
16799
17189
|
}
|
|
16800
|
-
function
|
|
16801
|
-
|
|
16802
|
-
if (policy.defaultEventType === "route" && typeof policy.defaultEventParamsJson?.["pipelineKey"] === "string") {
|
|
16803
|
-
keys.push(policy.defaultEventParamsJson["pipelineKey"]);
|
|
16804
|
-
}
|
|
16805
|
-
for (const rule of policy.rulesJson.rules) {
|
|
16806
|
-
if (rule.event.type === "route" && typeof rule.event.params?.["pipelineKey"] === "string") {
|
|
16807
|
-
keys.push(rule.event.params["pipelineKey"]);
|
|
16808
|
-
}
|
|
16809
|
-
}
|
|
16810
|
-
return keys;
|
|
17190
|
+
async function importModule(path) {
|
|
17191
|
+
return await import(pathToFileURL(path).href);
|
|
16811
17192
|
}
|
|
16812
|
-
|
|
16813
|
-
async function pushFromDirectory(dir, opts) {
|
|
16814
|
-
const log = opts.log ?? ((msg) => {
|
|
16815
|
-
console.warn(msg);
|
|
16816
|
-
});
|
|
16817
|
-
const headers = { Authorization: `Bearer ${opts.accessToken}` };
|
|
17193
|
+
async function collectDefinitionsFromDirectory(dir) {
|
|
16818
17194
|
const absDir = resolve(dir);
|
|
16819
|
-
const allFiles = readdirSync(absDir).filter((
|
|
16820
|
-
const
|
|
16821
|
-
const
|
|
16822
|
-
const pipelineSpecs = [];
|
|
17195
|
+
const allFiles = readdirSync(absDir).filter((file2) => file2.endsWith(".ts") || file2.endsWith(".js"));
|
|
17196
|
+
const sourceFiles = allFiles.filter((file2) => !PUSH_SCRIPT_NAMES.has(file2) && file2 !== DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
|
|
17197
|
+
const pipelines = [];
|
|
16823
17198
|
const stepMap = new Map;
|
|
16824
17199
|
for (const file2 of sourceFiles) {
|
|
16825
|
-
const
|
|
16826
|
-
const mod = await import(pathToFileURL(absPath).href);
|
|
17200
|
+
const mod = await importModule(join(absDir, file2));
|
|
16827
17201
|
for (const [exportName, value] of Object.entries(mod)) {
|
|
16828
17202
|
if (exportName === "default") {
|
|
16829
|
-
if (isPipelineDefinitionSpec(value))
|
|
16830
|
-
|
|
16831
|
-
}
|
|
17203
|
+
if (isPipelineDefinitionSpec(value))
|
|
17204
|
+
pipelines.push(value);
|
|
16832
17205
|
continue;
|
|
16833
17206
|
}
|
|
16834
17207
|
if (isStepDefinitionSpec(value)) {
|
|
@@ -16836,38 +17209,48 @@ async function pushFromDirectory(dir, opts) {
|
|
|
16836
17209
|
}
|
|
16837
17210
|
}
|
|
16838
17211
|
}
|
|
16839
|
-
for (const spec of
|
|
17212
|
+
for (const spec of pipelines) {
|
|
16840
17213
|
for (const embedded of spec._stepDefinitions ?? []) {
|
|
16841
17214
|
const key = `${embedded.key}@v${String(embedded.version)}`;
|
|
16842
|
-
if (!stepMap.has(key))
|
|
17215
|
+
if (!stepMap.has(key))
|
|
16843
17216
|
stepMap.set(key, embedded);
|
|
16844
|
-
}
|
|
16845
17217
|
}
|
|
16846
17218
|
}
|
|
16847
|
-
|
|
17219
|
+
return {
|
|
17220
|
+
pipelines,
|
|
17221
|
+
steps: [...stepMap.values()],
|
|
17222
|
+
defaultPipelineAssignment: await collectDefaultPipelineAssignment(absDir)
|
|
17223
|
+
};
|
|
17224
|
+
}
|
|
17225
|
+
async function collectDefaultPipelineAssignment(absDir) {
|
|
17226
|
+
const path = join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
|
|
17227
|
+
if (!existsSync(path))
|
|
17228
|
+
return null;
|
|
17229
|
+
const mod = await importModule(path);
|
|
17230
|
+
const spec = mod["default"];
|
|
17231
|
+
if (!isDefaultPipelineAssignmentSpec(spec)) {
|
|
17232
|
+
throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} must have a default export produced by defaultPipelineAssignment(({ assign, skip, ... }) => ({ default: ..., rules: [...] })). Got: ${typeof spec}`);
|
|
17233
|
+
}
|
|
17234
|
+
return spec;
|
|
17235
|
+
}
|
|
17236
|
+
// src/push/push-from-directory.ts
|
|
17237
|
+
async function pushFromDirectory(dir, opts) {
|
|
17238
|
+
const log = opts.log ?? ((msg) => {
|
|
17239
|
+
console.warn(msg);
|
|
17240
|
+
});
|
|
17241
|
+
const headers = { Authorization: `Bearer ${opts.accessToken}` };
|
|
17242
|
+
const collected = await collectDefinitionsFromDirectory(dir);
|
|
17243
|
+
const { pipelines, steps } = collected;
|
|
17244
|
+
log(`Found ${String(pipelines.length)} pipeline(s) and ${String(steps.length)} step(s).`);
|
|
16848
17245
|
const stepsClient = createStepDefinitionsClient(opts.baseUrl);
|
|
16849
|
-
|
|
17246
|
+
const pipelinesClient = createPipelineDefinitionsClient(opts.baseUrl);
|
|
17247
|
+
const serverPipelineKeys = pipelines.length > 0 ? (await pipelinesClient.listByProjectId(opts.projectId, { headers })).map((pipeline2) => pipeline2.key) : [];
|
|
17248
|
+
assertValidDefinitionSpecs({ pipelines, steps }, { knownPipelineKeys: serverPipelineKeys });
|
|
17249
|
+
for (const spec of steps) {
|
|
16850
17250
|
await stepsClient.upsertFromSpec(opts.projectId, spec, { headers });
|
|
16851
17251
|
log(`\u2713 step ${spec.key} v${String(spec.version)} \u2192 upserted`);
|
|
16852
17252
|
}
|
|
16853
|
-
|
|
16854
|
-
let pushedPipelinesCount = 0;
|
|
16855
|
-
if (pipelineSpecs.length > 0) {
|
|
16856
|
-
const existingPipelines = await pipelinesClient.listByProjectId(opts.projectId, { headers });
|
|
16857
|
-
const knownPipelineKeys = new Set([
|
|
16858
|
-
...pipelineSpecs.map((s) => s.key),
|
|
16859
|
-
...existingPipelines.map((p) => p.key)
|
|
16860
|
-
]);
|
|
16861
|
-
for (const spec of pipelineSpecs) {
|
|
16862
|
-
for (const step of spec.steps) {
|
|
16863
|
-
const routeKeys = extractRoutePipelineKeys(step.advancementPolicyDefinition);
|
|
16864
|
-
for (const routeKey of routeKeys) {
|
|
16865
|
-
if (!knownPipelineKeys.has(routeKey)) {
|
|
16866
|
-
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.`);
|
|
16867
|
-
}
|
|
16868
|
-
}
|
|
16869
|
-
}
|
|
16870
|
-
}
|
|
17253
|
+
if (pipelines.length > 0) {
|
|
16871
17254
|
const serverSteps = await stepsClient.listByProjectId(opts.projectId, {
|
|
16872
17255
|
headers
|
|
16873
17256
|
});
|
|
@@ -16876,27 +17259,17 @@ async function pushFromDirectory(dir, opts) {
|
|
|
16876
17259
|
key: s.key,
|
|
16877
17260
|
version: s.version
|
|
16878
17261
|
}));
|
|
16879
|
-
for (const spec of
|
|
17262
|
+
for (const spec of pipelines) {
|
|
16880
17263
|
await pipelinesClient.upsertFromSpec(opts.projectId, spec, stepDefs, {
|
|
16881
17264
|
headers
|
|
16882
17265
|
});
|
|
16883
17266
|
log(`\u2713 pipeline ${spec.key} v${String(spec.version)} \u2192 upserted`);
|
|
16884
17267
|
}
|
|
16885
|
-
pushedPipelinesCount = pipelineSpecs.length;
|
|
16886
|
-
}
|
|
16887
|
-
let syncedDefaultPipelineAssignment = false;
|
|
16888
|
-
if (hasAssignmentFile) {
|
|
16889
|
-
const assignmentFilePath = join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
|
|
16890
|
-
const assignmentMod = await import(pathToFileURL(assignmentFilePath).href);
|
|
16891
|
-
const assignmentSpec = assignmentMod["default"];
|
|
16892
|
-
if (!isDefaultPipelineAssignmentSpec(assignmentSpec)) {
|
|
16893
|
-
throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} must have a default export produced by defaultPipelineAssignment(({ assign, skip, ... }) => ({ default: ..., rules: [...] })). Got: ${typeof assignmentSpec}`);
|
|
16894
|
-
}
|
|
16895
|
-
syncedDefaultPipelineAssignment = await syncDefaultPipelineAssignment(assignmentSpec, opts, headers, pipelinesClient, log);
|
|
16896
17268
|
}
|
|
17269
|
+
const syncedDefaultPipelineAssignment = collected.defaultPipelineAssignment ? await syncDefaultPipelineAssignment(collected.defaultPipelineAssignment, opts, headers, pipelinesClient, log) : false;
|
|
16897
17270
|
return {
|
|
16898
|
-
pushedSteps:
|
|
16899
|
-
pushedPipelines:
|
|
17271
|
+
pushedSteps: steps.length,
|
|
17272
|
+
pushedPipelines: pipelines.length,
|
|
16900
17273
|
syncedDefaultPipelineAssignment
|
|
16901
17274
|
};
|
|
16902
17275
|
}
|
|
@@ -16918,7 +17291,7 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
|
|
|
16918
17291
|
}
|
|
16919
17292
|
for (const key of referencedKeys) {
|
|
16920
17293
|
if (!pipelineKeyToId.has(key)) {
|
|
16921
|
-
throw new Error(
|
|
17294
|
+
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\`.`);
|
|
16922
17295
|
}
|
|
16923
17296
|
}
|
|
16924
17297
|
const linearPipelineDefinitionId = pipelineKeyToId.get(serialized.linearPipelineDefinitionKey);
|
|
@@ -16930,7 +17303,7 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
|
|
|
16930
17303
|
const pKey = rule.event.params["pipelineKey"];
|
|
16931
17304
|
const pId = pipelineKeyToId.get(pKey);
|
|
16932
17305
|
if (!pId) {
|
|
16933
|
-
throw new Error(
|
|
17306
|
+
throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} assign() references pipeline "${pKey}" ` + `which was not found on the server.`);
|
|
16934
17307
|
}
|
|
16935
17308
|
return {
|
|
16936
17309
|
...rule,
|
|
@@ -16968,5 +17341,6 @@ async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClien
|
|
|
16968
17341
|
return true;
|
|
16969
17342
|
}
|
|
16970
17343
|
export {
|
|
16971
|
-
pushFromDirectory
|
|
17344
|
+
pushFromDirectory,
|
|
17345
|
+
collectDefinitionsFromDirectory
|
|
16972
17346
|
};
|
|
@@ -14,12 +14,18 @@ export interface PushFromDirectoryResult {
|
|
|
14
14
|
/**
|
|
15
15
|
* Imports every `.ts`/`.js` file in `dir` (except the push script itself and
|
|
16
16
|
* `default-pipeline-assignment.ts`), collects all pipeline and step
|
|
17
|
-
* definitions, then upserts them via the strongly-typed SDK
|
|
17
|
+
* definitions, validates them, then upserts them via the strongly-typed SDK
|
|
18
|
+
* clients.
|
|
18
19
|
*
|
|
19
20
|
* If `default-pipeline-assignment.ts` is present, it is imported separately
|
|
20
21
|
* after pipelines are pushed, and the project default pipeline assignment is
|
|
21
22
|
* updated on the server.
|
|
22
23
|
*
|
|
24
|
+
* Collection is `collectDefinitionsFromDirectory` (offline, no token) and
|
|
25
|
+
* validation is `validateDefinitionSpecs` (pure). Both run before the first
|
|
26
|
+
* mutating request, so a batch with a dead signal `sourcePath`, a dangling
|
|
27
|
+
* route target, or a backwards signal binding fails without half-pushing.
|
|
28
|
+
*
|
|
23
29
|
* Designed to run on the user's native runtime (bun, node-with-tsx, deno),
|
|
24
30
|
* NOT inside a `bun --compile`'d binary — that runtime can't resolve scoped
|
|
25
31
|
* package `exports` field remappings from external user files.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@boboddy/sdk",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.12-alpha",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"import": "./dist/definitions/advancement-policies/index.js",
|
|
29
29
|
"types": "./dist/definitions/advancement-policies/index.d.ts"
|
|
30
30
|
},
|
|
31
|
+
"./definitions/validation": {
|
|
32
|
+
"import": "./dist/definitions/validation/index.js",
|
|
33
|
+
"types": "./dist/definitions/validation/index.d.ts"
|
|
34
|
+
},
|
|
31
35
|
"./defaults": {
|
|
32
36
|
"import": "./dist/defaults/index.js",
|
|
33
37
|
"types": "./dist/defaults/index.d.ts"
|