paperclip-github-plugin 0.2.2 → 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) {
@@ -5163,6 +5336,10 @@ async function performSync(ctx, trigger, options = {}) {
5163
5336
  return next;
5164
5337
  }
5165
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
+ };
5166
5343
  const next = {
5167
5344
  ...settings,
5168
5345
  syncState: createErrorSyncState({
@@ -5172,10 +5349,14 @@ async function performSync(ctx, trigger, options = {}) {
5172
5349
  createdIssuesCount: 0,
5173
5350
  skippedIssuesCount: 0,
5174
5351
  erroredIssuesCount: 0,
5175
- errorDetails: {
5176
- phase: "configuration",
5177
- suggestedAction: "Update Paperclip to a runtime that supports plugin issue creation, then retry sync."
5178
- }
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
+ )
5179
5360
  })
5180
5361
  };
5181
5362
  await ctx.state.set(SETTINGS_SCOPE, next);
@@ -5211,14 +5392,23 @@ async function performSync(ctx, trigger, options = {}) {
5211
5392
  erroredIssuesCount: recoverableFailures.length,
5212
5393
  progress: currentProgress
5213
5394
  });
5395
+ async function throwIfSyncCancelled() {
5396
+ const cancellationRequest = await getSyncCancellationRequest(ctx);
5397
+ if (!cancellationRequest) {
5398
+ return;
5399
+ }
5400
+ throw new SyncCancellationError(cancellationRequest.requestedAt);
5401
+ }
5214
5402
  async function persistRunningProgress(force = false) {
5215
5403
  const progress = normalizeSyncProgress(currentProgress);
5404
+ const recentFailures = buildRecentSyncFailureLogEntries(recoverableFailures);
5216
5405
  const signature = JSON.stringify({
5217
5406
  syncedIssuesCount,
5218
5407
  createdIssuesCount,
5219
5408
  skippedIssuesCount,
5220
5409
  erroredIssuesCount: recoverableFailures.length,
5221
- progress
5410
+ progress,
5411
+ recentFailures
5222
5412
  });
5223
5413
  const now = Date.now();
5224
5414
  if (!force) {
@@ -5237,7 +5427,8 @@ async function performSync(ctx, trigger, options = {}) {
5237
5427
  createdIssuesCount,
5238
5428
  skippedIssuesCount,
5239
5429
  erroredIssuesCount: recoverableFailures.length,
5240
- progress
5430
+ progress,
5431
+ recentFailures
5241
5432
  })
5242
5433
  );
5243
5434
  activeRunningSyncState = currentSettings;
@@ -5254,7 +5445,9 @@ async function performSync(ctx, trigger, options = {}) {
5254
5445
  }
5255
5446
  const repositoryPlans = [];
