Skip to content

Commit cde26d2

Browse files
authored
Merge pull request #242 from austenstone/diagnostics-for-account-and-installations
Diagnostics-for-account-and-installations
2 parents 9684fb7 + 51b7898 commit cde26d2

File tree

15 files changed

+1080
-33
lines changed

15 files changed

+1080
-33
lines changed

backend/src/controllers/setup.controller.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,61 @@ import app from '../index.js';
33
import StatusService from '../services/status.service.js';
44
import logger from '../services/logger.js';
55

6+
// Type definitions for the diagnostic response
7+
interface OctokitTestResult {
8+
success: boolean;
9+
appName?: string;
10+
appOwner?: string;
11+
permissions?: Record<string, string | undefined>;
12+
error?: string;
13+
}
14+
15+
interface InstallationDiagnostic {
16+
index: number;
17+
installationId: number;
18+
accountLogin: string;
19+
accountId: string | number;
20+
accountType: string;
21+
accountAvatarUrl: string;
22+
appId: number;
23+
appSlug: string;
24+
targetType: string;
25+
permissions: Record<string, string | undefined>;
26+
events: string[];
27+
createdAt: string;
28+
updatedAt: string;
29+
suspendedAt: string | null;
30+
suspendedBy: { login: string; id: number } | null;
31+
hasOctokit: boolean;
32+
octokitTest: OctokitTestResult | null;
33+
isValid: boolean;
34+
validationErrors: string[];
35+
}
36+
37+
interface AppInfo {
38+
name: string;
39+
description: string;
40+
owner: string;
41+
htmlUrl: string;
42+
permissions: Record<string, string | undefined>;
43+
events: string[];
44+
}
45+
46+
interface DiagnosticsResponse {
47+
timestamp: string;
48+
appConnected: boolean;
49+
totalInstallations: number;
50+
installations: InstallationDiagnostic[];
51+
errors: string[];
52+
appInfo: AppInfo | null;
53+
summary: {
54+
validInstallations: number;
55+
invalidInstallations: number;
56+
organizationNames: string[];
57+
accountTypes: Record<string, number>;
58+
};
59+
}
60+
661
class SetupController {
762
async registrationComplete(req: Request, res: Response) {
863
try {
@@ -112,6 +167,140 @@ class SetupController {
112167
}
113168
}
114169

170+
async validateInstallations(req: Request, res: Response) {
171+
try {
172+
const diagnostics: DiagnosticsResponse = {
173+
timestamp: new Date().toISOString(),
174+
appConnected: !!app.github.app,
175+
totalInstallations: app.github.installations.length,
176+
installations: [],
177+
errors: [],
178+
appInfo: null,
179+
summary: {
180+
validInstallations: 0,
181+
invalidInstallations: 0,
182+
organizationNames: [],
183+
accountTypes: {}
184+
}
185+
};
186+
187+
// Basic app validation
188+
if (!app.github.app) {
189+
diagnostics.errors.push('GitHub App is not initialized');
190+
return res.json(diagnostics);
191+
}
192+
193+
// Validate each installation
194+
for (let i = 0; i < app.github.installations.length; i++) {
195+
const { installation, octokit } = app.github.installations[i];
196+
197+
const installationDiag: InstallationDiagnostic = {
198+
index: i,
199+
installationId: installation.id,
200+
accountLogin: installation.account?.login || 'MISSING',
201+
accountId: installation.account?.id || 'MISSING',
202+
accountType: installation.account?.type || 'MISSING',
203+
accountAvatarUrl: installation.account?.avatar_url || 'MISSING',
204+
appId: installation.app_id,
205+
appSlug: installation.app_slug,
206+
targetType: installation.target_type,
207+
permissions: installation.permissions || {},
208+
events: installation.events || [],
209+
createdAt: installation.created_at,
210+
updatedAt: installation.updated_at,
211+
suspendedAt: installation.suspended_at,
212+
suspendedBy: installation.suspended_by,
213+
hasOctokit: !!octokit,
214+
octokitTest: null,
215+
isValid: true,
216+
validationErrors: []
217+
};
218+
219+
// Validate required fields
220+
if (!installation.account?.login) {
221+
installationDiag.isValid = false;
222+
installationDiag.validationErrors.push('Missing account.login (organization name)');
223+
}
224+
225+
if (!installation.account?.id) {
226+
installationDiag.isValid = false;
227+
installationDiag.validationErrors.push('Missing account.id');
228+
}
229+
230+
if (!installation.account?.type) {
231+
installationDiag.isValid = false;
232+
installationDiag.validationErrors.push('Missing account.type');
233+
}
234+
235+
// Test Octokit functionality
236+
if (octokit) {
237+
try {
238+
// Test basic API call with the installation's octokit
239+
const authTest = await octokit.rest.apps.getAuthenticated();
240+
installationDiag.octokitTest = {
241+
success: true,
242+
appName: authTest.data?.name || 'Unknown',
243+
appOwner: (authTest.data?.owner && 'login' in authTest.data.owner) ? authTest.data.owner.login : 'Unknown',
244+
permissions: authTest.data?.permissions || {}
245+
};
246+
} catch (error) {
247+
installationDiag.octokitTest = {
248+
success: false,
249+
error: error instanceof Error ? error.message : 'Unknown error'
250+
};
251+
installationDiag.isValid = false;
252+
installationDiag.validationErrors.push(`Octokit API test failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
253+
}
254+
} else {
255+
installationDiag.isValid = false;
256+
installationDiag.validationErrors.push('Octokit instance is missing');
257+
}
258+
259+
// Update summary
260+
if (installationDiag.isValid) {
261+
diagnostics.summary.validInstallations++;
262+
if (installation.account?.login) {
263+
diagnostics.summary.organizationNames.push(installation.account.login);
264+
}
265+
} else {
266+
diagnostics.summary.invalidInstallations++;
267+
}
268+
269+
// Track account types
270+
const accountType = installation.account?.type || 'Unknown';
271+
diagnostics.summary.accountTypes[accountType] = (diagnostics.summary.accountTypes[accountType] || 0) + 1;
272+
273+
diagnostics.installations.push(installationDiag);
274+
}
275+
276+
// Additional app-level diagnostics
277+
try {
278+
const appInfo = await app.github.app.octokit.rest.apps.getAuthenticated();
279+
diagnostics.appInfo = {
280+
name: appInfo.data?.name || 'Unknown',
281+
description: appInfo.data?.description || 'No description',
282+
owner: (appInfo.data?.owner && 'login' in appInfo.data.owner) ? appInfo.data.owner.login : 'Unknown',
283+
htmlUrl: appInfo.data?.html_url || 'Unknown',
284+
permissions: appInfo.data?.permissions || {},
285+
events: appInfo.data?.events || []
286+
};
287+
} catch (error) {
288+
diagnostics.errors.push(`Failed to get app info: ${error instanceof Error ? error.message : 'Unknown error'}`);
289+
}
290+
291+
// Sort organization names for easier reading
292+
diagnostics.summary.organizationNames.sort();
293+
294+
res.json(diagnostics);
295+
} catch (error) {
296+
logger.error('Installation validation failed', error);
297+
res.status(500).json({
298+
error: 'Installation validation failed',
299+
details: error instanceof Error ? error.message : 'Unknown error'
300+
});
301+
}
302+
}
303+
115304

116305
}
117306

backend/src/routes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ router.get('/setup/manifest', setupController.getManifest);
5252
router.post('/setup/existing-app', setupController.addExistingApp);
5353
router.post('/setup/db', setupController.setupDB);
5454
router.get('/setup/status', setupController.setupStatus);
55+
router.get('/setup/validate-installations', setupController.validateInstallations);
5556

5657
router.get('/status', setupController.getStatus);
5758

backend/src/services/status.service.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface StatusType {
1111
};
1212
installations: {
1313
installation: Endpoints["GET /app/installations"]["response"]["data"][0]
14-
repos: Endpoints["GET /installation/repositories"]["response"]["data"]["repositories"];
14+
repos: Endpoints["GET /app/installations"]["response"]["data"];
1515
}[];
1616
surveyCount: number;
1717
auth?: {
@@ -56,12 +56,10 @@ class StatusService {
5656

5757
status.installations = [];
5858
for (const installation of app.github.installations) {
59-
const repos = await installation.octokit.paginate<Endpoints["GET /installation/repositories"]["response"]["data"]["repositories"][0]>(
60-
installation.installation.repositories_url
61-
);
59+
const repos = await installation.octokit.request(installation.installation.repositories_url);
6260
status.installations.push({
6361
installation: installation.installation,
64-
repos: repos
62+
repos: repos.data.repositories
6563
});
6664
}
6765

0 commit comments

Comments
 (0)