paperclip-github-plugin 0.2.1 → 0.2.3

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/worker.js CHANGED
@@ -515,6 +515,10 @@ var SYNC_STATE_SCOPE = {
515
515
  scopeKind: "instance",
516
516
  stateKey: "paperclip-github-plugin-last-sync"
517
517
  };
518
+ var SYNC_CANCELLATION_SCOPE = {
519
+ scopeKind: "instance",
520
+ stateKey: "paperclip-github-plugin-sync-cancel-request"
521
+ };
518
522
  var IMPORT_REGISTRY_SCOPE = {
519
523
  scopeKind: "instance",
520
524
  stateKey: "paperclip-github-plugin-import-registry"
@@ -527,7 +531,9 @@ var DEFAULT_PAPERCLIP_LABEL_COLOR = "#6366f1";
527
531
  var PAPERCLIP_LABEL_PAGE_SIZE = 100;
528
532
  var MANUAL_SYNC_RESPONSE_GRACE_PERIOD_MS = 500;
529
533
  var RUNNING_SYNC_MESSAGE = "GitHub sync is running in the background. This page will update when it finishes.";
534
+ var CANCELLING_SYNC_MESSAGE = "Cancellation requested. GitHub sync will stop after the current step finishes.";
530
535
  var SYNC_PROGRESS_PERSIST_INTERVAL_MS = 250;
536
+ var MAX_SYNC_FAILURE_LOG_ENTRIES = 25;
531
537
  var GITHUB_SECONDARY_RATE_LIMIT_FALLBACK_MS = 6e4;
532
538
  var MISSING_GITHUB_TOKEN_SYNC_MESSAGE = "Configure a GitHub token before running sync.";
533
539
  var MISSING_GITHUB_TOKEN_SYNC_ACTION = 'Open settings and save a GitHub token secret, or create ~/.paperclip/plugins/github-sync/config.json with a "githubToken" value, and then run sync again.';
@@ -577,6 +583,14 @@ var PaperclipLabelSyncError = class extends Error {
577
583
  this.labelNames = labelNames;
578
584
  }
579
585
  };
586
+ var SyncCancellationError = class extends Error {
587
+ name = "SyncCancellationError";
588
+ requestedAt;
589
+ constructor(requestedAt) {
590
+ super(CANCELLING_SYNC_MESSAGE);
591
+ this.requestedAt = requestedAt;
592
+ }
593
+ };
580
594
  var SUCCESSFUL_CHECK_RUN_CONCLUSIONS = /* @__PURE__ */ new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
