@boboddy/sdk 0.1.38-alpha → 0.1.39-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 +10 -0
- package/dist/definitions/pipelines/define-default-pipeline-assignment.d.ts +211 -0
- package/dist/definitions/pipelines/index.d.ts +1 -0
- package/dist/definitions/pipelines/index.js +149 -0
- package/dist/definitions/steps/index.js +10 -0
- package/dist/generated/index.d.ts +1 -1
- package/dist/generated/sdk.gen.d.ts +2 -1
- package/dist/generated/types.gen.d.ts +425 -0
- package/dist/index.js +149 -0
- package/dist/push/index.js +254 -28
- package/dist/push/push-from-directory.d.ts +9 -3
- package/package.json +1 -1
package/dist/push/index.js
CHANGED
|
@@ -12936,6 +12936,16 @@ class Projects extends HeyApiClient {
|
|
|
12936
12936
|
getProject(options) {
|
|
12937
12937
|
return (options.client ?? this.client).get({ url: "/api/projects/{projectId}", ...options });
|
|
12938
12938
|
}
|
|
12939
|
+
updateProjectDefaultPipelineAssignment(options) {
|
|
12940
|
+
return (options.client ?? this.client).put({
|
|
12941
|
+
url: "/api/projects/{projectId}/default-pipeline-assignment",
|
|
12942
|
+
...options,
|
|
12943
|
+
headers: {
|
|
12944
|
+
"Content-Type": "application/json",
|
|
12945
|
+
...options.headers
|
|
12946
|
+
}
|
|
12947
|
+
});
|
|
12948
|
+
}
|
|
12939
12949
|
updateProjectMemberPermissions(options) {
|
|
12940
12950
|
return (options.client ?? this.client).put({ url: "/api/projects/{projectId}/members/{userId}/permissions", ...options });
|
|
12941
12951
|
}
|
|
@@ -16416,8 +16426,143 @@ var buildPipelineDefinitionsClient = (pipelineDefinitions) => {
|
|
|
16416
16426
|
}
|
|
16417
16427
|
};
|
|
16418
16428
|
};
|
|
16429
|
+
// src/definitions/pipelines/define-default-pipeline-assignment.ts
|
|
16430
|
+
var DEFAULT_PIPELINE_ASSIGNMENT_FILENAME = "default-pipeline-assignment.ts";
|
|
16431
|
+
function makeLeaf(fact, path, operator, value) {
|
|
16432
|
+
const condition = {
|
|
16433
|
+
_tag: "leaf",
|
|
16434
|
+
fact,
|
|
16435
|
+
...path ? { path } : {},
|
|
16436
|
+
operator,
|
|
16437
|
+
value
|
|
16438
|
+
};
|
|
16439
|
+
return {
|
|
16440
|
+
_condition: condition,
|
|
16441
|
+
then(outcome) {
|
|
16442
|
+
return { _tag: "assignment_rule", conditions: [condition], mode: "all", outcome };
|
|
16443
|
+
}
|
|
16444
|
+
};
|
|
16445
|
+
}
|
|
16446
|
+
function makeFieldRef(fact, path) {
|
|
16447
|
+
return {
|
|
16448
|
+
eq: (v) => makeLeaf(fact, path, "equal", v),
|
|
16449
|
+
ne: (v) => makeLeaf(fact, path, "notEqual", v),
|
|
16450
|
+
gt: (v) => makeLeaf(fact, path, "greaterThan", v),
|
|
16451
|
+
gte: (v) => makeLeaf(fact, path, "greaterThanInclusive", v),
|
|
16452
|
+
lt: (v) => makeLeaf(fact, path, "lessThan", v),
|
|
16453
|
+
lte: (v) => makeLeaf(fact, path, "lessThanInclusive", v),
|
|
16454
|
+
in: (vs) => makeLeaf(fact, path, "in", vs),
|
|
16455
|
+
notIn: (vs) => makeLeaf(fact, path, "notIn", vs),
|
|
16456
|
+
contains: (v) => makeLeaf(fact, path, "contains", v),
|
|
16457
|
+
doesNotContain: (v) => makeLeaf(fact, path, "doesNotContain", v)
|
|
16458
|
+
};
|
|
16459
|
+
}
|
|
16460
|
+
function makeAssign(pipeline2) {
|
|
16461
|
+
if (typeof pipeline2 !== "object" || pipeline2 === null || typeof pipeline2["key"] !== "string" || !Array.isArray(pipeline2["steps"])) {
|
|
16462
|
+
throw new Error("assign() requires a pipeline spec produced by pipeline().build(). " + "Pass the default-exported value from a pipeline definition file.");
|
|
16463
|
+
}
|
|
16464
|
+
return { _tag: "assign", pipeline: pipeline2 };
|
|
16465
|
+
}
|
|
16466
|
+
function extractCondition2(ref) {
|
|
16467
|
+
return ref._condition;
|
|
16468
|
+
}
|
|
16469
|
+
function makeGroup(mode, refs) {
|
|
16470
|
+
const conditions = refs.map(extractCondition2);
|
|
16471
|
+
const condition = { _tag: "group", mode, conditions };
|
|
16472
|
+
return {
|
|
16473
|
+
_condition: condition,
|
|
16474
|
+
then(outcome) {
|
|
16475
|
+
return { _tag: "assignment_rule", conditions, mode, outcome };
|
|
16476
|
+
}
|
|
16477
|
+
};
|
|
16478
|
+
}
|
|
16479
|
+
function buildCtx() {
|
|
16480
|
+
return {
|
|
16481
|
+
workItem: {
|
|
16482
|
+
field: (name) => makeFieldRef("workItem", `$.fields.${name}`)
|
|
16483
|
+
},
|
|
16484
|
+
context: {
|
|
16485
|
+
isNew: makeFieldRef("context", "$.isNew")
|
|
16486
|
+
},
|
|
16487
|
+
assign: makeAssign,
|
|
16488
|
+
skip: () => ({ _tag: "skip" }),
|
|
16489
|
+
all: (...refs) => makeGroup("all", refs),
|
|
16490
|
+
any: (...refs) => makeGroup("any", refs)
|
|
16491
|
+
};
|
|
16492
|
+
}
|
|
16493
|
+
function defaultPipelineAssignment(callback) {
|
|
16494
|
+
const input = callback(buildCtx());
|
|
16495
|
+
return {
|
|
16496
|
+
_tag: "default_pipeline_assignment",
|
|
16497
|
+
default: input.default,
|
|
16498
|
+
rules: input.rules
|
|
16499
|
+
};
|
|
16500
|
+
}
|
|
16501
|
+
function serializeConditionNode(condition) {
|
|
16502
|
+
if (condition._tag === "leaf") {
|
|
16503
|
+
return {
|
|
16504
|
+
fact: condition.fact,
|
|
16505
|
+
...condition.path ? { path: condition.path } : {},
|
|
16506
|
+
operator: condition.operator,
|
|
16507
|
+
value: condition.value
|
|
16508
|
+
};
|
|
16509
|
+
}
|
|
16510
|
+
if (condition._tag === "group") {
|
|
16511
|
+
if (condition.mode === "all") {
|
|
16512
|
+
return { all: condition.conditions.map(serializeConditionNode) };
|
|
16513
|
+
}
|
|
16514
|
+
return { any: condition.conditions.map(serializeConditionNode) };
|
|
16515
|
+
}
|
|
16516
|
+
throw new Error(`Unknown condition tag: ${JSON.stringify(condition["_tag"])}`);
|
|
16517
|
+
}
|
|
16518
|
+
function serializeOutcome(outcome) {
|
|
16519
|
+
if (outcome._tag === "skip")
|
|
16520
|
+
return { type: "skip", params: null };
|
|
16521
|
+
return { type: "assign", params: { pipelineKey: outcome.pipeline.key } };
|
|
16522
|
+
}
|
|
16523
|
+
function serializeAssignmentRule(rule) {
|
|
16524
|
+
const { type, params } = serializeOutcome(rule.outcome);
|
|
16525
|
+
return {
|
|
16526
|
+
conditions: { [rule.mode]: rule.conditions.map(serializeConditionNode) },
|
|
16527
|
+
event: { type, ...params ? { params } : {} }
|
|
16528
|
+
};
|
|
16529
|
+
}
|
|
16530
|
+
function serializeDefaultPipelineAssignment(spec) {
|
|
16531
|
+
let primaryKey = null;
|
|
16532
|
+
if (spec.default._tag === "assign") {
|
|
16533
|
+
primaryKey = spec.default.pipeline.key;
|
|
16534
|
+
} else {
|
|
16535
|
+
for (const rule of spec.rules) {
|
|
16536
|
+
if (rule.outcome._tag === "assign") {
|
|
16537
|
+
primaryKey = rule.outcome.pipeline.key;
|
|
16538
|
+
break;
|
|
16539
|
+
}
|
|
16540
|
+
}
|
|
16541
|
+
}
|
|
16542
|
+
if (primaryKey === null) {
|
|
16543
|
+
throw new Error("defaultPipelineAssignment must contain at least one assign() outcome " + "(in `default` or in `rules`). A policy that only skips is not useful.");
|
|
16544
|
+
}
|
|
16545
|
+
const { type: defaultType, params: defaultParams } = serializeOutcome(spec.default);
|
|
16546
|
+
const outcomeSet = new Set([defaultType]);
|
|
16547
|
+
const serializedRules = spec.rules.map((rule) => {
|
|
16548
|
+
outcomeSet.add(serializeOutcome(rule.outcome).type);
|
|
16549
|
+
return serializeAssignmentRule(rule);
|
|
16550
|
+
});
|
|
16551
|
+
return {
|
|
16552
|
+
linearPipelineDefinitionKey: primaryKey,
|
|
16553
|
+
rulesJson: { rules: serializedRules },
|
|
16554
|
+
defaultEventType: defaultType,
|
|
16555
|
+
defaultEventParamsJson: defaultParams,
|
|
16556
|
+
allowedEventTypes: [...outcomeSet]
|
|
16557
|
+
};
|
|
16558
|
+
}
|
|
16559
|
+
function isDefaultPipelineAssignmentSpec(value) {
|
|
16560
|
+
if (typeof value !== "object" || value === null)
|
|
16561
|
+
return false;
|
|
16562
|
+
return value["_tag"] === "default_pipeline_assignment";
|
|
16563
|
+
}
|
|
16419
16564
|
// src/push/push-from-directory.ts
|
|
16420
|
-
import { readdirSync } from "fs";
|
|
16565
|
+
import { existsSync, readdirSync } from "fs";
|
|
16421
16566
|
import { join, resolve } from "path";
|
|
16422
16567
|
import { pathToFileURL } from "url";
|
|
16423
16568
|
function isStepDefinitionSpec(value) {
|
|
@@ -16449,7 +16594,9 @@ async function pushFromDirectory(dir, opts) {
|
|
|
16449
16594
|
const log = opts.log ?? ((msg) => console.log(msg));
|
|
16450
16595
|
const headers = { Authorization: `Bearer ${opts.accessToken}` };
|
|
16451
16596
|
const absDir = resolve(dir);
|
|
16452
|
-
const
|
|
16597
|
+
const allFiles = readdirSync(absDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
|
|
16598
|
+
const hasAssignmentFile = existsSync(join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME));
|
|
16599
|
+
const sourceFiles = allFiles.filter((f) => !PUSH_SCRIPT_NAMES.has(f) && f !== DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
|
|
16453
16600
|
const pipelineSpecs = [];
|
|
16454
16601
|
const stepMap = new Map;
|
|
16455
16602
|
for (const file2 of sourceFiles) {
|
|
@@ -16481,40 +16628,119 @@ async function pushFromDirectory(dir, opts) {
|
|
|
16481
16628
|
await stepsClient.upsertFromSpec(opts.projectId, spec, { headers });
|
|
16482
16629
|
log(`\u2713 step ${spec.key} v${String(spec.version)} \u2192 upserted`);
|
|
16483
16630
|
}
|
|
16484
|
-
if (pipelineSpecs.length === 0) {
|
|
16485
|
-
return { pushedSteps: stepMap.size, pushedPipelines: 0 };
|
|
16486
|
-
}
|
|
16487
16631
|
const pipelinesClient = createPipelineDefinitionsClient(opts.baseUrl);
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
|
|
16495
|
-
|
|
16496
|
-
for (const
|
|
16497
|
-
|
|
16498
|
-
|
|
16632
|
+
let pushedPipelinesCount = 0;
|
|
16633
|
+
if (pipelineSpecs.length > 0) {
|
|
16634
|
+
const existingPipelines = await pipelinesClient.listByProjectId(opts.projectId, { headers });
|
|
16635
|
+
const knownPipelineKeys = new Set([
|
|
16636
|
+
...pipelineSpecs.map((s) => s.key),
|
|
16637
|
+
...existingPipelines.map((p) => p.key)
|
|
16638
|
+
]);
|
|
16639
|
+
for (const spec of pipelineSpecs) {
|
|
16640
|
+
for (const step of spec.steps) {
|
|
16641
|
+
const routeKeys = extractRoutePipelineKeys(step.advancementPolicyDefinition);
|
|
16642
|
+
for (const routeKey of routeKeys) {
|
|
16643
|
+
if (!knownPipelineKeys.has(routeKey)) {
|
|
16644
|
+
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.`);
|
|
16645
|
+
}
|
|
16499
16646
|
}
|
|
16500
16647
|
}
|
|
16501
16648
|
}
|
|
16649
|
+
const serverSteps = await stepsClient.listByProjectId(opts.projectId, {
|
|
16650
|
+
headers
|
|
16651
|
+
});
|
|
16652
|
+
const stepDefs = (serverSteps ?? []).map((s) => ({
|
|
16653
|
+
id: s.id,
|
|
16654
|
+
key: s.key,
|
|
16655
|
+
version: s.version
|
|
16656
|
+
}));
|
|
16657
|
+
for (const spec of pipelineSpecs) {
|
|
16658
|
+
await pipelinesClient.upsertFromSpec(opts.projectId, spec, stepDefs, {
|
|
16659
|
+
headers
|
|
16660
|
+
});
|
|
16661
|
+
log(`\u2713 pipeline ${spec.key} v${String(spec.version)} \u2192 upserted`);
|
|
16662
|
+
}
|
|
16663
|
+
pushedPipelinesCount = pipelineSpecs.length;
|
|
16664
|
+
}
|
|
16665
|
+
let syncedDefaultPipelineAssignment = false;
|
|
16666
|
+
if (hasAssignmentFile) {
|
|
16667
|
+
const assignmentFilePath = join(absDir, DEFAULT_PIPELINE_ASSIGNMENT_FILENAME);
|
|
16668
|
+
const assignmentMod = await import(pathToFileURL(assignmentFilePath).href);
|
|
16669
|
+
const assignmentSpec = assignmentMod["default"];
|
|
16670
|
+
if (!isDefaultPipelineAssignmentSpec(assignmentSpec)) {
|
|
16671
|
+
throw new Error(`${DEFAULT_PIPELINE_ASSIGNMENT_FILENAME} must have a default export produced by defaultPipelineAssignment(({ assign, skip, ... }) => ({ default: ..., rules: [...] })). Got: ${typeof assignmentSpec}`);
|
|
16672
|
+
}
|
|
16673
|
+
syncedDefaultPipelineAssignment = await syncDefaultPipelineAssignment(assignmentSpec, opts, headers, pipelinesClient, log);
|
|
16502
16674
|
}
|
|
16503
|
-
|
|
16675
|
+
return {
|
|
16676
|
+
pushedSteps: stepMap.size,
|
|
16677
|
+
pushedPipelines: pushedPipelinesCount,
|
|
16678
|
+
syncedDefaultPipelineAssignment
|
|
16679
|
+
};
|
|
16680
|
+
}
|
|
16681
|
+
async function syncDefaultPipelineAssignment(spec, opts, headers, pipelinesClient, log) {
|
|
16682
|
+
const serialized = serializeDefaultPipelineAssignment(spec);
|
|
16683
|
+
const serverPipelines = await pipelinesClient.listByProjectId(opts.projectId, {
|
|
16504
16684
|
headers
|
|
16505
16685
|
});
|
|
16506
|
-
const
|
|
16507
|
-
|
|
16508
|
-
|
|
16509
|
-
|
|
16510
|
-
|
|
16511
|
-
|
|
16512
|
-
|
|
16513
|
-
|
|
16514
|
-
|
|
16515
|
-
|
|
16686
|
+
const pipelineKeyToId = new Map(serverPipelines.map((p) => [
|
|
16687
|
+
p.key,
|
|
16688
|
+
p.id
|
|
16689
|
+
]));
|
|
16690
|
+
const referencedKeys = new Set;
|
|
16691
|
+
referencedKeys.add(serialized.linearPipelineDefinitionKey);
|
|
16692
|
+
for (const rule of serialized.rulesJson.rules) {
|
|
16693
|
+
if (rule.event.type === "assign" && typeof rule.event.params?.["pipelineKey"] === "string") {
|
|
16694
|
+
referencedKeys.add(rule.event.params["pipelineKey"]);
|
|
16695
|
+
}
|
|
16696
|
+
}
|
|
16697
|
+
for (const key of referencedKeys) {
|
|
16698
|
+
if (!pipelineKeyToId.has(key)) {
|
|
16699
|
+
throw new Error(`default-pipeline-assignment.ts references pipeline "${key}", but no pipeline with that key was found on the server. Push the pipeline first with \`boboddy pipelines push\`.`);
|
|
16700
|
+
}
|
|
16701
|
+
}
|
|
16702
|
+
const linearPipelineDefinitionId = pipelineKeyToId.get(serialized.linearPipelineDefinitionKey);
|
|
16703
|
+
const resolvedRules = serialized.rulesJson.rules.map((rule) => {
|
|
16704
|
+
if (rule.event.type === "assign" && typeof rule.event.params?.["pipelineKey"] === "string") {
|
|
16705
|
+
const pKey = rule.event.params["pipelineKey"];
|
|
16706
|
+
const pId = pipelineKeyToId.get(pKey);
|
|
16707
|
+
if (!pId) {
|
|
16708
|
+
throw new Error(`default-pipeline-assignment.ts assign() references pipeline "${pKey}" which was not found on the server.`);
|
|
16709
|
+
}
|
|
16710
|
+
return {
|
|
16711
|
+
...rule,
|
|
16712
|
+
event: {
|
|
16713
|
+
...rule.event,
|
|
16714
|
+
params: {
|
|
16715
|
+
...rule.event.params,
|
|
16716
|
+
pipelineDefinitionId: pId
|
|
16717
|
+
}
|
|
16718
|
+
}
|
|
16719
|
+
};
|
|
16720
|
+
}
|
|
16721
|
+
return rule;
|
|
16722
|
+
});
|
|
16723
|
+
const projectsClient = new Projects({
|
|
16724
|
+
client: createClient({ baseUrl: opts.baseUrl })
|
|
16725
|
+
});
|
|
16726
|
+
const result = await projectsClient.updateProjectDefaultPipelineAssignment({
|
|
16727
|
+
path: { projectId: opts.projectId },
|
|
16728
|
+
body: {
|
|
16729
|
+
defaultPipelineAssignment: {
|
|
16730
|
+
linearPipelineDefinitionId,
|
|
16731
|
+
rulesJson: { rules: resolvedRules },
|
|
16732
|
+
defaultEventType: serialized.defaultEventType,
|
|
16733
|
+
defaultEventParamsJson: serialized.defaultEventParamsJson,
|
|
16734
|
+
allowedEventTypes: serialized.allowedEventTypes
|
|
16735
|
+
}
|
|
16736
|
+
},
|
|
16737
|
+
headers
|
|
16738
|
+
});
|
|
16739
|
+
if (result.error) {
|
|
16740
|
+
throw new Error(`Failed to update project default pipeline assignment: ${JSON.stringify(result.error)}`);
|
|
16516
16741
|
}
|
|
16517
|
-
|
|
16742
|
+
log(`\u2713 default pipeline assignment \u2192 synced (primary pipeline: ${serialized.linearPipelineDefinitionKey})`);
|
|
16743
|
+
return true;
|
|
16518
16744
|
}
|
|
16519
16745
|
export {
|
|
16520
16746
|
pushFromDirectory
|
|
@@ -8,11 +8,17 @@ export interface PushFromDirectoryOptions {
|
|
|
8
8
|
export interface PushFromDirectoryResult {
|
|
9
9
|
pushedSteps: number;
|
|
10
10
|
pushedPipelines: number;
|
|
11
|
+
/** true if the default pipeline assignment was synced to the server. */
|
|
12
|
+
syncedDefaultPipelineAssignment: boolean;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
|
-
* Imports every `.ts`/`.js` file in `dir` (except the push script itself
|
|
14
|
-
* collects all pipeline and step
|
|
15
|
-
* strongly-typed SDK clients.
|
|
15
|
+
* Imports every `.ts`/`.js` file in `dir` (except the push script itself and
|
|
16
|
+
* `default-pipeline-assignment.ts`), collects all pipeline and step
|
|
17
|
+
* definitions, then upserts them via the strongly-typed SDK clients.
|
|
18
|
+
*
|
|
19
|
+
* If `default-pipeline-assignment.ts` is present, it is imported separately
|
|
20
|
+
* after pipelines are pushed, and the project default pipeline assignment is
|
|
21
|
+
* updated on the server.
|
|
16
22
|
*
|
|
17
23
|
* Designed to run on the user's native runtime (bun, node-with-tsx, deno),
|
|
18
24
|
* NOT inside a `bun --compile`'d binary — that runtime can't resolve scoped
|