Skip to content

Commit 18fad64

Browse files
feat(web): Add force resync buttons for repo & connections (#610)
1 parent 2dfafda commit 18fad64

File tree

14 files changed

+329
-81
lines changed

14 files changed

+329
-81
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Fixed incorrect shutdown of PostHog SDK in the worker. [#609](https://github.com/sourcebot-dev/sourcebot/pull/609)
1212
- Fixed race condition in job schedulers. [#607](https://github.com/sourcebot-dev/sourcebot/pull/607)
1313

14+
### Added
15+
- Added force resync buttons for connections and repositories. [#610](https://github.com/sourcebot-dev/sourcebot/pull/610)
16+
1417
## [4.9.1] - 2025-11-07
1518

1619
### Added

packages/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"cross-fetch": "^4.0.0",
4141
"dotenv": "^16.4.5",
4242
"express": "^4.21.2",
43+
"express-async-errors": "^3.1.1",
4344
"git-url-parse": "^16.1.0",
4445
"gitea-js": "^1.22.0",
4546
"glob": "^11.0.0",

packages/backend/src/api.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { PrismaClient, RepoIndexingJobType } from '@sourcebot/db';
2+
import { createLogger } from '@sourcebot/shared';
3+
import express, { Request, Response } from 'express';
4+
import 'express-async-errors';
5+
import * as http from "http";
6+
import z from 'zod';
7+
import { ConnectionManager } from './connectionManager.js';
8+
import { PromClient } from './promClient.js';
9+
import { RepoIndexManager } from './repoIndexManager.js';
10+
11+
const logger = createLogger('api');
12+
const PORT = 3060;
13+
14+
export class Api {
15+
private server: http.Server;
16+
17+
constructor(
18+
promClient: PromClient,
19+
private prisma: PrismaClient,
20+
private connectionManager: ConnectionManager,
21+
private repoIndexManager: RepoIndexManager,
22+
) {
23+
const app = express();
24+
app.use(express.json());
25+
app.use(express.urlencoded({ extended: true }));
26+
27+
// Prometheus metrics endpoint
28+
app.use('/metrics', async (_req: Request, res: Response) => {
29+
res.set('Content-Type', promClient.registry.contentType);
30+
const metrics = await promClient.registry.metrics();
31+
res.end(metrics);
32+
});
33+
34+
app.post('/api/sync-connection', this.syncConnection.bind(this));
35+
app.post('/api/index-repo', this.indexRepo.bind(this));
36+
37+
this.server = app.listen(PORT, () => {
38+
logger.info(`API server is running on port ${PORT}`);
39+
});
40+
}
41+
42+
private async syncConnection(req: Request, res: Response) {
43+
const schema = z.object({
44+
connectionId: z.number(),
45+
}).strict();
46+
47+
const parsed = schema.safeParse(req.body);
48+
if (!parsed.success) {
49+
res.status(400).json({ error: parsed.error.message });
50+
return;
51+
}
52+
53+
const { connectionId } = parsed.data;
54+
const connection = await this.prisma.connection.findUnique({
55+
where: {
56+
id: connectionId,
57+
}
58+
});
59+
60+
if (!connection) {
61+
res.status(404).json({ error: 'Connection not found' });
62+
return;
63+
}
64+
65+
const [jobId] = await this.connectionManager.createJobs([connection]);
66+
67+
res.status(200).json({ jobId });
68+
}
69+
70+
private async indexRepo(req: Request, res: Response) {
71+
const schema = z.object({
72+
repoId: z.number(),
73+
}).strict();
74+
75+
const parsed = schema.safeParse(req.body);
76+
if (!parsed.success) {
77+
res.status(400).json({ error: parsed.error.message });
78+
return;
79+
}
80+
81+
const { repoId } = parsed.data;
82+
const repo = await this.prisma.repo.findUnique({
83+
where: { id: repoId },
84+
});
85+
86+
if (!repo) {
87+
res.status(404).json({ error: 'Repo not found' });
88+
return;
89+
}
90+
91+
const [jobId] = await this.repoIndexManager.createJobs([repo], RepoIndexingJobType.INDEX);
92+
res.status(200).json({ jobId });
93+
}
94+
95+
public async dispose() {
96+
return new Promise<void>((resolve, reject) => {
97+
this.server.close((err) => {
98+
if (err) reject(err);
99+
else resolve(undefined);
100+
});
101+
});
102+
}
103+
}

packages/backend/src/index.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import "./instrument.js";
22

33
import { PrismaClient } from "@sourcebot/db";
4-
import { createLogger } from "@sourcebot/shared";
5-
import { env, getConfigSettings, hasEntitlement, getDBConnectionString } from '@sourcebot/shared';
4+
import { createLogger, env, getConfigSettings, getDBConnectionString, hasEntitlement } from "@sourcebot/shared";
5+
import 'express-async-errors';
66
import { existsSync } from 'fs';
77
import { mkdir } from 'fs/promises';
88
import { Redis } from 'ioredis';
9+
import { Api } from "./api.js";
910
import { ConfigManager } from "./configManager.js";
1011
import { ConnectionManager } from './connectionManager.js';
1112
import { INDEX_CACHE_DIR, REPOS_CACHE_DIR } from './constants.js';
13+
import { AccountPermissionSyncer } from "./ee/accountPermissionSyncer.js";
1214
import { GithubAppManager } from "./ee/githubAppManager.js";
1315
import { RepoPermissionSyncer } from './ee/repoPermissionSyncer.js';
14-
import { AccountPermissionSyncer } from "./ee/accountPermissionSyncer.js";
16+
import { shutdownPosthog } from "./posthog.js";
1517
import { PromClient } from './promClient.js';
1618
import { RepoIndexManager } from "./repoIndexManager.js";
17-
import { shutdownPosthog } from "./posthog.js";
18-
1919

2020
const logger = createLogger('backend-entrypoint');
2121

@@ -74,6 +74,13 @@ else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement(
7474
accountPermissionSyncer.startScheduler();
7575
}
7676

77+
const api = new Api(
78+
promClient,
79+
prisma,
80+
connectionManager,
81+
repoIndexManager,
82+
);
83+
7784
logger.info('Worker started.');
7885

7986
const cleanup = async (signal: string) => {
@@ -88,7 +95,6 @@ const cleanup = async (signal: string) => {
8895
connectionManager.dispose(),
8996
repoPermissionSyncer.dispose(),
9097
accountPermissionSyncer.dispose(),
91-
promClient.dispose(),
9298
configManager.dispose(),
9399
]),
94100
new Promise((_, reject) =>
@@ -102,6 +108,7 @@ const cleanup = async (signal: string) => {
102108

103109
await prisma.$disconnect();
104110
await redis.quit();
111+
await api.dispose();
105112
await shutdownPosthog();
106113
}
107114

packages/backend/src/promClient.ts

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
1-
import express, { Request, Response } from 'express';
2-
import { Server } from 'http';
31
import client, { Registry, Counter, Gauge } from 'prom-client';
4-
import { createLogger } from "@sourcebot/shared";
5-
6-
const logger = createLogger('prometheus-client');
7-
82
export class PromClient {
9-
private registry: Registry;
10-
private app: express.Application;
11-
private server: Server;
3+
public registry: Registry;
124

135
public activeRepoIndexJobs: Gauge<string>;
146
public pendingRepoIndexJobs: Gauge<string>;
@@ -22,8 +14,6 @@ export class PromClient {
2214
public connectionSyncJobFailTotal: Counter<string>;
2315
public connectionSyncJobSuccessTotal: Counter<string>;
2416

25-
public readonly PORT = 3060;
26-
2717
constructor() {
2818
this.registry = new Registry();
2919

@@ -100,26 +90,5 @@ export class PromClient {
10090
client.collectDefaultMetrics({
10191
register: this.registry,
10292
});
103-
104-
this.app = express();
105-
this.app.get('/metrics', async (req: Request, res: Response) => {
106-
res.set('Content-Type', this.registry.contentType);
107-
108-
const metrics = await this.registry.metrics();
109-
res.end(metrics);
110-
});
111-
112-
this.server = this.app.listen(this.PORT, () => {
113-
logger.info(`Prometheus metrics server is running on port ${this.PORT}`);
114-
});
115-
}
116-
117-
async dispose() {
118-
return new Promise<void>((resolve, reject) => {
119-
this.server.close((err) => {
120-
if (err) reject(err);
121-
else resolve();
122-
});
123-
});
12493
}
12594
}

packages/backend/src/repoIndexManager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export class RepoIndexManager {
192192
}
193193
}
194194

195-
private async createJobs(repos: Repo[], type: RepoIndexingJobType) {
195+
public async createJobs(repos: Repo[], type: RepoIndexingJobType) {
196196
// @note: we don't perform this in a transaction because
197197
// we want to avoid the situation where a job is created and run
198198
// prior to the transaction being committed.
@@ -221,6 +221,8 @@ export class RepoIndexManager {
221221
const jobTypeLabel = getJobTypePrometheusLabel(type);
222222
this.promClient.pendingRepoIndexJobs.inc({ repo: job.repo.name, type: jobTypeLabel });
223223
}
224+
225+
return jobs.map(job => job.id);
224226
}
225227

226228
private async runJob(job: ReservedJob<JobPayload>) {

packages/web/src/app/[domain]/repos/[id]/page.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { sew } from "@/actions"
1+
import { getCurrentUserRole, sew } from "@/actions"
22
import { Badge } from "@/components/ui/badge"
33
import { Button } from "@/components/ui/button"
44
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
@@ -19,6 +19,7 @@ import { BackButton } from "../../components/backButton"
1919
import { DisplayDate } from "../../components/DisplayDate"
2020
import { RepoBranchesTable } from "../components/repoBranchesTable"
2121
import { RepoJobsTable } from "../components/repoJobsTable"
22+
import { OrgRole } from "@sourcebot/db"
2223

2324
export default async function RepoDetailPage({ params }: { params: Promise<{ id: string }> }) {
2425
const { id } = await params
@@ -51,6 +52,11 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
5152

5253
const repoMetadata = repoMetadataSchema.parse(repo.metadata);
5354

55+
const userRole = await getCurrentUserRole(SINGLE_TENANT_ORG_DOMAIN);
56+
if (isServiceError(userRole)) {
57+
throw new ServiceErrorException(userRole);
58+
}
59+
5460
return (
5561
<>
5662
<div className="mb-6">
@@ -172,7 +178,11 @@ export default async function RepoDetailPage({ params }: { params: Promise<{ id:
172178
</CardHeader>
173179
<CardContent>
174180
<Suspense fallback={<Skeleton className="h-96 w-full" />}>
175-
<RepoJobsTable data={repo.jobs} />
181+
<RepoJobsTable
182+
data={repo.jobs}
183+
repoId={repo.id}
184+
isIndexButtonVisible={userRole === OrgRole.OWNER}
185+
/>
176186
</Suspense>
177187
</CardContent>
178188
</Card>

0 commit comments

Comments
 (0)