581
595
  var FAILED_CHECK_RUN_CONCLUSIONS = /* @__PURE__ */ new Set([
582
596
  "ACTION_REQUIRED",
@@ -884,6 +898,20 @@ function createIdleSyncState() {
884
898
  status: "idle"
885
899
  };
886
900
  }
901
+ function createCancelledSyncState(params) {
902
+ const { message, trigger, syncedIssuesCount, createdIssuesCount, skippedIssuesCount, erroredIssuesCount, progress } = params;
903
+ return {
904
+ status: "cancelled",
905
+ message,
906
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
907
+ syncedIssuesCount,
908
+ createdIssuesCount,
909
+ skippedIssuesCount,
910
+ erroredIssuesCount,
911
+ lastRunTrigger: trigger,
912
+ ...progress ? { progress: normalizeSyncProgress(progress) } : {}
913
+ };
914
+ }
887
915
  function formatGitHubIssueCountLabel(count) {
888
916
  const normalizedCount = Math.max(0, Math.floor(count));
889
917
  return `${normalizedCount} GitHub ${normalizedCount === 1 ? "issue" : "issues"}`;
@@ -1047,6 +1075,7 @@ function normalizeSyncConfigurationIssue(value) {
1047
1075
  switch (value) {
1048
1076
  case "missing_token":
1049
1077
  case "missing_mapping":
1078
+ case "missing_board_access":
1050
1079
  return value;
1051
1080
  default:
1052
1081
  return void 0;
@@ -1120,6 +1149,30 @@ function normalizeSyncErrorDetails(value) {
1120
1149
  ...rateLimitResource ? { rateLimitResource } : {}
1121
1150
  };
1122
1151
  }
1152
+ function normalizeSyncFailureLogEntry(value) {
1153
+ if (!value || typeof value !== "object") {
1154
+ return void 0;
1155
+ }
1156
+ const record = value;
1157
+ const details = normalizeSyncErrorDetails(record);
1158
+ const message = typeof record.message === "string" && record.message.trim() ? record.message.trim() : void 0;
1159
+ const occurredAt = typeof record.occurredAt === "string" && record.occurredAt.trim() ? record.occurredAt.trim() : void 0;
1160
+ if (!message || !occurredAt) {
1161
+ return void 0;
1162
+ }
1163
+ return {
1164
+ message,
1165
+ occurredAt,
1166
+ ...details ?? {}
1167
+ };
1168
+ }
1169
+ function normalizeSyncFailureLogEntries(value) {
1170
+ if (!Array.isArray(value)) {
1171
+ return void 0;
1172
+ }
1173
+ const entries = value.map((entry) => normalizeSyncFailureLogEntry(entry)).filter((entry) => entry !== void 0).slice(-MAX_SYNC_FAILURE_LOG_ENTRIES);
1174
+ return entries.length > 0 ? entries : void 0;
1175
+ }
1123
1176
  function formatSyncFailurePhase(phase) {
1124
1177
  switch (phase) {
1125
1178
  case "configuration":
@@ -1274,8 +1327,49 @@ function buildSyncErrorDetails(error, context) {
1274
1327
  ...rateLimitPause?.resource ? { rateLimitResource: rateLimitPause.resource } : {}
1275
1328
  };
1276
1329
  }
1330
+ function createSyncFailureLogEntry(params) {
1331
+ const message = params.message.trim();
1332
+ const occurredAt = typeof params.occurredAt === "string" && params.occurredAt.trim() ? params.occurredAt.trim() : (/* @__PURE__ */ new Date()).toISOString();
1333
+ const errorDetails = normalizeSyncErrorDetails(params.errorDetails);
1334
+ if (!message) {
1335
+ return void 0;
1336
+ }
1337
+ return {
1338
+ message,
1339
+ occurredAt,
1340
+ ...errorDetails ?? {}
1341
+ };
1342
+ }
1343
+ function buildSyncFailureLogEntry(error, context, occurredAt) {
1344
+ return createSyncFailureLogEntry({
1345
+ message: buildSyncFailureMessage(error, context),
1346
+ occurredAt,
1347
+ errorDetails: buildSyncErrorDetails(error, context)
1348
+ });
1349
+ }
1350
+ function buildRecentSyncFailureLogEntries(failures) {
1351
+ const entries = failures.slice(-MAX_SYNC_FAILURE_LOG_ENTRIES).map((failure) => buildSyncFailureLogEntry(failure.error, failure.context, failure.occurredAt)).filter((entry) => entry !== void 0);
1352
+ return entries.length > 0 ? entries : void 0;
1353
+ }
1354
+ function appendRecentSyncFailureLogEntry(entries, entry) {
1355
+ if (!entry) {
1356
+ return entries;
1357
+ }
1358
+ return [...entries ?? [], entry].slice(-MAX_SYNC_FAILURE_LOG_ENTRIES);
1359
+ }
1277
1360
  function createErrorSyncState(params) {
1278
- const { message, trigger, syncedIssuesCount, createdIssuesCount, skippedIssuesCount, erroredIssuesCount, progress, errorDetails } = params;
1361
+ const {
1362
+ message,
1363
+ trigger,
1364
+ syncedIssuesCount,
1365
+ createdIssuesCount,
1366
+ skippedIssuesCount,
1367
+ erroredIssuesCount,
1368
+ progress,
1369
+ errorDetails,
1370
+ recentFailures
1371
+ } = params;
1372
+ const normalizedRecentFailures = normalizeSyncFailureLogEntries(recentFailures);
1279
1373
  return {
1280
1374
  status: "error",
1281
1375
  message,
@@ -1286,20 +1380,27 @@ function createErrorSyncState(params) {
1286
1380
  erroredIssuesCount,
1287
1381
  lastRunTrigger: trigger,
1288
1382
  ...progress ? { progress: normalizeSyncProgress(progress) } : {},
1289
- ...errorDetails ? { errorDetails } : {}
1383
+ ...errorDetails ? { errorDetails } : {},
1384
+ ...normalizedRecentFailures ? { recentFailures: normalizedRecentFailures } : {}
1290
1385
  };
1291
1386
  }
1292
1387
  function createRunningSyncState(previous, trigger, options = {}) {
1388
+ const previousRunningState = previous.status === "running" ? previous : void 0;
1389
+ const nextMessage = options.message ?? previousRunningState?.message ?? RUNNING_SYNC_MESSAGE;
1390
+ const nextCancelRequestedAt = options.cancelRequestedAt ?? previousRunningState?.cancelRequestedAt;
1391
+ const normalizedRecentFailures = normalizeSyncFailureLogEntries(options.recentFailures);
1293
1392
  return {
1294
1393
  status: "running",
1295
- message: options.message ?? RUNNING_SYNC_MESSAGE,
1394
+ message: nextMessage,
1296
1395
  checkedAt: previous.checkedAt,
1297
1396
  syncedIssuesCount: options.syncedIssuesCount ?? 0,
1298
1397
  createdIssuesCount: options.createdIssuesCount ?? 0,
1299
1398
  skippedIssuesCount: options.skippedIssuesCount ?? 0,
1300
1399
  erroredIssuesCount: options.erroredIssuesCount ?? 0,
1301
1400
  lastRunTrigger: trigger,
1302
- ...options.progress ? { progress: normalizeSyncProgress(options.progress) } : {}
1401
+ ...nextCancelRequestedAt ? { cancelRequestedAt: nextCancelRequestedAt } : {},
1402
+ ...options.progress ? { progress: normalizeSyncProgress(options.progress) } : {},
1403
+ ...normalizedRecentFailures ? { recentFailures: normalizedRecentFailures } : {}
1303
1404
  };
1304
1405
  }
1305
1406
  function getSyncableMappings(mappings) {
@@ -1785,7 +1886,16 @@ function createSetupConfigurationErrorSyncState(issue, trigger) {
1785
1886
  phase: "configuration",
1786
1887
  configurationIssue: "missing_token",
1787
1888
  suggestedAction: MISSING_GITHUB_TOKEN_SYNC_ACTION
1788
- }
1889
+ },
1890
+ recentFailures: [
1891
+ {
1892
+ message: MISSING_GITHUB_TOKEN_SYNC_MESSAGE,
1893
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
1894
+ phase: "configuration",
1895
+ configurationIssue: "missing_token",
1896
+ suggestedAction: MISSING_GITHUB_TOKEN_SYNC_ACTION
1897
+ }
1898
+ ]
1789
1899
  });
1790
1900
  case "missing_mapping":
1791
1901
  return createErrorSyncState({
@@ -1799,7 +1909,16 @@ function createSetupConfigurationErrorSyncState(issue, trigger) {
1799
1909
  phase: "configuration",
1800
1910
  configurationIssue: "missing_mapping",
1801
1911
  suggestedAction: MISSING_MAPPING_SYNC_ACTION
1802
- }
1912
+ },
1913
+ recentFailures: [
1914
+ {
1915
+ message: MISSING_MAPPING_SYNC_MESSAGE,
1916
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
1917
+ phase: "configuration",
1918
+ configurationIssue: "missing_mapping",
1919
+ suggestedAction: MISSING_MAPPING_SYNC_ACTION
1920
+ }
1921
+ ]
1803
1922
  });
1804
1923
  case "missing_board_access":
1805
1924
  return createErrorSyncState({
@@ -1813,7 +1932,16 @@ function createSetupConfigurationErrorSyncState(issue, trigger) {
1813
1932
  phase: "configuration",
1814
1933
  configurationIssue: "missing_board_access",
1815
1934
  suggestedAction: MISSING_BOARD_ACCESS_SYNC_ACTION
1816
- }
1935
+ },
1936
+ recentFailures: [
1937
+ {
1938
+ message: MISSING_BOARD_ACCESS_SYNC_MESSAGE,
1939
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
1940
+ phase: "configuration",
1941
+ configurationIssue: "missing_board_access",
1942
+ suggestedAction: MISSING_BOARD_ACCESS_SYNC_ACTION
1943
+ }
1944
+ ]
1817
1945
  });
1818
1946
  }
