paperclip-github-plugin 0.4.5 → 0.4.7

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
@@ -623,6 +623,8 @@ var GITHUB_TOKEN_PERMISSION_AUDIT_CACHE_TTL_MS = 5 * 6e4;
623
623
  var MANUAL_SYNC_RESPONSE_GRACE_PERIOD_MS = 500;
624
624
  var RUNNING_SYNC_MESSAGE = "GitHub sync is running in the background. This page will update when it finishes.";
625
625
  var CANCELLING_SYNC_MESSAGE = "Cancellation requested. GitHub sync will stop after the current step finishes.";
626
+ var INTERRUPTED_SYNC_MESSAGE = "GitHub sync stopped unexpectedly before it finished. The worker restarted while the sync was running.";
627
+ var INTERRUPTED_SYNC_ACTION = "Run GitHub sync again. If it stops on the same repository or issue, retry that narrower scope to isolate the failing step.";
626
628
  var SYNC_PROGRESS_PERSIST_INTERVAL_MS = 250;
627
629
  var MAX_SYNC_FAILURE_LOG_ENTRIES = 25;
628
630
  var GITHUB_SECONDARY_RATE_LIMIT_FALLBACK_MS = 6e4;
@@ -2895,6 +2897,97 @@ async function getSyncCancellationRequest(ctx) {
2895
2897
  }
2896
2898
  return normalizeSyncCancellationRequest(await ctx.state.get(SYNC_CANCELLATION_SCOPE));
2897
2899
  }
