KrishnaCosmic commited on
Commit
8f7b71b
·
1 Parent(s): e31d5c3

bug fix 15

Browse files
src/app/api/sync/reconcile/route.ts ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Issue Reconciliation API Route
3
+ *
4
+ * POST /api/sync/reconcile - Reconcile a specific issue's state
5
+ *
6
+ * Checks the issue state on GitHub and updates the database accordingly.
7
+ * Use this to immediately sync a specific issue without running full sync.
8
+ */
9
+
10
+ import { NextRequest, NextResponse } from "next/server";
11
+ import { getCurrentUser } from "@/lib/auth";
12
+ import { reconcileSingleIssue, reconcileOpenTriageIssue1 } from "@/lib/sync/github-sync";
13
+
14
+ export async function POST(request: NextRequest) {
15
+ try {
16
+ const user = await getCurrentUser(request);
17
+ if (!user) {
18
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
19
+ }
20
+
21
+ if (!user.githubAccessToken) {
22
+ return NextResponse.json({ error: "GitHub access token not found" }, { status: 400 });
23
+ }
24
+
25
+ // Parse request body
26
+ const body = await request.json().catch(() => ({}));
27
+ const { owner, repo, issueNumber, reconcileOpenTriage } = body;
28
+
29
+ // Option 1: Reconcile the specific openTriage#1 issue
30
+ if (reconcileOpenTriage) {
31
+ const result = await reconcileOpenTriageIssue1(user.githubAccessToken);
32
+ return NextResponse.json({
33
+ success: result.success,
34
+ message: result.message,
35
+ result,
36
+ });
37
+ }
38
+
39
+ // Option 2: Reconcile a specific issue by owner/repo/number
40
+ if (owner && repo && issueNumber) {
41
+ const result = await reconcileSingleIssue(
42
+ user.githubAccessToken,
43
+ owner,
44
+ repo,
45
+ Number(issueNumber)
46
+ );
47
+ return NextResponse.json({
48
+ success: result.success,
49
+ message: result.message,
50
+ result,
51
+ });
52
+ }
53
+
54
+ return NextResponse.json({
55
+ error: "Missing required parameters. Provide either { reconcileOpenTriage: true } or { owner, repo, issueNumber }",
56
+ }, { status: 400 });
57
+
58
+ } catch (error) {
59
+ console.error("POST /api/sync/reconcile error:", error);
60
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
61
+ }
62
+ }
63
+
64
+ export async function GET() {
65
+ return NextResponse.json({
66
+ description: "Issue Reconciliation API",
67
+ usage: {
68
+ reconcileOpenTriage: {
69
+ method: "POST",
70
+ body: { reconcileOpenTriage: true },
71
+ description: "Reconcile cosmicMagnetar/openTriage#1 specifically",
72
+ },
73
+ reconcileSpecific: {
74
+ method: "POST",
75
+ body: { owner: "string", repo: "string", issueNumber: "number" },
76
+ description: "Reconcile any specific issue by owner/repo/number",
77
+ },
78
+ },
79
+ behavior: [
80
+ "Fetches the issue state directly from GitHub API",
81
+ "If issue is closed/not found on GitHub, removes it from database",
82
+ "If issue is open, updates database to match GitHub state",
83
+ "Publishes real-time events via Ably if configured",
84
+ ],
85
+ });
86
+ }
src/app/api/sync/run/route.ts CHANGED
@@ -7,7 +7,7 @@
7
 
8
  import { NextRequest, NextResponse } from "next/server";
9
  import { getCurrentUser } from "@/lib/auth";
10
- import { runFullSync, runMaintainerSync, runContributorSync, SYNC_INTERVAL_MS } from "@/lib/sync/github-sync";
11
 
12
  export async function POST(request: NextRequest) {
13
  try {
@@ -32,11 +32,17 @@ export async function POST(request: NextRequest) {
32
  stats = await runContributorSync(user.id, user.username, user.githubAccessToken);
33
  }
34
 
 
 
 
35
  return NextResponse.json({
36
  success: true,
37
  message: "Sync completed",
38
  role: userRole,
39
  stats,
 
 
 
40
  nextSyncIntervalMs: SYNC_INTERVAL_MS,
41
  });
42
  } catch (error) {
 
7
 
8
  import { NextRequest, NextResponse } from "next/server";
9
  import { getCurrentUser } from "@/lib/auth";
10
+ import { runFullSync, runMaintainerSync, runContributorSync, reconcileOpenTriageIssue1, SYNC_INTERVAL_MS } from "@/lib/sync/github-sync";
11
 
12
  export async function POST(request: NextRequest) {
13
  try {
 
32
  stats = await runContributorSync(user.id, user.username, user.githubAccessToken);
33
  }
34
 
35
+ // Always reconcile the critical openTriage#1 issue to ensure immediate state sync
36
+ const reconcileResult = await reconcileOpenTriageIssue1(user.githubAccessToken);
37
+
38
  return NextResponse.json({
39
  success: true,
40
  message: "Sync completed",
41
  role: userRole,
42
  stats,
43
+ reconcile: {
44
+ openTriageIssue1: reconcileResult,
45
+ },
46
  nextSyncIntervalMs: SYNC_INTERVAL_MS,
47
  });
48
  } catch (error) {
src/lib/sync/github-sync.ts CHANGED
@@ -588,3 +588,160 @@ export async function runMaintainerSync(
588
  role: 'MAINTAINER',
589
  });
590
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
588
  role: 'MAINTAINER',
589
  });
590
  }