1819
1947
  }
@@ -1826,6 +1954,25 @@ async function saveSettingsSyncState(ctx, settings, syncState) {
1826
1954
  await ctx.state.set(SYNC_STATE_SCOPE, next.syncState);
1827
1955
  return next;
1828
1956
  }
1957
+ async function setSyncCancellationRequest(ctx, request) {
1958
+ await ctx.state.set(SYNC_CANCELLATION_SCOPE, request);
1959
+ }
1960
+ async function getSyncCancellationRequest(ctx) {
1961
+ const activeRequestedAt = activeRunningSyncState?.syncState.cancelRequestedAt?.trim();
1962
+ if (activeRunningSyncState?.syncState.status === "running" && activeRequestedAt) {
1963
+ return {
1964
+ requestedAt: activeRequestedAt
1965
+ };
1966
+ }
1967
+ return normalizeSyncCancellationRequest(await ctx.state.get(SYNC_CANCELLATION_SCOPE));
1968
+ }
1969
+ function buildCancelledSyncMessage(target, progress) {
1970
+ const completedIssueCount = typeof progress?.completedIssueCount === "number" ? Math.max(0, progress.completedIssueCount) : void 0;
1971
+ const totalIssueCount = typeof progress?.totalIssueCount === "number" ? Math.max(0, progress.totalIssueCount) : void 0;
1972
+ const scopeLabel = target ? `GitHub sync for ${target.displayLabel}` : "GitHub sync";
1973
+ const completionSummary = completedIssueCount !== void 0 && totalIssueCount !== void 0 ? ` Completed ${Math.min(completedIssueCount, totalIssueCount)} of ${totalIssueCount} issues before stopping.` : "";
1974
+ return `${scopeLabel} was cancelled before it finished.${completionSummary}`;
1975
+ }
1829
1976
  async function createUnexpectedSyncErrorResult(ctx, trigger, error) {
1830
1977
  const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
1831
1978
  const errorDetails = buildSyncErrorDetails(error, {
@@ -1844,7 +1991,14 @@ async function createUnexpectedSyncErrorResult(ctx, trigger, error) {
1844
1991
  createdIssuesCount: settings.syncState.createdIssuesCount ?? 0,
1845
1992
  skippedIssuesCount: settings.syncState.skippedIssuesCount ?? 0,
1846
1993
  erroredIssuesCount: 0,
1847
- errorDetails
1994
+ errorDetails,
1995
+ recentFailures: appendRecentSyncFailureLogEntry(
1996
+ void 0,
1997
+ createSyncFailureLogEntry({
1998
+ message,
1999
+ errorDetails
2000
+ })
2001
+ )
1848
2002
  })
1849
2003
  );
1850
2004
  }
