Skip to content

Commit 6b10a85

Browse files
fix(backend): warn on missing GitHub App org installation
1 parent 698885c commit 6b10a85

3 files changed

Lines changed: 97 additions & 10 deletions

File tree

packages/backend/src/ee/githubAppManager.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ type Installation = {
1616
};
1717
};
1818

19+
export class GithubAppInstallationNotFoundError extends Error {
20+
constructor(owner: string, deploymentHostname: string) {
21+
super(`GitHub App installation not found for ${deploymentHostname}/${owner}`);
22+
this.name = 'GithubAppInstallationNotFoundError';
23+
}
24+
}
25+
1926
export class GithubAppManager {
2027
private static instance: GithubAppManager | null = null;
2128
private octokitApps: Map<number, App>;
@@ -112,7 +119,7 @@ export class GithubAppManager {
112119
const key = this.generateMapKey(owner, deploymentHostname);
113120
const installation = this.installationMap.get(key) as Installation | undefined;
114121
if (!installation) {
115-
throw new Error(`GitHub App Installation not found for ${key}`);
122+
throw new GithubAppInstallationNotFoundError(owner, deploymentHostname);
116123
}
117124

118125
const octokitApp = this.octokitApps.get(installation.appId) as App;

packages/backend/src/github.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { hasEntitlement } from "./entitlements.js";
99
import micromatch from "micromatch";
1010
import pLimit from "p-limit";
1111
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
12-
import { GithubAppManager } from "./ee/githubAppManager.js";
12+
import { GithubAppInstallationNotFoundError, GithubAppManager } from "./ee/githubAppManager.js";
1313
import { fetchWithRetry, measure } from "./utils.js";
1414

1515
export const GITHUB_CLOUD_HOSTNAME = "github.com";
@@ -145,6 +145,10 @@ export const getOctokitWithGithubApp = async (
145145
});
146146
return octokitFromToken;
147147
} catch (error) {
148+
if (error instanceof GithubAppInstallationNotFoundError) {
149+
throw error;
150+
}
151+
148152
logger.error(`Error getting GitHub App token for ${context}.`, error);
149153
throw error;
150154
}
@@ -403,6 +407,15 @@ const getReposForOrgs = async (orgs: string[], octokit: Octokit, signal: AbortSi
403407
data
404408
};
405409
} catch (error) {
410+
if (error instanceof GithubAppInstallationNotFoundError) {
411+
const warning = error.message;
412+
logger.warn(warning);
413+
return {
414+
type: 'warning' as const,
415+
warning
416+
};
417+
}
418+
406419
Sentry.captureException(error);
407420
logger.error(`Failed to fetch repositories for org ${org}.`, error);
408421

packages/backend/src/githubAppAuth.test.ts

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,61 @@
1-
import type { Octokit } from '@octokit/rest';
21
import { beforeEach, describe, expect, test, vi } from 'vitest';
32

43
const mocks = vi.hoisted(() => ({
54
appsConfigured: vi.fn(),
65
ensureInitialized: vi.fn(),
76
getInstallationToken: vi.fn(),
87
hasEntitlement: vi.fn(),
8+
logger: {
9+
debug: vi.fn(),
10+
error: vi.fn(),
11+
info: vi.fn(),
12+
warn: vi.fn(),
13+
},
14+
GithubAppInstallationNotFoundError: class GithubAppInstallationNotFoundError extends Error {
15+
constructor(owner: string, deploymentHostname: string) {
16+
super(`GitHub App installation not found for ${deploymentHostname}/${owner}`);
17+
this.name = 'GithubAppInstallationNotFoundError';
18+
}
19+
},
20+
}));
21+
22+
vi.mock('@octokit/rest', () => ({
23+
Octokit: class {
24+
public paginate = {
25+
iterator: async function* (_request: unknown, options: { org: string }) {
26+
yield {
27+
data: [{
28+
clone_url: `https://github.com/${options.org}/repo.git`,
29+
full_name: `${options.org}/repo`,
30+
id: 1,
31+
name: 'repo',
32+
owner: {
33+
avatar_url: '',
34+
login: options.org,
35+
},
36+
}],
37+
};
38+
},
39+
};
40+
41+
public repos = {
42+
listForOrg: vi.fn(),
43+
};
44+
45+
public rest = {
46+
users: {
47+
getAuthenticated: vi.fn(),
48+
},
49+
};
50+
},
951
}));
1052

1153
vi.mock('@sentry/node', () => ({
1254
captureException: vi.fn(),
1355
}));
1456

1557
vi.mock('@sourcebot/shared', () => ({
16-
createLogger: vi.fn(() => ({
17-
debug: vi.fn(),
18-
error: vi.fn(),
19-
info: vi.fn(),
20-
warn: vi.fn(),
21-
})),
58+
createLogger: vi.fn(() => mocks.logger),
2259
env: {
2360
FALLBACK_GITHUB_CLOUD_TOKEN: undefined,
2461
},
@@ -30,6 +67,7 @@ vi.mock('./entitlements.js', () => ({
3067
}));
3168

3269
vi.mock('./ee/githubAppManager.js', () => ({
70+
GithubAppInstallationNotFoundError: mocks.GithubAppInstallationNotFoundError,
3371
GithubAppManager: {
3472
getInstance: () => ({
3573
appsConfigured: mocks.appsConfigured,
@@ -39,14 +77,19 @@ vi.mock('./ee/githubAppManager.js', () => ({
3977
},
4078
}));
4179

42-
import { getOctokitWithGithubApp } from './github.js';
80+
import type { Octokit } from '@octokit/rest';
81+
import { getGitHubReposFromConfig, getOctokitWithGithubApp } from './github.js';
4382

4483
describe('getOctokitWithGithubApp', () => {
4584
beforeEach(() => {
4685
mocks.appsConfigured.mockReset().mockReturnValue(true);
4786
mocks.ensureInitialized.mockReset().mockResolvedValue(undefined);
4887
mocks.getInstallationToken.mockReset().mockResolvedValue('installation-token');
4988
mocks.hasEntitlement.mockReset();
89+
mocks.logger.debug.mockReset();
90+
mocks.logger.error.mockReset();
91+
mocks.logger.info.mockReset();
92+
mocks.logger.warn.mockReset();
5093
});
5194

5295
test('fails safely, then uses the GitHub App when the entitlement appears after startup', async () => {
@@ -101,4 +144,28 @@ describe('getOctokitWithGithubApp', () => {
101144
'org example',
102145
)).rejects.toBe(error);
103146
});
147+
148+
test('warns and continues when the GitHub App is not installed for one organization', async () => {
149+
mocks.hasEntitlement.mockResolvedValue(true);
150+
mocks.getInstallationToken.mockImplementation(async (owner: string) => {
151+
if (owner === 'invalid-org') {
152+
throw new mocks.GithubAppInstallationNotFoundError(owner, 'github.com');
153+
}
154+
return 'installation-token';
155+
});
156+
157+
const result = await getGitHubReposFromConfig({
158+
type: 'github',
159+
orgs: ['valid-org', 'invalid-org'],
160+
}, new AbortController().signal);
161+
162+
expect(result.repos.map(repo => repo.full_name)).toEqual(['valid-org/repo']);
163+
expect(result.warnings).toEqual([
164+
'GitHub App installation not found for github.com/invalid-org',
165+
]);
166+
expect(mocks.logger.warn).toHaveBeenCalledWith(
167+
'GitHub App installation not found for github.com/invalid-org',
168+
);
169+
expect(mocks.logger.error).not.toHaveBeenCalled();
170+
});
104171
});

0 commit comments

Comments
 (0)