5256
5447
  try {
5448
+ await throwIfSyncCancelled();
5257
5449
  for (const [mappingIndex, mapping] of mappings.entries()) {
5450
+ await throwIfSyncCancelled();
5258
5451
  try {
5259
5452
  const repository = requireRepositoryReference(mapping.repositoryUrl);
5260
5453
  const importedIssueRecords = nextRegistry.filter((entry) => doesImportedIssueRecordMatchMapping(entry, mapping)).filter((entry) => doesImportedIssueMatchTarget(entry, options.target));
@@ -5342,7 +5535,7 @@ async function performSync(ctx, trigger, options = {}) {
5342
5535
  trackedIssueCount
5343
5536
  });
5344
5537
  } catch (error) {
5345
- if (isGitHubRateLimitError(error)) {
5538
+ if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
5346
5539
  throw error;
5347
5540
  }
5348
5541
  recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
@@ -5371,6 +5564,7 @@ async function performSync(ctx, trigger, options = {}) {
5371
5564
  }
5372
5565
  await persistRunningProgress(true);
5373
5566
  for (const plan of repositoryPlans) {
5567
+ await throwIfSyncCancelled();
5374
5568
  try {
5375
5569
  const { mapping, advancedSettings, repository, repositoryIndex, allIssuesById, issues } = plan;
5376
5570
  const companyId = mapping.companyId;
@@ -5425,8 +5619,9 @@ async function performSync(ctx, trigger, options = {}) {
5425
5619
  openLinkedPullRequestNumbers,
5426
5620
  pullRequestStatusCache
5427
5621
  );
5622
+ await throwIfSyncCancelled();
5428
5623
  } catch (error) {
5429
- if (isGitHubRateLimitError(error)) {
5624
+ if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
5430
5625
  throw error;
5431
5626
  }
5432
5627
  }
@@ -5441,6 +5636,7 @@ async function performSync(ctx, trigger, options = {}) {
5441
5636
  };
5442
5637
  await persistRunningProgress(true);
5443
5638
  for (const [issueIndex, issue] of issues.entries()) {
5639
+ await throwIfSyncCancelled();
5444
5640
  const createdIssueCountBefore = createdIssueIds.size;
5445
5641
  const skippedIssueCountBefore = skippedIssueIds.size;
5446
5642
  try {
@@ -5489,6 +5685,7 @@ async function performSync(ctx, trigger, options = {}) {
5489
5685
  totalIssueCount: totalTrackedIssueCount
5490
5686
  };
5491
5687
  await persistRunningProgress(true);
5688
+ await throwIfSyncCancelled();
5492
5689
  const synchronizationResult = await synchronizePaperclipIssueStatuses(
5493
5690
  ctx,
5494
5691
  octokit,
@@ -5506,6 +5703,7 @@ async function performSync(ctx, trigger, options = {}) {
5506
5703
  repositoryMaintainerCache,
5507
5704
  failureContext,
5508
5705
  recoverableFailures,
5706
+ throwIfSyncCancelled,
5509
5707
  async (progress) => {
5510
5708
  markTrackedIssueProcessed(mapping, progress.githubIssueId);
5511
5709
  currentProgress = {
@@ -5524,7 +5722,7 @@ async function performSync(ctx, trigger, options = {}) {
5524
5722
  updatedLabelsCount += synchronizationResult.updatedLabelsCount;
5525
5723
  updatedDescriptionsCount += synchronizationResult.updatedDescriptionsCount;
5526
5724
  } catch (error) {
5527
- if (isGitHubRateLimitError(error)) {
5725
+ if (error instanceof SyncCancellationError || isGitHubRateLimitError(error)) {
5528
5726
  throw error;
5529
5727
  }
5530
5728
  recordRecoverableSyncFailure(ctx, recoverableFailures, error, failureContext);
@@ -5548,7 +5746,8 @@ async function performSync(ctx, trigger, options = {}) {
5548
5746
  skippedIssuesCount,
5549
5747
  erroredIssuesCount: recoverableFailures.length,
5550
5748
  progress: currentProgress,
5551
- errorDetails
5749
+ errorDetails,
5750
+ recentFailures: buildRecentSyncFailureLogEntries(recoverableFailures)
5552
5751
  })
5553
5752
  };
5554
5753
  await ctx.state.set(SETTINGS_SCOPE, next2);
@@ -5574,6 +5773,24 @@ async function performSync(ctx, trigger, options = {}) {
5574
5773
  await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
5575
5774
  return next;
5576
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
+ }
5577
5794
  const errorDetails = buildSyncErrorDetails(error, failureContext);
5578
5795
  const next = {
5579
5796
  ...currentSettings,
@@ -5585,7 +5802,11 @@ async function performSync(ctx, trigger, options = {}) {
5585
5802
  skippedIssuesCount,
5586
5803
  erroredIssuesCount: recoverableFailures.length,
5587
5804
  progress: currentProgress,
5588
- errorDetails
5805
+ errorDetails,
5806
+ recentFailures: appendRecentSyncFailureLogEntry(
5807
+ buildRecentSyncFailureLogEntries(recoverableFailures),
5808
+ buildSyncFailureLogEntry(error, failureContext)
5809
+ )
5589
5810
  })
5590
5811
  };
5591
5812
  await ctx.state.set(SETTINGS_SCOPE, next);
@@ -5639,6 +5860,7 @@ async function startSync(ctx, trigger, options = {}) {
5639
5860
  if (trigger !== "manual" && !token.trim()) {
5640
5861
  return currentSettings;
5641
5862
  }
5863
+ await setSyncCancellationRequest(ctx, null);
5642
5864
  const runningStatePromise = (async () => {
5643
5865
  const syncableMappings = getSyncableMappingsForTarget(currentSettings.mappings, options.target);
5644
5866
  const syncState = createRunningSyncState(currentSettings.syncState, trigger, {
@@ -5668,6 +5890,7 @@ async function startSync(ctx, trigger, options = {}) {
5668
5890
  } catch (error) {
5669
5891
  return await createUnexpectedSyncErrorResult(ctx, trigger, error);
5670
5892
  } finally {
5893
+ await setSyncCancellationRequest(ctx, null);
5671
5894
  activePaperclipApiAuthTokensByCompanyId = null;
5672
5895
  activeRunningSyncState = null;
5673
5896
  activeSyncPromise = null;
@@ -6528,6 +6751,32 @@ var plugin = definePlugin({
6528
6751
  ...target ? { target } : {}
6529
6752
  });
6530
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
+ });
6531
6780
  registerGitHubAgentTools(ctx);
6532
6781
  ctx.jobs.register("sync.github-issues", async (job) => {
6533
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.2",
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",