@@ -1892,7 +2046,8 @@ function recordRecoverableSyncFailure(ctx, failures, error, context) {
1892
2046
  const snapshot = cloneSyncFailureContext(context);
1893
2047
  failures.push({
1894
2048
  error,
1895
- context: snapshot
2049
+ context: snapshot,
2050
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
1896
2051
  });
1897
2052
  ctx.logger.warn("GitHub sync skipped a failed item and continued.", {
1898
2053
  phase: snapshot.phase,
@@ -1998,6 +2153,13 @@ function normalizePaperclipBoardApiTokenRefs(value) {
1998
2153
  }
1999
2154
  return Object.fromEntries(entries);
2000
2155
  }
2156
+ function normalizeSyncCancellationRequest(value) {
2157
+ if (!value || typeof value !== "object") {
2158
+ return null;
2159
+ }
2160
+ const requestedAt = typeof value.requestedAt === "string" ? value.requestedAt.trim() : "";
2161
+ return requestedAt ? { requestedAt } : null;
2162
+ }
2001
2163
  function normalizeSyncState(value) {
2002
2164
  if (!value || typeof value !== "object") {
2003
2165
  return DEFAULT_SETTINGS.syncState;
@@ -2007,8 +2169,9 @@ function normalizeSyncState(value) {
2007
2169
  const lastRunTrigger = record.lastRunTrigger;
2008
2170
  const progress = normalizeSyncProgress(record.progress);
2009
2171
  const errorDetails = normalizeSyncErrorDetails(record.errorDetails);
2172
+ const recentFailures = normalizeSyncFailureLogEntries(record.recentFailures);
2010
2173
  return {
2011
- status: status === "running" || status === "success" || status === "error" ? status : "idle",
2174
+ status: status === "running" || status === "success" || status === "error" || status === "cancelled" ? status : "idle",
2012
2175
  message: typeof record.message === "string" ? record.message : void 0,
2013
2176
  checkedAt: typeof record.checkedAt === "string" ? record.checkedAt : void 0,
2014
2177
  syncedIssuesCount: typeof record.syncedIssuesCount === "number" ? record.syncedIssuesCount : void 0,
@@ -2016,8 +2179,10 @@ function normalizeSyncState(value) {
2016
2179
  skippedIssuesCount: typeof record.skippedIssuesCount === "number" ? record.skippedIssuesCount : void 0,
2017
2180
  erroredIssuesCount: typeof record.erroredIssuesCount === "number" ? record.erroredIssuesCount : void 0,
2018
2181
  lastRunTrigger: lastRunTrigger === "manual" || lastRunTrigger === "schedule" || lastRunTrigger === "retry" ? lastRunTrigger : void 0,
2182
+ cancelRequestedAt: typeof record.cancelRequestedAt === "string" ? record.cancelRequestedAt : void 0,
2019
2183
  ...progress ? { progress } : {},
2020
- ...errorDetails ? { errorDetails } : {}
2184
+ ...errorDetails ? { errorDetails } : {},
2185
+ ...recentFailures ? { recentFailures } : {}
2021
2186
  };
2022
2187
  }
2023
2188
  function normalizeMappings(value) {
@@ -4318,25 +4483,30 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
4318
4483
  createdIssueDescription = createdIssue.description;
4319
4484
  createPath = "sdk";
4320
4485
  }
4486
+ const ensuredCreatedIssueId = createdIssueId;
4487
+ if (!ensuredCreatedIssueId) {
4488
+ throw new Error("GitHub sync could not resolve the created Paperclip issue id.");
4489
+ }
4490
+ const normalizedCreatedIssueDescription = createdIssueDescription ?? void 0;
4321
4491
  if (createPath !== "sdk") {
4322
4492
  await applyDefaultAssigneeToPaperclipIssue(ctx, {
4323
4493
  companyId: mapping.companyId,
4324
- issueId: createdIssueId,
4494
+ issueId: ensuredCreatedIssueId,
4325
4495
  defaultAssigneeAgentId: advancedSettings.defaultAssigneeAgentId
4326
4496
  });
4327
4497
  }
4328
- if (normalizeIssueDescriptionValue(createdIssueDescription) !== description) {
4498
+ if (normalizeIssueDescriptionValue(normalizedCreatedIssueDescription) !== description) {
4329
4499
  logIssueDescriptionDiagnostic(
4330
4500
  ctx,
4331
4501
  "warn",
4332
4502
  "GitHub sync detected a missing or mismatched Paperclip issue description immediately after issue creation.",
4333
4503
  {
4334
4504
  companyId: mapping.companyId,
4335
- issueId: createdIssueId,
4505
+ issueId: ensuredCreatedIssueId,
4336
4506
  paperclipApiBaseUrl,
4337
4507
  githubIssue: issue,
4338
4508
  linkedPullRequestNumbers: [],
4339
- currentDescription: createdIssueDescription,
4509
+ currentDescription: normalizedCreatedIssueDescription,
4340
4510
  nextDescription: description,
4341
4511
  reason: "create_response_mismatch",
4342
4512
  createPath
@@ -4346,8 +4516,8 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
4346
4516
  ctx,
4347
4517
  {
4348
4518
  companyId: mapping.companyId,
4349
- issueId: createdIssueId,
4350
- currentDescription: createdIssueDescription,
4519
+ issueId: ensuredCreatedIssueId,
4520
+ currentDescription: normalizedCreatedIssueDescription,
4351
4521
  githubIssue: issue,
4352
4522
  linkedPullRequestNumbers: [],
4353
4523
  paperclipApiBaseUrl,
@@ -4355,7 +4525,7 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
4355
4525
  }
4356
4526
  );
4357
4527
  }
4358
- await upsertGitHubIssueLinkRecord(ctx, mapping, createdIssueId, issue, []);
4528
+ await upsertGitHubIssueLinkRecord(ctx, mapping, ensuredCreatedIssueId, issue, []);
4359
4529
  if (syncFailureContext) {
4360
4530
  updateSyncFailureContext(syncFailureContext, {
4361
4531
  phase: "syncing_labels",
@@ -4373,11 +4543,11 @@ async function createPaperclipIssue(ctx, mapping, advancedSettings, issue, avail
4373
4543
  await applyPaperclipLabelsToIssue(
4374
4544
  ctx,
4375
4545
  mapping.companyId,
4376
- createdIssueId,
4546
+ ensuredCreatedIssueId,
4377
4547
  labelResolution.labels
4378
4548
  );
4379
4549
  return {
4380
- id: createdIssueId,
4550
+ id: ensuredCreatedIssueId,
4381
4551
  unresolvedGitHubLabels: labelResolution.unresolvedGitHubLabels,
4382
4552
  ...labelResolution.failure ? { labelResolutionFailure: labelResolution.failure } : {}
4383
4553
  };
@@ -4449,7 +4619,7 @@ async function ensurePaperclipIssueImported(ctx, mapping, advancedSettings, issu
4449
4619
  }
4450
4620
  return createdIssue.id;
4451
4621
  }
4452
- async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mapping, advancedSettings, allIssuesById, importedIssues, createdIssueIds, availableLabels, paperclipApiBaseUrl, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache, repositoryMaintainerCache, syncFailureContext, failures, onProgress) {
4622
+ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mapping, advancedSettings, allIssuesById, importedIssues, createdIssueIds, availableLabels, paperclipApiBaseUrl, linkedPullRequestsByIssueNumber, issueStatusSnapshotCache, pullRequestStatusCache, repositoryMaintainerCache, syncFailureContext, failures, assertNotCancelled, onProgress) {
4453
4623
  if (!mapping.companyId || !ctx.issues || typeof ctx.issues.get !== "function" || typeof ctx.issues.update !== "function") {
4454
4624
  return {
4455
4625
  updatedStatusesCount: 0,
@@ -4463,6 +4633,9 @@ async function synchronizePaperclipIssueStatuses(ctx, octokit, repository, mappi
4463
4633
  let completedIssueCount = 0;
4464
4634
  const totalIssueCount = importedIssues.length;
4465
4635
  for (const importedIssue of importedIssues) {
4636
+ if (assertNotCancelled) {
4637
+ await assertNotCancelled();
4638
+ }
4466
4639
  const githubIssue = allIssuesById.get(importedIssue.githubIssueId);
4467
4640
  try {
4468
4641
  if (!githubIssue) {
@@ -5128,6 +5301,7 @@ async function performSync(ctx, trigger, options = {}) {
5128
5301
  const config = await getResolvedConfig(ctx);
5129
5302
  const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
5130
5303
  const token = typeof options.resolvedToken === "string" ? options.resolvedToken : await resolveGithubToken(ctx);
5304
+ const paperclipApiBaseUrl = getConfiguredPaperclipApiBaseUrl(settings, config);
5131
5305
  const mappings = getSyncableMappingsForTarget(settings.mappings, options.target);
5132
5306
  activePaperclipApiAuthTokensByCompanyId = null;
5133
5307
  const failureContext = {
@@ -5152,7 +5326,7 @@ async function performSync(ctx, trigger, options = {}) {
5152
5326
  return next;
5153
5327
  }
5154
5328
  const mappingsMissingBoardAccess = getMappingsMissingPaperclipBoardAccess(settings, config, mappings);
5155
- if (mappingsMissingBoardAccess.length > 0 && await detectPaperclipBoardAccessRequirement(settings.paperclipApiBaseUrl)) {
5329
+ if (mappingsMissingBoardAccess.length > 0 && await detectPaperclipBoardAccessRequirement(paperclipApiBaseUrl)) {
5156
5330
  const next = {
5157
5331
  ...settings,
5158
5332
  syncState: createSetupConfigurationErrorSyncState("missing_board_access", trigger)
@@ -5162,6 +5336,10 @@ async function performSync(ctx, trigger, options = {}) {
5162
5336
  return next;
5163
5337
  }
5164
5338
  if (!ctx.issues || typeof ctx.issues.create !== "function") {
5339
+ const errorDetails = {
5340
+ phase: "configuration",
5341
+ suggestedAction: "Update Paperclip to a runtime that supports plugin issue creation, then retry sync."
5342
+ };
5165
5343
  const next = {
5166
5344
  ...settings,
5167
5345
  syncState: createErrorSyncState({
@@ -5171,10 +5349,14 @@ async function performSync(ctx, trigger, options = {}) {
5171
5349
  createdIssuesCount: 0,
5172
5350
  skippedIssuesCount: 0,
5173
5351
  erroredIssuesCount: 0,
5174
- errorDetails: {
5175
- phase: "configuration",
5176
- suggestedAction: "Update Paperclip to a runtime that supports plugin issue creation, then retry sync."
5177
- }
5352
+ errorDetails,
5353
+ recentFailures: appendRecentSyncFailureLogEntry(
5354
+ void 0,
5355
+ createSyncFailureLogEntry({
5356
+ message: "This Paperclip runtime does not expose plugin issue creation yet.",
5357
+ errorDetails
5358
+ })
5359
+ )
5178
5360
  })
5179
5361
  };
5180
5362
  await ctx.state.set(SETTINGS_SCOPE, next);
@@ -5210,14 +5392,23 @@ async function performSync(ctx, trigger, options = {}) {
5210
5392
  erroredIssuesCount: recoverableFailures.length,
5211
5393
  progress: currentProgress
5212
5394
  });
5395
+ async function throwIfSyncCancelled() {
5396
+ const cancellationRequest = await getSyncCancellationRequest(ctx);
5397
+ if (!cancellationRequest) {
5398
+ return;
5399
+ }
5400
+ throw new SyncCancellationError(cancellationRequest.requestedAt);
5401
+ }
5213
5402
  async function persistRunningProgress(force = false) {
5214
5403
  const progress = normalizeSyncProgress(currentProgress);
5404
+ const recentFailures = buildRecentSyncFailureLogEntries(recoverableFailures);
5215
5405
  const signature = JSON.stringify({
5216
5406
  syncedIssuesCount,
5217
5407
  createdIssuesCount,
5218
5408
  skippedIssuesCount,
5219
5409
  erroredIssuesCount: recoverableFailures.length,
5220
- progress
5410
+ progress,
5411
+ recentFailures
5221
5412
  });
5222
5413
  const now = Date.now();
5223
5414
  if (!force) {
@@ -5236,7 +5427,8 @@ async function performSync(ctx, trigger, options = {}) {
5236
5427
  createdIssuesCount,
5237
5428
  skippedIssuesCount,
5238
5429
  erroredIssuesCount: recoverableFailures.length,
5239
- progress
5430
+ progress,
5431
+ recentFailures
5240
5432
  })
5241
5433
  );
5242
5434
  activeRunningSyncState = currentSettings;
@@ -5253,7 +5445,9 @@ async function performSync(ctx, trigger, options = {}) {
5253
5445
  }
5254
5446
  const repositoryPlans = [];
5255
5447
  try {
5448
+ await throwIfSyncCancelled();
5256
5449
  for (const [mappingIndex, mapping] of mappings.entries()) {
5450
+ await throwIfSyncCancelled();
5257
5451
  try {
5258
5452
  const repository = requireRepositoryReference(mapping.repositoryUrl);
5259
5453
  const importedIssueRecords = nextRegistry.filter((entry) => doesImportedIssueRecordMatchMapping(entry, mapping)).filter((entry) => doesImportedIssueMatchTarget(entry, options.target));
@@ -5276,7 +5470,7 @@ async function performSync(ctx, trigger, options = {}) {
5276
5470
  updateSyncFailureContext(failureContext, {
5277
5471
  phase: "loading_paperclip_labels"
5278
5472
  });
5279
- availableLabels = supportsPaperclipLabelMapping && companyId ? await buildPaperclipLabelDirectory(ctx, companyId, settings.paperclipApiBaseUrl) : /* @__PURE__ */ new Map();
5473
+ availableLabels = supportsPaperclipLabelMapping && companyId ? await buildPaperclipLabelDirectory(ctx, companyId, paperclipApiBaseUrl) : /* @__PURE__ */ new Map();
5280
5474
  if (companyId) {
5281
5475
  companyLabelDirectoryCache.set(companyId, availableLabels);
5282
5476
  }
@@ -5341,7 +5535,7 @@ async function performSync(ctx, trigger, options = {}) {
5341
5535
  trackedIssueCount
5342
5536
  });
5343
5537
  } catch (error) {
5344
- if (isGitHubRateLimitError(error)) {
5538
+ if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
5345
5539
  throw error;
5346
5540
  }
5347
5541
  recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
@@ -5370,6 +5564,7 @@ async function performSync(ctx, trigger, options = {}) {
5370
5564
  }
5371
5565
  await persistRunningProgress(true);
5372
5566
  for (const plan of repositoryPlans) {
5567
+ await throwIfSyncCancelled();
5373
5568
  try {
5374
5569
  const { mapping, advancedSettings, repository, repositoryIndex, allIssuesById, issues } = plan;
5375
5570
  const companyId = mapping.companyId;
@@ -5380,7 +5575,7 @@ async function performSync(ctx, trigger, options = {}) {
5380
5575
  repositoryUrl: repository.url,
5381
5576
  githubIssueNumber: void 0
5382
5577
  });
5383
- availableLabels = supportsPaperclipLabelMapping && companyId ? await buildPaperclipLabelDirectory(ctx, companyId, settings.paperclipApiBaseUrl) : /* @__PURE__ */ new Map();
5578
+ availableLabels = supportsPaperclipLabelMapping && companyId ? await buildPaperclipLabelDirectory(ctx, companyId, paperclipApiBaseUrl) : /* @__PURE__ */ new Map();
5384
5579
  if (companyId) {
5385
5580
  companyLabelDirectoryCache.set(companyId, availableLabels);
5386
5581
  }
@@ -5424,8 +5619,9 @@ async function performSync(ctx, trigger, options = {}) {
5424
5619
  openLinkedPullRequestNumbers,
5425
5620
  pullRequestStatusCache
5426
5621
  );
5622
+ await throwIfSyncCancelled();
5427
5623
  } catch (error) {
5428
- if (isGitHubRateLimitError(error)) {
5624
+ if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
5429
5625
  throw error;
5430
5626
  }
5431
5627
  }
@@ -5440,6 +5636,7 @@ async function performSync(ctx, trigger, options = {}) {
5440
5636
  };
5441
5637
  await persistRunningProgress(true);
5442
5638
  for (const [issueIndex, issue] of issues.entries()) {
5639
+ await throwIfSyncCancelled();
5443
5640
  const createdIssueCountBefore = createdIssueIds.size;
5444
5641
  const skippedIssueCountBefore = skippedIssueIds.size;
5445
5642
  try {
@@ -5449,7 +5646,7 @@ async function performSync(ctx, trigger, options = {}) {
5449
5646
  advancedSettings,
5450
5647
  issue,
5451
5648
  availableLabels,
5452
- settings.paperclipApiBaseUrl,
5649
+ paperclipApiBaseUrl,
5453
5650
  importRegistryByIssueId,
5454
5651
  existingImportedPaperclipIssuesByUrl,
5455
5652
  nextRegistry,
@@ -5488,6 +5685,7 @@ async function performSync(ctx, trigger, options = {}) {
5488
5685
  totalIssueCount: totalTrackedIssueCount
5489
5686
  };
5490
5687
  await persistRunningProgress(true);
5688
+ await throwIfSyncCancelled();
5491
5689
  const synchronizationResult = await synchronizePaperclipIssueStatuses(
5492
5690
  ctx,
5493
5691
  octokit,
@@ -5498,13 +5696,14 @@ async function performSync(ctx, trigger, options = {}) {
5498
5696
  importedIssuesForSynchronization,
5499
5697
  createdIssueIds,
5500
5698
  availableLabels,
5501
- settings.paperclipApiBaseUrl,
5699
+ paperclipApiBaseUrl,
5502
5700
  linkedPullRequestsByIssueNumber,
5503
5701
  issueStatusSnapshotCache,
5504
5702
  pullRequestStatusCache,
5505
5703
  repositoryMaintainerCache,
5506
5704
  failureContext,
5507
5705
  recoverableFailures,
5706
+ throwIfSyncCancelled,
5508
5707
  async (progress) => {
5509
5708
  markTrackedIssueProcessed(mapping, progress.githubIssueId);
5510
5709
  currentProgress = {
@@ -5523,7 +5722,7 @@ async function performSync(ctx, trigger, options = {}) {
5523
5722
  updatedLabelsCount += synchronizationResult.updatedLabelsCount;
5524
5723
  updatedDescriptionsCount += synchronizationResult.updatedDescriptionsCount;
5525
5724
  } catch (error) {
5526
- if (isGitHubRateLimitError(error)) {
5725
+ if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
5527
5726
  throw error;
5528
5727
  }
5529
5728
  recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
@@ -5547,7 +5746,8 @@ async function performSync(ctx, trigger, options = {}) {
5547
5746
  skippedIssuesCount,
5548
5747
  erroredIssuesCount: recoverableFailures.length,
5549
5748
  progress: currentProgress,
5550
- errorDetails
5749
+ errorDetails,
5750
+ recentFailures: buildRecentSyncFailureLogEntries(recoverableFailures)
5551
5751
  })
5552
5752
  };
5553
5753
  await ctx.state.set(SETTINGS_SCOPE, next2);
@@ -5573,6 +5773,24 @@ async function performSync(ctx, trigger, options = {}) {
5573
5773
  await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
5574
5774
  return next;
5575
5775
  } catch (error) {
5776
+ if (error instanceof SyncCancellationError) {
5777
+ const next2 = {
5778
+ ...currentSettings,
5779
+ syncState: createCancelledSyncState({
5780
+ message: buildCancelledSyncMessage(options.target, currentProgress),
5781
+ trigger,
5782
+ syncedIssuesCount,
5783
+ createdIssuesCount,
5784
+ skippedIssuesCount,
5785
+ erroredIssuesCount: recoverableFailures.length,
5786
+ progress: currentProgress
5787
+ })
5788
+ };
5789
+ await ctx.state.set(SETTINGS_SCOPE, next2);
5790
+ await ctx.state.set(SYNC_STATE_SCOPE, next2.syncState);
5791
+ await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
5792
+ return next2;
5793
+ }
5576
5794
  const errorDetails = buildSyncErrorDetails(error, failureContext);
5577
5795
  const next = {
5578
5796
  ...currentSettings,
@@ -5584,7 +5802,11 @@ async function performSync(ctx, trigger, options = {}) {
5584
5802
  skippedIssuesCount,
5585
5803
  erroredIssuesCount: recoverableFailures.length,
5586
5804
  progress: currentProgress,
5587
- errorDetails
5805
+ errorDetails,
5806
+ recentFailures: appendRecentSyncFailureLogEntry(
5807
+ buildRecentSyncFailureLogEntries(recoverableFailures),
5808
+ buildSyncFailureLogEntry(error, failureContext)
5809
+ )
5588
5810
  })
5589
5811
  };
5590
5812
  await ctx.state.set(SETTINGS_SCOPE, next);
@@ -5638,6 +5860,7 @@ async function startSync(ctx, trigger, options = {}) {
5638
5860
  if (trigger !== "manual" && !token.trim()) {
5639
5861
  return currentSettings;
5640
5862
  }
5863
+ await setSyncCancellationRequest(ctx, null);
5641
5864
  const runningStatePromise = (async () => {
5642
5865
  const syncableMappings = getSyncableMappingsForTarget(currentSettings.mappings, options.target);
5643
5866
  const syncState = createRunningSyncState(currentSettings.syncState, trigger, {
@@ -5667,6 +5890,7 @@ async function startSync(ctx, trigger, options = {}) {
5667
5890
  } catch (error) {
5668
5891
  return await createUnexpectedSyncErrorResult(ctx, trigger, error);
5669
5892
  } finally {
5893
+ await setSyncCancellationRequest(ctx, null);
5670
5894
  activePaperclipApiAuthTokensByCompanyId = null;
5671
5895
  activeRunningSyncState = null;
5672
5896
  activeSyncPromise = null;
@@ -6527,6 +6751,32 @@ var plugin = definePlugin({
6527
6751
  ...target ? { target } : {}
6528
6752
  });
6529
6753
  });
6754
+ ctx.actions.register("sync.cancel", async () => {
6755
+ const currentSettings = await getActiveOrCurrentSyncState(ctx);
6756
+ if (currentSettings.syncState.status !== "running") {
6757
+ return currentSettings;
6758
+ }
6759
+ const existingRequest = currentSettings.syncState.cancelRequestedAt?.trim() ? { requestedAt: currentSettings.syncState.cancelRequestedAt.trim() } : await getSyncCancellationRequest(ctx);
6760
+ const cancellationRequest = existingRequest ?? {
6761
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
6762
+ };
6763
+ await setSyncCancellationRequest(ctx, cancellationRequest);
6764
+ const next = await saveSettingsSyncState(
6765
+ ctx,
6766
+ currentSettings,
6767
+ createRunningSyncState(currentSettings.syncState, currentSettings.syncState.lastRunTrigger ?? "manual", {
6768
+ syncedIssuesCount: currentSettings.syncState.syncedIssuesCount ?? 0,
6769
+ createdIssuesCount: currentSettings.syncState.createdIssuesCount ?? 0,
6770
+ skippedIssuesCount: currentSettings.syncState.skippedIssuesCount ?? 0,
6771
+ erroredIssuesCount: currentSettings.syncState.erroredIssuesCount ?? 0,
6772
+ progress: currentSettings.syncState.progress,
6773
+ message: CANCELLING_SYNC_MESSAGE,
6774
+ cancelRequestedAt: cancellationRequest.requestedAt
6775
+ })
6776
+ );
6777
+ activeRunningSyncState = next;
6778
+ return next;
6779
+ });
6530
6780
  registerGitHubAgentTools(ctx);
6531
6781
  ctx.jobs.register("sync.github-issues", async (job) => {
6532
6782
  const settings = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",