paperclip-github-plugin 0.4.5 → 0.4.6

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
  }
@@ -10026,6 +10122,7 @@ async function startSync(ctx, trigger, options = {}) {
10026
10122
  );
10027
10123
  return quickResult2 ?? await getActiveOrCurrentSyncState(ctx);
10028
10124
  }
10125
+ await reconcileOrphanedRunningSyncState(ctx, options.target?.companyId);
10029
10126
  const [config, persistedSettings] = await Promise.all([
10030
10127
  getResolvedConfig(ctx),
10031
10128
  ctx.state.get(SETTINGS_SCOPE).then((value) => normalizeSettings(value))
@@ -10893,6 +10990,7 @@ var plugin = definePlugin({
10893
10990
  const record = input && typeof input === "object" ? input : {};
10894
10991
  const requestedCompanyId = normalizeCompanyId(record.companyId);
10895
10992
  const includeAssignees = Boolean(requestedCompanyId && record.includeAssignees === true);
10993
+ await reconcileOrphanedRunningSyncState(ctx, requestedCompanyId);
10896
10994
  const saved = await ctx.state.get(SETTINGS_SCOPE);
10897
10995
  const importRegistry = normalizeImportRegistry(await ctx.state.get(IMPORT_REGISTRY_SCOPE));
10898
10996
  const normalizedSettings = normalizeSettings(saved);
@@ -11179,7 +11277,12 @@ var plugin = definePlugin({
11179
11277
  });
11180
11278
  });
11181
11279
  ctx.actions.register("sync.cancel", async () => {
11182
- const currentSettings = await getActiveOrCurrentSyncState(ctx, activeRunningSyncCompanyId);
11280
+ const persistedRunningSyncCompanyId = activeRunningSyncState?.syncState.status === "running" ? activeRunningSyncCompanyId : resolvePersistedRunningSyncCompanyId(normalizeSettings(await ctx.state.get(SETTINGS_SCOPE)));
11281
+ const currentSettings = await reconcileOrphanedRunningSyncState(
11282
+ ctx,
11283
+ persistedRunningSyncCompanyId === null ? void 0 : persistedRunningSyncCompanyId,
11284
+ "cancelled"
11285
+ );
11183
11286
  if (currentSettings.syncState.status !== "running") {
11184
11287
  return currentSettings;
11185
11288
  }
@@ -11200,7 +11303,7 @@ var plugin = definePlugin({
11200
11303
  message: CANCELLING_SYNC_MESSAGE,
11201
11304
  cancelRequestedAt: cancellationRequest.requestedAt
11202
11305
  }),
11203
- activeRunningSyncCompanyId
11306
+ persistedRunningSyncCompanyId === null ? void 0 : persistedRunningSyncCompanyId
11204
11307
  );
11205
11308
  activeRunningSyncState = next;
11206
11309
  return next;
@@ -11211,14 +11314,15 @@ var plugin = definePlugin({
11211
11314
  const trigger = job.trigger === "retry" ? "retry" : "schedule";
11212
11315
  const scheduledTargets = listScheduledSyncTargets(settings);
11213
11316
  if (scheduledTargets.length === 0) {
11214
- if (job.trigger === "schedule" && !shouldRunScheduledSync(settings, job.scheduledAt)) {
11317
+ const reconciledSettings = await reconcileOrphanedRunningSyncState(ctx);
11318
+ if (job.trigger === "schedule" && !shouldRunScheduledSync(reconciledSettings, job.scheduledAt)) {
11215
11319
  return;
11216
11320
  }
11217
11321
  await startSync(ctx, trigger);
11218
11322
  return;
11219
11323
  }
11220
11324
  for (const target of scheduledTargets) {
11221
- const scopedSettings = materializeScopedSettings(settings, null, target?.companyId);
11325
+ const scopedSettings = await reconcileOrphanedRunningSyncState(ctx, target?.companyId);
11222
11326
  if (job.trigger === "schedule" && !shouldRunScheduledSync(scopedSettings, job.scheduledAt)) {
11223
11327
  continue;
11224
11328
  }
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.6",
4
4
  "description": "Paperclip plugin for synchronizing GitHub issues into Paperclip projects.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",