591
+
592
+ // =============================================================================
593
+ // Reconcile Single Issue - Check specific issue state on GitHub
594
+ // =============================================================================
595
+
596
+ interface IssueReconcileResult {
597
+ success: boolean;
598
+ owner: string;
599
+ repo: string;
600
+ issueNumber: number;
601
+ githubState: 'open' | 'closed' | 'not_found';
602
+ dbState: 'open' | 'closed' | 'not_found';
603
+ action: 'none' | 'deleted' | 'updated' | 'created';
604
+ message: string;
605
+ }
606
+
607
+ /**
608
+ * Reconcile a specific issue by checking its state on GitHub
609
+ * and updating the database accordingly.
610
+ *
611
+ * This is useful for immediately syncing a specific issue
612
+ * (e.g., cosmicMagnetar/openTriage#1) without waiting for full sync.
613
+ */
614
+ export async function reconcileSingleIssue(
615
+ accessToken: string,
616
+ owner: string,
617
+ repo: string,
618
+ issueNumber: number
619
+ ): Promise<IssueReconcileResult> {
620
+ const octokit = new Octokit({ auth: accessToken });
621
+ const repoName = `${owner}/${repo}`;
622
+
623
+ try {
624
+ // 1. Check issue state on GitHub
625
+ let githubIssue: GitHubIssue | null = null;
626
+ let githubState: 'open' | 'closed' | 'not_found' = 'not_found';
627
+
628
+ try {
629
+ const response = await octokit.issues.get({
630
+ owner,
631
+ repo,
632
+ issue_number: issueNumber,
633
+ });
634
+ githubIssue = response.data as GitHubIssue;
635
+ githubState = githubIssue.state as 'open' | 'closed';
636
+ } catch (error: any) {
637
+ if (error.status === 404) {
638
+ githubState = 'not_found';
639
+ } else {
640
+ throw error;
641
+ }
642
+ }
643
+
644
+ // 2. Check current state in database
645
+ const dbIssue = await db.select()
646
+ .from(issues)
647
+ .where(
648
+ and(
649
+ eq(issues.owner, owner),
650
+ eq(issues.repo, repo),
651
+ eq(issues.number, issueNumber)
652
+ )
653
+ )
654
+ .limit(1);
655
+
656
+ const existingIssue = dbIssue[0];
657
+ const dbState: 'open' | 'closed' | 'not_found' = existingIssue
658
+ ? (existingIssue.state as 'open' | 'closed')
659
+ : 'not_found';
660
+
661
+ // 3. Reconcile based on states
662
+ let action: 'none' | 'deleted' | 'updated' | 'created' = 'none';
663
+ let message = '';
664
+
665
+ if (githubState === 'closed' || githubState === 'not_found') {
666
+ // Issue is closed or doesn't exist on GitHub - delete from DB
667
+ if (existingIssue) {
668
+ await db.delete(issues).where(eq(issues.id, existingIssue.id));
669
+ action = 'deleted';
670
+ message = `Issue #${issueNumber} is ${githubState} on GitHub - removed from database`;
671
+
672
+ // Publish deletion event
673
+ if (isAblyConfigured()) {
674
+ await publishIssueDeleted({
675
+ id: existingIssue.id,
676
+ githubIssueId: existingIssue.githubIssueId,
677
+ number: issueNumber,
678
+ title: existingIssue.title,
679
+ repoName,
680
+ owner,
681
+ repo,
682
+ isPR: existingIssue.isPR,
683
+ state: 'closed',
684
+ });
685
+ }
686
+
687
+ console.log(`[Reconcile] ${repoName}#${issueNumber}: Deleted (${githubState} on GitHub)`);
688
+ } else {
689
+ message = `Issue #${issueNumber} is ${githubState} on GitHub and not in database - no action needed`;
690
+ }
691
+ } else if (githubState === 'open' && githubIssue) {
692
+ // Issue is open on GitHub
693
+ if (existingIssue) {
694
+ // Update existing issue
695
+ if (existingIssue.state !== 'open' || existingIssue.title !== githubIssue.title) {
696
+ await db.update(issues)
697
+ .set({
698
+ state: 'open',
699
+ title: githubIssue.title,
700
+ body: githubIssue.body || null,
701
+ })
702
+ .where(eq(issues.id, existingIssue.id));
703
+ action = 'updated';
704
+ message = `Issue #${issueNumber} updated to match GitHub state`;
705
+ } else {
706
+ message = `Issue #${issueNumber} is already in sync`;
707
+ }
708
+ } else {
709
+ // Issue is open on GitHub but not in DB - would need repo context to create
710
+ message = `Issue #${issueNumber} is open on GitHub but not tracked. Run full sync to add it.`;
711
+ }
712
+ }
713
+
714
+ return {
715
+ success: true,
716
+ owner,
717
+ repo,
718
+ issueNumber,
719
+ githubState,
720
+ dbState,
721
+ action,
722
+ message,
723
+ };
724
+
725
+ } catch (error) {
726
+ const errorMessage = error instanceof Error ? error.message : String(error);
727
+ console.error(`[Reconcile] Error for ${repoName}#${issueNumber}:`, errorMessage);
728
+ return {
729
+ success: false,
730
+ owner,
731
+ repo,
732
+ issueNumber,
733
+ githubState: 'not_found',
734
+ dbState: 'not_found',
735
+ action: 'none',
736
+ message: `Error reconciling issue: ${errorMessage}`,
737
+ };
738
+ }
739
+ }
740
+
741
+ /**
742
+ * Reconcile the critical tracked issue: cosmicMagnetar/openTriage#1
743
+ * This ensures the issue state is immediately synced from GitHub.
744
+ */
745
+ export async function reconcileOpenTriageIssue1(accessToken: string): Promise<IssueReconcileResult> {
746
+ return reconcileSingleIssue(accessToken, 'cosmicMagnetar', 'openTriage', 1);
747
+ }