Skip to content
Open
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `GET /api/connections` returns a paginated, auth-gated list of code-host connections in the org with per-connection sync state (last sync timestamp, latest job status, in-flight job count, repo count) so operators can monitor connection health from a script and pipe into Prometheus / Datadog / Grafana / Slack alerts. [#1517](https://github.com/sourcebot-dev/sourcebot/pull/1517)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
### Changed
- Vulnerability triage now keeps Linear issues synchronized with current security findings.

Expand Down
209 changes: 209 additions & 0 deletions docs/api-reference/sourcebot-public.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,123 @@
"status"
]
},
"PublicConnectionLatestJob": {
"type": "object",
"nullable": true,
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"PENDING",
"IN_PROGRESS",
"COMPLETED",
"FAILED"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"completedAt": {
"type": "string",
"nullable": true,
"format": "date-time"
},
"errorMessage": {
"type": "string",
"nullable": true
}
},
"required": [
"id",
"status",
"createdAt",
"completedAt",
"errorMessage"
]
},
"PublicConnectionSummary": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true
},
"name": {
"type": "string"
},
"connectionType": {
"type": "string",
"enum": [
"github",
"gitlab",
"gitea",
"gerrit",
"bitbucket-server",
"bitbucket-cloud",
"generic-git-host",
"azuredevops"
]
},
"isDeclarative": {
"type": "boolean"
},
"syncedAt": {
"type": "string",
"nullable": true,
"format": "date-time"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"repoCount": {
"type": "integer",
"minimum": 0
},
"inFlightJobCount": {
"type": "integer",
"minimum": 0
},
"latestJob": {
"$ref": "#/components/schemas/PublicConnectionLatestJob"
}
},
"required": [
"id",
"name",
"connectionType",
"isDeclarative",
"syncedAt",
"createdAt",
"updatedAt",
"repoCount",
"inFlightJobCount",
"latestJob"
]
},
"PublicListConnectionsResponse": {
"type": "object",
"properties": {
"connections": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PublicConnectionSummary"
}
}
},
"required": [
"connections"
]
},
"PublicFileSourceResponse": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -1479,6 +1596,98 @@
}
}
},
"/api/connections": {
"get": {
"operationId": "listConnections",
"tags": [
"System"
],
"summary": "List code-host connections",
"description": "Returns a paginated list of code-host connections in the org, with per-connection sync state (last sync timestamp, latest job status, in-flight job count, repo count). Auth-gated. The connection `config` is never returned (it carries tokens).",
"parameters": [
{
"schema": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true,
"default": 1
},
"required": false,
"name": "page",
"in": "query"
},
{
"schema": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true,
"maximum": 100,
"default": 50
},
"required": false,
"name": "perPage",
"in": "query"
}
],
"responses": {
"200": {
"description": "Paginated connection list.",
"headers": {
"X-Total-Count": {
"description": "Total number of connections matching the query across all pages.",
"schema": {
"type": "integer",
"example": 5
}
},
"Link": {
"description": "Pagination links formatted per RFC 8288. Includes `rel=\"next\"`, `rel=\"prev\"`, `rel=\"first\"`, and `rel=\"last\"` when applicable.",
"schema": {
"type": "string"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PublicListConnectionsResponse"
}
}
}
},
"400": {
"description": "Invalid query parameters.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PublicApiServiceError"
}
}
}
},
"401": {
"description": "Not authenticated.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PublicApiServiceError"
}
}
}
},
"500": {
"description": "Unexpected connection listing failure.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PublicApiServiceError"
}
}
}
}
}
}
},
"/api/source": {
"get": {
"operationId": "getFileSource",
Expand Down
3 changes: 2 additions & 1 deletion docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@
"icon": "server",
"pages": [
"GET /api/version",
"GET /api/health"
"GET /api/health",
"GET /api/connections"
]
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'server-only';

import { ConnectionSyncJobStatus } from '@sourcebot/db';
import { withAuth } from '@/middleware/withAuth';
import { sew } from '@/middleware/sew';

export interface ListConnectionsParams {
page: number;
perPage: number;
}

export const listConnections = async (
{ page, perPage }: ListConnectionsParams,
) => sew(() =>
withAuth(async ({ prisma, org }) => {
const skip = (page - 1) * perPage;

const [connections, totalCount] = await Promise.all([
prisma.connection.findMany({
where: { orgId: org.id },
orderBy: { name: 'asc' },
skip,
take: perPage,
include: {
_count: {
select: {
repos: true,
syncJobs: true,
},
},
syncJobs: {
orderBy: { createdAt: 'desc' },
take: 1,
},
},
}),
prisma.connection.count({ where: { orgId: org.id } }),
]);

// Count in-flight jobs (PENDING + IN_PROGRESS) per connection in
// a single grouped query rather than N+1 small counts.
const inFlightByConnection = await prisma.connectionSyncJob.groupBy({
by: ['connectionId'],
where: {
connectionId: { in: connections.map((c) => c.id) },
status: {
in: [ConnectionSyncJobStatus.PENDING, ConnectionSyncJobStatus.IN_PROGRESS],
},
},
_count: { _all: true },
});
const inFlightMap = new Map(
inFlightByConnection.map((row) => [row.connectionId, row._count._all]),
);

return {
data: connections.map((connection) => {
const latestJob = connection.syncJobs[0] ?? null;
return {
id: connection.id,
name: connection.name,
connectionType: connection.connectionType,
isDeclarative: connection.isDeclarative,
syncedAt: connection.syncedAt,
createdAt: connection.createdAt,
updatedAt: connection.updatedAt,
repoCount: connection._count.repos,
inFlightJobCount: inFlightMap.get(connection.id) ?? 0,
latestJob: latestJob
? {
id: latestJob.id,
status: latestJob.status,
createdAt: latestJob.createdAt,
completedAt: latestJob.completedAt,
errorMessage: latestJob.errorMessage,
}
: null,
};
}),
totalCount,
};
}),
);
Loading