@harness-engineering/core 0.17.0 → 0.17.1

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/index.d.mts CHANGED
@@ -5166,6 +5166,10 @@ interface GitHubAdapterOptions {
5166
5166
  fetchFn?: typeof fetch;
5167
5167
  /** Override API base URL (for GitHub Enterprise) */
5168
5168
  apiBase?: string;
5169
+ /** Max retries on rate limit (default: 5) */
5170
+ maxRetries?: number;
5171
+ /** Base delay in ms for exponential backoff (default: 1000) */
5172
+ baseDelayMs?: number;
5169
5173
  }
5170
5174
  declare class GitHubIssuesSyncAdapter implements TrackerSyncAdapter {
5171
5175
  private readonly token;
@@ -5174,8 +5178,14 @@ declare class GitHubIssuesSyncAdapter implements TrackerSyncAdapter {
5174
5178
  private readonly apiBase;
5175
5179
  private readonly owner;
5176
5180
  private readonly repo;
5181
+ private readonly retryOpts;
5177
5182
  constructor(options: GitHubAdapterOptions);
5178
5183
  private headers;
5184
+ /**
5185
+ * Close an issue if the feature status maps to 'closed'.
5186
+ * GitHub Issues API doesn't accept state on POST — requires a follow-up PATCH.
5187
+ */
5188
+ private closeIfDone;
5179
5189
  createTicket(feature: RoadmapFeature, milestone: string): Promise<Result<ExternalTicket>>;
5180
5190
  updateTicket(externalId: string, changes: Partial<RoadmapFeature>): Promise<Result<ExternalTicket>>;
5181
5191
  fetchTicketState(externalId: string): Promise<Result<ExternalTicketState>>;
package/dist/index.d.ts CHANGED
@@ -5166,6 +5166,10 @@ interface GitHubAdapterOptions {
5166
5166
  fetchFn?: typeof fetch;
5167
5167
  /** Override API base URL (for GitHub Enterprise) */
5168
5168
  apiBase?: string;
5169
+ /** Max retries on rate limit (default: 5) */
5170
+ maxRetries?: number;
5171
+ /** Base delay in ms for exponential backoff (default: 1000) */
5172
+ baseDelayMs?: number;
5169
5173
  }
5170
5174
  declare class GitHubIssuesSyncAdapter implements TrackerSyncAdapter {
5171
5175
  private readonly token;
@@ -5174,8 +5178,14 @@ declare class GitHubIssuesSyncAdapter implements TrackerSyncAdapter {
5174
5178
  private readonly apiBase;
5175
5179
  private readonly owner;
5176
5180
  private readonly repo;
5181
+ private readonly retryOpts;
5177
5182
  constructor(options: GitHubAdapterOptions);
5178
5183
  private headers;
5184
+ /**
5185
+ * Close an issue if the feature status maps to 'closed'.
5186
+ * GitHub Issues API doesn't accept state on POST — requires a follow-up PATCH.
5187
+ */
5188
+ private closeIfDone;
5179
5189
  createTicket(feature: RoadmapFeature, milestone: string): Promise<Result<ExternalTicket>>;
5180
5190
  updateTicket(externalId: string, changes: Partial<RoadmapFeature>): Promise<Result<ExternalTicket>>;
5181
5191
  fetchTicketState(externalId: string): Promise<Result<ExternalTicketState>>;
package/dist/index.js CHANGED
@@ -12574,6 +12574,29 @@ function labelsForStatus(status, config) {
12574
12574
  }
12575
12575
  return [...base];
12576
12576
  }
12577
+ var RETRY_DEFAULTS = { maxRetries: 5, baseDelayMs: 1e3 };
12578
+ function sleep(ms) {
12579
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
12580
+ }
12581
+ async function fetchWithRetry(fetchFn, input, init, opts = RETRY_DEFAULTS) {
12582
+ let lastResponse;
12583
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
12584
+ const response = await fetchFn(input, init);
12585
+ if (response.status !== 403 && response.status !== 429) return response;
12586
+ lastResponse = response;
12587
+ if (attempt === opts.maxRetries) break;
12588
+ const retryAfter = response.headers.get("Retry-After");
12589
+ let delayMs;
12590
+ if (retryAfter) {
12591
+ const seconds = parseInt(retryAfter, 10);
12592
+ delayMs = isNaN(seconds) ? opts.baseDelayMs : seconds * 1e3;
12593
+ } else {
12594
+ delayMs = opts.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500;
12595
+ }
12596
+ await sleep(delayMs);
12597
+ }
12598
+ return lastResponse;
12599
+ }
12577
12600
  var GitHubIssuesSyncAdapter = class {
12578
12601
  token;
12579
12602
  config;
@@ -12581,11 +12604,16 @@ var GitHubIssuesSyncAdapter = class {
12581
12604
  apiBase;
12582
12605
  owner;
12583
12606
  repo;
12607
+ retryOpts;
12584
12608
  constructor(options) {
12585
12609
  this.token = options.token;
12586
12610
  this.config = options.config;
12587
12611
  this.fetchFn = options.fetchFn ?? globalThis.fetch;
12588
12612
  this.apiBase = options.apiBase ?? "https://api.github.com";
12613
+ this.retryOpts = {
12614
+ maxRetries: options.maxRetries ?? RETRY_DEFAULTS.maxRetries,
12615
+ baseDelayMs: options.baseDelayMs ?? RETRY_DEFAULTS.baseDelayMs
12616
+ };
12589
12617
  const repoParts = (options.config.repo ?? "").split("/");
12590
12618
  if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
12591
12619
  throw new Error(`Invalid repo format: "${options.config.repo}". Expected "owner/repo".`);
@@ -12601,6 +12629,20 @@ var GitHubIssuesSyncAdapter = class {
12601
12629
  "X-GitHub-Api-Version": "2022-11-28"
12602
12630
  };
12603
12631
  }
12632
+ /**
12633
+ * Close an issue if the feature status maps to 'closed'.
12634
+ * GitHub Issues API doesn't accept state on POST — requires a follow-up PATCH.
12635
+ */
12636
+ async closeIfDone(issueNumber, featureStatus) {
12637
+ const externalStatus = this.config.statusMap[featureStatus];
12638
+ if (externalStatus !== "closed") return;
12639
+ await fetchWithRetry(
12640
+ this.fetchFn,
12641
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues/${issueNumber}`,
12642
+ { method: "PATCH", headers: this.headers(), body: JSON.stringify({ state: "closed" }) },
12643
+ this.retryOpts
12644
+ );
12645
+ }
12604
12646
  async createTicket(feature, milestone) {
12605
12647
  try {
12606
12648
  const labels = labelsForStatus(feature.status, this.config);
@@ -12610,17 +12652,15 @@ var GitHubIssuesSyncAdapter = class {
12610
12652
  `**Milestone:** ${milestone}`,
12611
12653
  feature.spec ? `**Spec:** ${feature.spec}` : ""
12612
12654
  ].filter(Boolean).join("\n");
12613
- const response = await this.fetchFn(
12655
+ const response = await fetchWithRetry(
12656
+ this.fetchFn,
12614
12657
  `${this.apiBase}/repos/${this.owner}/${this.repo}/issues`,
12615
12658
  {
12616
12659
  method: "POST",
12617
12660
  headers: this.headers(),
12618
- body: JSON.stringify({
12619
- title: feature.name,
12620
- body,
12621
- labels
12622
- })
12623
- }
12661
+ body: JSON.stringify({ title: feature.name, body, labels })
12662
+ },
12663
+ this.retryOpts
12624
12664
  );
12625
12665
  if (!response.ok) {
12626
12666
  const text = await response.text();
@@ -12628,6 +12668,7 @@ var GitHubIssuesSyncAdapter = class {
12628
12668
  }
12629
12669
  const data = await response.json();
12630
12670
  const externalId = buildExternalId(this.owner, this.repo, data.number);
12671
+ await this.closeIfDone(data.number, feature.status);
12631
12672
  return (0, import_types21.Ok)({ externalId, url: data.html_url });
12632
12673
  } catch (error) {
12633
12674
  return (0, import_types21.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -12648,13 +12689,15 @@ var GitHubIssuesSyncAdapter = class {
12648
12689
  patch.state = externalStatus;
12649
12690
  patch.labels = labelsForStatus(changes.status, this.config);
12650
12691
  }
12651
- const response = await this.fetchFn(
12692
+ const response = await fetchWithRetry(
12693
+ this.fetchFn,
12652
12694
  `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
12653
12695
  {
12654
12696
  method: "PATCH",
12655
12697
  headers: this.headers(),
12656
12698
  body: JSON.stringify(patch)
12657
- }
12699
+ },
12700
+ this.retryOpts
12658
12701
  );
12659
12702
  if (!response.ok) {
12660
12703
  const text = await response.text();
@@ -12670,12 +12713,14 @@ var GitHubIssuesSyncAdapter = class {
12670
12713
  try {
12671
12714
  const parsed = parseExternalId(externalId);
12672
12715
  if (!parsed) return (0, import_types21.Err)(new Error(`Invalid externalId format: "${externalId}"`));
12673
- const response = await this.fetchFn(
12716
+ const response = await fetchWithRetry(
12717
+ this.fetchFn,
12674
12718
  `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
12675
12719
  {
12676
12720
  method: "GET",
12677
12721
  headers: this.headers()
12678
- }
12722
+ },
12723
+ this.retryOpts
12679
12724
  );
12680
12725
  if (!response.ok) {
12681
12726
  const text = await response.text();
@@ -12700,12 +12745,14 @@ var GitHubIssuesSyncAdapter = class {
12700
12745
  let page = 1;
12701
12746
  const perPage = 100;
12702
12747
  while (true) {
12703
- const response = await this.fetchFn(
12748
+ const response = await fetchWithRetry(
12749
+ this.fetchFn,
12704
12750
  `${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
12705
12751
  {
12706
12752
  method: "GET",
12707
12753
  headers: this.headers()
12708
- }
12754
+ },
12755
+ this.retryOpts
12709
12756
  );
12710
12757
  if (!response.ok) {
12711
12758
  const text = await response.text();
@@ -12734,13 +12781,15 @@ var GitHubIssuesSyncAdapter = class {
12734
12781
  const parsed = parseExternalId(externalId);
12735
12782
  if (!parsed) return (0, import_types21.Err)(new Error(`Invalid externalId format: "${externalId}"`));
12736
12783
  const login = assignee.startsWith("@") ? assignee.slice(1) : assignee;
12737
- const response = await this.fetchFn(
12784
+ const response = await fetchWithRetry(
12785
+ this.fetchFn,
12738
12786
  `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/assignees`,
12739
12787
  {
12740
12788
  method: "POST",
12741
12789
  headers: this.headers(),
12742
12790
  body: JSON.stringify({ assignees: [login] })
12743
- }
12791
+ },
12792
+ this.retryOpts
12744
12793
  );
12745
12794
  if (!response.ok) {
12746
12795
  const text = await response.text();
package/dist/index.mjs CHANGED
@@ -10465,6 +10465,29 @@ function labelsForStatus(status, config) {
10465
10465
  }
10466
10466
  return [...base];
10467
10467
  }
10468
+ var RETRY_DEFAULTS = { maxRetries: 5, baseDelayMs: 1e3 };
10469
+ function sleep(ms) {
10470
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
10471
+ }
10472
+ async function fetchWithRetry(fetchFn, input, init, opts = RETRY_DEFAULTS) {
10473
+ let lastResponse;
10474
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
10475
+ const response = await fetchFn(input, init);
10476
+ if (response.status !== 403 && response.status !== 429) return response;
10477
+ lastResponse = response;
10478
+ if (attempt === opts.maxRetries) break;
10479
+ const retryAfter = response.headers.get("Retry-After");
10480
+ let delayMs;
10481
+ if (retryAfter) {
10482
+ const seconds = parseInt(retryAfter, 10);
10483
+ delayMs = isNaN(seconds) ? opts.baseDelayMs : seconds * 1e3;
10484
+ } else {
10485
+ delayMs = opts.baseDelayMs * Math.pow(2, attempt) + Math.random() * 500;
10486
+ }
10487
+ await sleep(delayMs);
10488
+ }
10489
+ return lastResponse;
10490
+ }
10468
10491
  var GitHubIssuesSyncAdapter = class {
10469
10492
  token;
10470
10493
  config;
@@ -10472,11 +10495,16 @@ var GitHubIssuesSyncAdapter = class {
10472
10495
  apiBase;
10473
10496
  owner;
10474
10497
  repo;
10498
+ retryOpts;
10475
10499
  constructor(options) {
10476
10500
  this.token = options.token;
10477
10501
  this.config = options.config;
10478
10502
  this.fetchFn = options.fetchFn ?? globalThis.fetch;
10479
10503
  this.apiBase = options.apiBase ?? "https://api.github.com";
10504
+ this.retryOpts = {
10505
+ maxRetries: options.maxRetries ?? RETRY_DEFAULTS.maxRetries,
10506
+ baseDelayMs: options.baseDelayMs ?? RETRY_DEFAULTS.baseDelayMs
10507
+ };
10480
10508
  const repoParts = (options.config.repo ?? "").split("/");
10481
10509
  if (repoParts.length !== 2 || !repoParts[0] || !repoParts[1]) {
10482
10510
  throw new Error(`Invalid repo format: "${options.config.repo}". Expected "owner/repo".`);
@@ -10492,6 +10520,20 @@ var GitHubIssuesSyncAdapter = class {
10492
10520
  "X-GitHub-Api-Version": "2022-11-28"
10493
10521
  };
10494
10522
  }
10523
+ /**
10524
+ * Close an issue if the feature status maps to 'closed'.
10525
+ * GitHub Issues API doesn't accept state on POST — requires a follow-up PATCH.
10526
+ */
10527
+ async closeIfDone(issueNumber, featureStatus) {
10528
+ const externalStatus = this.config.statusMap[featureStatus];
10529
+ if (externalStatus !== "closed") return;
10530
+ await fetchWithRetry(
10531
+ this.fetchFn,
10532
+ `${this.apiBase}/repos/${this.owner}/${this.repo}/issues/${issueNumber}`,
10533
+ { method: "PATCH", headers: this.headers(), body: JSON.stringify({ state: "closed" }) },
10534
+ this.retryOpts
10535
+ );
10536
+ }
10495
10537
  async createTicket(feature, milestone) {
10496
10538
  try {
10497
10539
  const labels = labelsForStatus(feature.status, this.config);
@@ -10501,17 +10543,15 @@ var GitHubIssuesSyncAdapter = class {
10501
10543
  `**Milestone:** ${milestone}`,
10502
10544
  feature.spec ? `**Spec:** ${feature.spec}` : ""
10503
10545
  ].filter(Boolean).join("\n");
10504
- const response = await this.fetchFn(
10546
+ const response = await fetchWithRetry(
10547
+ this.fetchFn,
10505
10548
  `${this.apiBase}/repos/${this.owner}/${this.repo}/issues`,
10506
10549
  {
10507
10550
  method: "POST",
10508
10551
  headers: this.headers(),
10509
- body: JSON.stringify({
10510
- title: feature.name,
10511
- body,
10512
- labels
10513
- })
10514
- }
10552
+ body: JSON.stringify({ title: feature.name, body, labels })
10553
+ },
10554
+ this.retryOpts
10515
10555
  );
10516
10556
  if (!response.ok) {
10517
10557
  const text = await response.text();
@@ -10519,6 +10559,7 @@ var GitHubIssuesSyncAdapter = class {
10519
10559
  }
10520
10560
  const data = await response.json();
10521
10561
  const externalId = buildExternalId(this.owner, this.repo, data.number);
10562
+ await this.closeIfDone(data.number, feature.status);
10522
10563
  return Ok4({ externalId, url: data.html_url });
10523
10564
  } catch (error) {
10524
10565
  return Err3(error instanceof Error ? error : new Error(String(error)));
@@ -10539,13 +10580,15 @@ var GitHubIssuesSyncAdapter = class {
10539
10580
  patch.state = externalStatus;
10540
10581
  patch.labels = labelsForStatus(changes.status, this.config);
10541
10582
  }
10542
- const response = await this.fetchFn(
10583
+ const response = await fetchWithRetry(
10584
+ this.fetchFn,
10543
10585
  `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
10544
10586
  {
10545
10587
  method: "PATCH",
10546
10588
  headers: this.headers(),
10547
10589
  body: JSON.stringify(patch)
10548
- }
10590
+ },
10591
+ this.retryOpts
10549
10592
  );
10550
10593
  if (!response.ok) {
10551
10594
  const text = await response.text();
@@ -10561,12 +10604,14 @@ var GitHubIssuesSyncAdapter = class {
10561
10604
  try {
10562
10605
  const parsed = parseExternalId(externalId);
10563
10606
  if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
10564
- const response = await this.fetchFn(
10607
+ const response = await fetchWithRetry(
10608
+ this.fetchFn,
10565
10609
  `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}`,
10566
10610
  {
10567
10611
  method: "GET",
10568
10612
  headers: this.headers()
10569
- }
10613
+ },
10614
+ this.retryOpts
10570
10615
  );
10571
10616
  if (!response.ok) {
10572
10617
  const text = await response.text();
@@ -10591,12 +10636,14 @@ var GitHubIssuesSyncAdapter = class {
10591
10636
  let page = 1;
10592
10637
  const perPage = 100;
10593
10638
  while (true) {
10594
- const response = await this.fetchFn(
10639
+ const response = await fetchWithRetry(
10640
+ this.fetchFn,
10595
10641
  `${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
10596
10642
  {
10597
10643
  method: "GET",
10598
10644
  headers: this.headers()
10599
- }
10645
+ },
10646
+ this.retryOpts
10600
10647
  );
10601
10648
  if (!response.ok) {
10602
10649
  const text = await response.text();
@@ -10625,13 +10672,15 @@ var GitHubIssuesSyncAdapter = class {
10625
10672
  const parsed = parseExternalId(externalId);
10626
10673
  if (!parsed) return Err3(new Error(`Invalid externalId format: "${externalId}"`));
10627
10674
  const login = assignee.startsWith("@") ? assignee.slice(1) : assignee;
10628
- const response = await this.fetchFn(
10675
+ const response = await fetchWithRetry(
10676
+ this.fetchFn,
10629
10677
  `${this.apiBase}/repos/${parsed.owner}/${parsed.repo}/issues/${parsed.number}/assignees`,
10630
10678
  {
10631
10679
  method: "POST",
10632
10680
  headers: this.headers(),
10633
10681
  body: JSON.stringify({ assignees: [login] })
10634
- }
10682
+ },
10683
+ this.retryOpts
10635
10684
  );
10636
10685
  if (!response.ok) {
10637
10686
  const text = await response.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/core",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Core library for Harness Engineering toolkit",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",