@boboddy/sdk 0.1.38-alpha → 0.1.40-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.
@@ -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 sourceFiles = readdirSync(absDir).filter((f) => (f.endsWith(".ts") || f.endsWith(".js")) && !PUSH_SCRIPT_NAMES.has(f));
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
- const existingPipelines = await pipelinesClient.listByProjectId(opts.projectId, { headers });
16489
- const knownPipelineKeys = new Set([
16490
- ...pipelineSpecs.map((s) => s.key),
16491
- ...existingPipelines.map((p) => p.key)
16492
- ]);
16493
- for (const spec of pipelineSpecs) {
16494
- for (const step of spec.steps) {
16495
- const routeKeys = extractRoutePipelineKeys(step.advancementPolicyDefinition);
16496
- for (const routeKey of routeKeys) {
16497
- if (!knownPipelineKeys.has(routeKey)) {
16498
- 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.`);
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
- const serverSteps = await stepsClient.listByProjectId(opts.projectId, {
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 stepDefs = (serverSteps ?? []).map((s) => ({
16507
- id: s.id,
16508
- key: s.key,
16509
- version: s.version
16510
- }));
16511
- for (const spec of pipelineSpecs) {
16512
- await pipelinesClient.upsertFromSpec(opts.projectId, spec, stepDefs, {
16513
- headers
16514
- });
16515
- log(`\u2713 pipeline ${spec.key} v${String(spec.version)} \u2192 upserted`);
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
- return { pushedSteps: stepMap.size, pushedPipelines: pipelineSpecs.length };
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 definitions, then upserts them via the
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
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.1.38-alpha",
4
+ "version": "0.1.40-alpha",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {