-
Notifications
You must be signed in to change notification settings - Fork 359
Expand file tree
/
Copy pathgithubAppManager.ts
More file actions
138 lines (114 loc) · 5.23 KB
/
Copy pathgithubAppManager.ts
File metadata and controls
138 lines (114 loc) · 5.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { App } from "@octokit/app";
import { getTokenFromConfig } from "@sourcebot/shared";
import { createLogger } from "@sourcebot/shared";
import { GitHubAppConfig } from "@sourcebot/schemas/v3/index.type";
import { env, loadConfig } from "@sourcebot/shared";
const logger = createLogger('githubAppManager');
const GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME = 'github.com';
type Installation = {
id: number;
appId: number;
account: {
login: string;
type: 'organization' | 'user';
};
};
export class GithubAppInstallationNotFoundError extends Error {
constructor(owner: string, deploymentHostname: string) {
super(`GitHub App installation not found for ${deploymentHostname}/${owner}`);
this.name = 'GithubAppInstallationNotFoundError';
}
}
export class GithubAppManager {
private static instance: GithubAppManager | null = null;
private octokitApps: Map<number, App>;
private installationMap: Map<string, Installation>;
private initialized: boolean = false;
private initializationPromise: Promise<void> | null = null;
private constructor() {
this.octokitApps = new Map<number, App>();
this.installationMap = new Map<string, Installation>();
}
public static getInstance(): GithubAppManager {
if (!GithubAppManager.instance) {
GithubAppManager.instance = new GithubAppManager();
}
return GithubAppManager.instance;
}
private assertInitialized(): void {
if (!this.initialized) {
throw new Error('GithubAppManager must be initialized before use. Call ensureInitialized() first.');
}
}
public async ensureInitialized(): Promise<void> {
if (this.initialized) {
return;
}
if (!this.initializationPromise) {
this.initializationPromise = this.init().catch((error) => {
// Allow a later operation to retry after a transient GitHub or
// secret-resolution failure.
this.initializationPromise = null;
throw error;
});
}
await this.initializationPromise;
}
private async init(): Promise<void> {
const config = await loadConfig(env.CONFIG_PATH);
if (!config.apps) {
this.initialized = true;
return;
}
const githubApps = config.apps.filter(app => app.type === 'github') as GitHubAppConfig[];
logger.info(`Found ${githubApps.length} GitHub apps in config`);
for (const app of githubApps) {
const deploymentHostname = app.deploymentHostname as string || GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME;
const privateKey = await getTokenFromConfig(app.privateKey);
const octokitApp = new App({
appId: Number(app.id),
privateKey: privateKey,
});
this.octokitApps.set(Number(app.id), octokitApp);
const installations = await octokitApp.octokit.request("GET /app/installations");
logger.info(`Found ${installations.data.length} GitHub App installations for ${deploymentHostname}/${app.id}:`);
for (const installationData of installations.data) {
if (!installationData.account || !installationData.account.login || !installationData.account.type) {
logger.warn(`Skipping installation ${installationData.id}: missing account data (${installationData.account})`);
continue;
}
logger.info(`\tInstallation ID: ${installationData.id}, Account: ${installationData.account.login}, Type: ${installationData.account.type}`);
const owner = installationData.account.login;
const accountType = installationData.account.type.toLowerCase() as 'organization' | 'user';
const installation: Installation = {
id: installationData.id,
appId: Number(app.id),
account: {
login: owner,
type: accountType,
},
};
this.installationMap.set(this.generateMapKey(owner, deploymentHostname), installation);
}
}
this.initialized = true;
}
public async getInstallationToken(owner: string, deploymentHostname: string = GITHUB_DEFAULT_DEPLOYMENT_HOSTNAME): Promise<string> {
this.assertInitialized();
const key = this.generateMapKey(owner, deploymentHostname);
const installation = this.installationMap.get(key) as Installation | undefined;
if (!installation) {
throw new GithubAppInstallationNotFoundError(owner, deploymentHostname);
}
const octokitApp = this.octokitApps.get(installation.appId) as App;
const installationOctokit = await octokitApp.getInstallationOctokit(installation.id);
const auth = await installationOctokit.auth({ type: "installation" }) as { expires_at: string, token: string };
return auth.token;
}
public appsConfigured() {
return this.octokitApps.size > 0;
}
private generateMapKey(owner: string, deploymentHostname: string): string {
return `${deploymentHostname}/${owner}`;
}
}