2900
+ function buildInterruptedSyncMessage(progress) {
2901
+ const completedIssueCount = typeof progress?.completedIssueCount === "number" ? Math.max(0, progress.completedIssueCount) : void 0;
2902
+ const totalIssueCount = typeof progress?.totalIssueCount === "number" ? Math.max(0, progress.totalIssueCount) : void 0;
2903
+ const completionSummary = completedIssueCount !== void 0 && totalIssueCount !== void 0 ? ` Completed ${Math.min(completedIssueCount, totalIssueCount)} of ${totalIssueCount} issues before the worker restarted.` : "";
2904
+ return `${INTERRUPTED_SYNC_MESSAGE}${completionSummary}`;
2905
+ }
2906
+ function getInterruptedSyncFailurePhase(progress) {
2907
+ switch (progress?.phase) {
2908
+ case "preparing":
2909
+ return "building_import_plan";
2910
+ case "importing":
2911
+ return "importing_issue";
2912
+ case "syncing":
2913
+ return "evaluating_github_status";
2914
+ default:
2915
+ return void 0;
2916
+ }
2917
+ }
2918
+ function getActiveRunningSyncForScope(companyId) {
2919
+ const normalizedCompanyId = normalizeCompanyId(companyId);
2920
+ if (activeRunningSyncState?.syncState.status !== "running") {
2921
+ return null;
2922
+ }
2923
+ const activeSyncMatchesScope = normalizedCompanyId === void 0 || activeRunningSyncCompanyId === void 0 || activeRunningSyncCompanyId === normalizedCompanyId;
2924
+ if (!activeSyncMatchesScope) {
2925
+ return null;
2926
+ }
2927
+ return materializeScopedSettings(activeRunningSyncState, null, normalizedCompanyId);
2928
+ }
2929
+ async function reconcileOrphanedRunningSyncState(ctx, companyId, resolution = "error") {
2930
+ const normalizedCompanyId = normalizeCompanyId(companyId);
2931
+ const activeRunningSync = getActiveRunningSyncForScope(normalizedCompanyId);
2932
+ if (activeRunningSync) {
2933
+ return activeRunningSync;
2934
+ }
2935
+ const current = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
2936
+ const scopedSyncState = getScopedSyncState(current, normalizedCompanyId);
2937
+ if (scopedSyncState.status !== "running") {
2938
+ return materializeScopedSettings(current, null, normalizedCompanyId);
2939
+ }
2940
+ const trigger = scopedSyncState.lastRunTrigger ?? "manual";
2941
+ const progress = normalizeSyncProgress(scopedSyncState.progress);
2942
+ const syncCounts = {
2943
+ syncedIssuesCount: scopedSyncState.syncedIssuesCount ?? 0,
2944
+ createdIssuesCount: scopedSyncState.createdIssuesCount ?? 0,
2945
+ skippedIssuesCount: scopedSyncState.skippedIssuesCount ?? 0,
2946
+ erroredIssuesCount: scopedSyncState.erroredIssuesCount ?? 0
2947
+ };
2948
+ const nextSyncState = resolution === "cancelled" || Boolean(scopedSyncState.cancelRequestedAt?.trim()) ? createCancelledSyncState({
2949
+ message: buildCancelledSyncMessage(void 0, progress),
2950
+ trigger,
2951
+ ...syncCounts,
2952
+ ...progress ? { progress } : {}
2953
+ }) : (() => {
2954
+ const errorDetails = normalizeSyncErrorDetails({
2955
+ ...getInterruptedSyncFailurePhase(progress) ? { phase: getInterruptedSyncFailurePhase(progress) } : {},
2956
+ ...progress?.currentRepositoryUrl ? { repositoryUrl: progress.currentRepositoryUrl } : {},
2957
+ ...typeof progress?.currentIssueNumber === "number" ? { githubIssueNumber: progress.currentIssueNumber } : {},
2958
+ rawMessage: INTERRUPTED_SYNC_MESSAGE,
2959
+ suggestedAction: INTERRUPTED_SYNC_ACTION
2960
+ });
2961
+ const message = buildInterruptedSyncMessage(progress);
2962
+ return createErrorSyncState({
2963
+ message,
2964
+ trigger,
2965
+ ...syncCounts,
2966
+ ...progress ? { progress } : {},
2967
+ ...errorDetails ? { errorDetails } : {},
2968
+ recentFailures: appendRecentSyncFailureLogEntry(
2969
+ scopedSyncState.recentFailures,
2970
+ createSyncFailureLogEntry({
2971
+ message,
2972
+ ...errorDetails ? { errorDetails } : {}
2973
+ })
2974
+ )
2975
+ });
2976
+ })();
2977
+ const next = await saveSettingsSyncState(ctx, current, nextSyncState, normalizedCompanyId);
2978
+ await setSyncCancellationRequest(ctx, null);
2979
+ return next;
2980
+ }
2981
+ function resolvePersistedRunningSyncCompanyId(settings) {
2982
+ if (normalizeSyncState(settings.syncState).status === "running") {
2983
+ return void 0;
2984
+ }
2985
+ const runningCompanyIds = Object.entries(settings.syncStateByCompanyId ?? {}).flatMap(([companyId, syncState]) => {
2986
+ const normalizedCompanyId = normalizeCompanyId(companyId);
2987
+ return normalizedCompanyId && normalizeSyncState(syncState).status === "running" ? [normalizedCompanyId] : [];
2988
+ });
2989
+ return runningCompanyIds.length === 1 ? runningCompanyIds[0] : null;
2990
+ }
2898
2991
  function buildCancelledSyncMessage(target, progress) {
2899
2992
  const completedIssueCount = typeof progress?.completedIssueCount === "number" ? Math.max(0, progress.completedIssueCount) : void 0;
2900
2993
  const totalIssueCount = typeof progress?.totalIssueCount === "number" ? Math.max(0, progress.totalIssueCount) : void 0;
@@ -2949,11 +3042,11 @@ async function waitForSyncResultWithinGracePeriod(promise, timeoutMs) {
2949
3042
  }
2950
3043
  async function getActiveOrCurrentSyncState(ctx, companyId) {
2951
3044
  const normalizedCompanyId = normalizeCompanyId(companyId);
2952
- if (activeRunningSyncState?.syncState.status === "running" && activeRunningSyncCompanyId === normalizedCompanyId) {
2953
- return materializeScopedSettings(activeRunningSyncState, null, normalizedCompanyId);
3045
+ const activeRunningSync = getActiveRunningSyncForScope(normalizedCompanyId);
3046
+ if (activeRunningSync) {
3047
+ return activeRunningSync;
2954
3048
  }
2955
- const current = normalizeSettings(await ctx.state.get(SETTINGS_SCOPE));
2956
- return materializeScopedSettings(current, null, normalizedCompanyId);
3049
+ return reconcileOrphanedRunningSyncState(ctx, normalizedCompanyId);
2957
3050
  }
2958
3051
  function updateSyncFailureContext(current, next) {
2959
3052
  if ("phase" in next) {
@@ -9467,6 +9560,9 @@ function shouldRunScheduledSync(settings, scheduledAt) {
9467
9560
  if (getActiveGitHubRateLimitPause(settings.syncState, now)) {
9468
9561
  return false;
9469
9562
  }
9563
+ if (settings.syncState.status === "running") {
9564
+ return false;
9565
+ }
9470
9566
  if (!settings.syncState.checkedAt) {
9471
9567
  return true;
9472
9568
  }
@@ -9956,28 +10052,25 @@ async function performSync(ctx, trigger, options = {}) {
9956
10052
  await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
9957
10053
  return materializeScopedSettings(next2, config, targetCompanyId);
9958
10054
  }
9959
- const next = {
9960
- ...currentSettings,
9961
- syncState: {
9962
- status: "success",
9963
- message: `${options.target ? `GitHub sync for ${options.target.displayLabel} is complete. ` : "Sync complete. "}Imported ${createdIssuesCount} issues, updated ${updatedStatusesCount} issue status${updatedStatusesCount === 1 ? "" : "es"}, updated ${updatedLabelsCount} issue label set${updatedLabelsCount === 1 ? "" : "s"}, updated ${updatedDescriptionsCount} issue description${updatedDescriptionsCount === 1 ? "" : "s"}, and skipped ${skippedIssuesCount} already-synced issue${skippedIssuesCount === 1 ? "" : "s"}.`,
9964
- checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
9965
- syncedIssuesCount,
9966
- createdIssuesCount,
9967
- skippedIssuesCount,
9968
- erroredIssuesCount: 0,
9969
- lastRunTrigger: trigger
9970
- }
10055
+ const nextSyncState = {
10056
+ status: "success",
10057
+ message: `${options.target ? `GitHub sync for ${options.target.displayLabel} is complete. ` : "Sync complete. "}Imported ${createdIssuesCount} issues, updated ${updatedStatusesCount} issue status${updatedStatusesCount === 1 ? "" : "es"}, updated ${updatedLabelsCount} issue label set${updatedLabelsCount === 1 ? "" : "s"}, updated ${updatedDescriptionsCount} issue description${updatedDescriptionsCount === 1 ? "" : "s"}, and skipped ${skippedIssuesCount} already-synced issue${skippedIssuesCount === 1 ? "" : "s"}.`,
10058
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
10059
+ syncedIssuesCount,
10060
+ createdIssuesCount,
10061
+ skippedIssuesCount,
10062
+ erroredIssuesCount: 0,
10063
+ lastRunTrigger: trigger
9971
10064
  };
9972
- await ctx.state.set(SETTINGS_SCOPE, next);
9973
- await ctx.state.set(SYNC_STATE_SCOPE, next.syncState);
10065
+ const next = await saveSettingsSyncState(ctx, currentSettings, nextSyncState, targetCompanyId);
9974
10066
  await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
9975
10067
  return next;
9976
10068
  } catch (error) {
9977
10069
  if (error instanceof SyncCancellationError) {
9978
- const next2 = {
9979
- ...currentSettings,
9980
- syncState: createCancelledSyncState({
10070
+ const next2 = await saveSettingsSyncState(
10071
+ ctx,
10072
+ currentSettings,
10073
+ createCancelledSyncState({
9981
10074
  message: buildCancelledSyncMessage(options.target, currentProgress),
9982
10075
  trigger,
9983
10076
  syncedIssuesCount,
@@ -9985,10 +10078,9 @@ async function performSync(ctx, trigger, options = {}) {
9985
10078
  skippedIssuesCount,
9986
10079
  erroredIssuesCount: recoverableFailures.length,
9987
10080
  progress: currentProgress
9988
- })
9989
- };
9990
- await ctx.state.set(SETTINGS_SCOPE, next2);
9991
- await ctx.state.set(SYNC_STATE_SCOPE, next2.syncState);
10081
+ }),
10082
+ targetCompanyId
10083
+ );
9992
10084
  await ctx.state.set(IMPORT_REGISTRY_SCOPE, nextRegistry);
9993
10085
  return next2;
9994
10086
  }
@@ -10026,6 +10118,7 @@ async function startSync(ctx, trigger, options = {}) {
10026
10118
  );
10027
10119
  return quickResult2 ?? await getActiveOrCurrentSyncState(ctx);
10028
10120
  }
10121
+ await reconcileOrphanedRunningSyncState(ctx, options.target?.companyId);
10029
10122
  const [config, persistedSettings] = await Promise.all([
10030
10123
  getResolvedConfig(ctx),
10031
10124
  ctx.state.get(SETTINGS_SCOPE).then((value) => normalizeSettings(value))
@@ -10893,6 +10986,7 @@ var plugin = definePlugin({
10893
10986
  const record = input && typeof input === "object" ? input : {};
10894
10987
  const requestedCompanyId = normalizeCompanyId(record.companyId);
10895
10988
  const includeAssignees = Boolean(requestedCompanyId && record.includeAssignees === true);
10989
+ await reconcileOrphanedRunningSyncState(ctx, requestedCompanyId);
10896
10990
  const saved = await ctx.state.get(SETTINGS_SCOPE);
10897
10991
  const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
10898
10992
  const normalizedSettings = normalizeSettings(saved);
@@ -11179,7 +11273,12 @@ var plugin = definePlugin({
11179
11273
  });
11180
11274
  });
11181
11275
  ctx.actions.register("sync.cancel", async () => {
11182
- const currentSettings = await getActiveOrCurrentSyncState(ctx, activeRunningSyncCompanyId);
11276
+ const persistedRunningSyncCompanyId = activeRunningSyncState?.syncState.status === "running" ? activeRunningSyncCompanyId : resolvePersistedRunningSyncCompanyId(normalizeSettings(await ctx.state.get(SETTINGS_SCOPE)));
11277
+ const currentSettings = await reconcileOrphanedRunningSyncState(
11278
+ ctx,
11279
+ persistedRunningSyncCompanyId === null ? void 0 : persistedRunningSyncCompanyId,
11280
+ "cancelled"
11281
+ );
11183
11282
  if (currentSettings.syncState.status !== "running") {
11184
11283
  return currentSettings;
11185
11284
  }
@@ -11200,7 +11299,7 @@ var plugin = definePlugin({
11200
11299
  message: CANCELLING_SYNC_MESSAGE,
11201
11300
  cancelRequestedAt: cancellationRequest.requestedAt
11202
11301
  }),
11203
- activeRunningSyncCompanyId
11302
+ persistedRunningSyncCompanyId === null ? void 0 : persistedRunningSyncCompanyId
11204
11303
  );
11205
11304
  activeRunningSyncState = next;
11206
11305
  return next;
@@ -11211,14 +11310,15 @@ var plugin = definePlugin({
11211
11310
  const trigger = job.trigger === "retry" ? "retry" : "schedule";
11212
11311
  const scheduledTargets = listScheduledSyncTargets(settings);
11213
11312
  if (scheduledTargets.length === 0) {
11214
- if (job.trigger === "schedule" && !shouldRunScheduledSync(settings, job.scheduledAt)) {
11313
+ const reconciledSettings = await reconcileOrphanedRunningSyncState(ctx);
11314
+ if (job.trigger === "schedule" && !shouldRunScheduledSync(reconciledSettings, job.scheduledAt)) {
11215
11315
  return;
11216
11316
  }
11217
11317
  await startSync(ctx, trigger);
11218
11318
  return;
11219
11319
  }
11220
11320
  for (const target of scheduledTargets) {
11221
- const scopedSettings = materializeScopedSettings(settings, null, target?.companyId);
11321
+ const scopedSettings = await reconcileOrphanedRunningSyncState(ctx, target?.companyId);
11222
11322
  if (job.trigger === "schedule" && !shouldRunScheduledSync(scopedSettings, job.scheduledAt)) {
11223
11323
  continue;
11224
11324
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paperclip-github-plugin",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",