Skip to content
This repository was archived by the owner on Sep 9, 2024. It is now read-only.

Commit bbd1f99

Browse files
committed
Support app tokens for Github backend
1 parent 734cecd commit bbd1f99

File tree

6 files changed

+89
-16
lines changed

6 files changed

+89
-16
lines changed

packages/core/src/backends/git-gateway/__tests__/GitHubAPI.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const createApi = (options: Partial<GitHubApiOptions> = {}) => {
1212
cmsLabelPrefix: 'CMS',
1313
isLargeMedia: () => Promise.resolve(false),
1414
commitAuthor: { name: 'Bob' },
15+
getUser: () => Promise.reject('Unexpected call'),
16+
getRepo: () => Promise.reject('Unexpected call'),
1517
...options,
1618
});
1719
};

packages/core/src/backends/git-gateway/implementation.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,13 @@ export default class GitGateway implements BackendClass {
316316
};
317317

318318
if (this.backendType === 'github') {
319-
this.api = new GitHubAPI(apiConfig);
320-
this.backend = new GitHubBackend(this.config, { ...this.options, API: this.api });
319+
this.backend = new GitHubBackend(this.config, { ...this.options });
320+
this.api = new GitHubAPI({
321+
...apiConfig,
322+
getUser: this.backend.currentUser,
323+
getRepo: this.backend.getRepo,
324+
});
325+
this.backend.api = this.api;
321326
} else if (this.backendType === 'gitlab') {
322327
this.api = new GitLabAPI(apiConfig);
323328
this.backend = new GitLabBackend(this.config, { ...this.options, API: this.api });

packages/core/src/backends/github/API.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface Config {
7676
apiRoot?: string;
7777
token?: string;
7878
authScheme?: AuthScheme;
79+
authenticateAsGithubApp?: boolean;
7980
branch?: string;
8081
useOpenAuthoring?: boolean;
8182
openAuthoringEnabled?: boolean;
@@ -84,6 +85,8 @@ export interface Config {
8485
squashMerges: boolean;
8586
initialWorkflowStatus: WorkflowStatus;
8687
cmsLabelPrefix: string;
88+
getUser: ({ token }: { token: string }) => Promise<GitHubUser>;
89+
getRepo: ({ token }: { token: string }) => Promise<ReposGetResponse>;
8790
}
8891

8992
type Override<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
@@ -164,6 +167,7 @@ export default class API {
164167
apiRoot: string;
165168
token: string;
166169
authScheme: AuthScheme;
170+
authenticateAsGithubApp: boolean;
167171
branch: string;
168172
useOpenAuthoring?: boolean;
169173
openAuthoringEnabled?: boolean;
@@ -180,14 +184,18 @@ export default class API {
180184
cmsLabelPrefix: string;
181185

182186
_userPromise?: Promise<GitHubUser>;
187+
_repoPromise?: Promise<ReposGetResponse>;
183188
_metadataSemaphore?: Semaphore;
184189

190+
getUser: ({ token }: { token: string }) => Promise<GitHubUser>;
191+
getRepo: ({ token }: { token: string }) => Promise<ReposGetResponse>;
185192
commitAuthor?: {};
186193

187194
constructor(config: Config) {
188195
this.apiRoot = config.apiRoot || 'https://api.github.com';
189196
this.token = config.token || '';
190197
this.authScheme = config.authScheme || 'token';
198+
this.authenticateAsGithubApp = config.authenticateAsGithubApp || false;
191199
this.branch = config.branch || 'main';
192200
this.useOpenAuthoring = config.useOpenAuthoring;
193201
this.repo = config.repo || '';
@@ -207,26 +215,30 @@ export default class API {
207215
this.cmsLabelPrefix = config.cmsLabelPrefix;
208216
this.initialWorkflowStatus = config.initialWorkflowStatus;
209217
this.openAuthoringEnabled = config.openAuthoringEnabled;
218+
219+
this.getUser = config.getUser;
220+
this.getRepo = config.getRepo;
210221
}
211222

212223
static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Static CMS';
213224

214-
user(): Promise<{ name: string; login: string }> {
225+
user(): Promise<{ name: string; login: string; avatar_url?: string }> {
215226
if (!this._userPromise) {
216-
this._userPromise = this.getUser();
227+
this._userPromise = this.getUser({ token: this.token });
217228
}
218229
return this._userPromise;
219230
}
220231

221-
getUser() {
222-
return this.request('/user') as Promise<GitHubUser>;
223-
}
224-
225232
async hasWriteAccess() {
226233
try {
227-
const result: ReposGetResponse = await this.request(this.repoURL);
234+
const result: ReposGetResponse = await this.getRepo({ token: this.token });
228235
// update config repoOwner to avoid case sensitivity issues with GitHub
229236
this.repoOwner = result.owner.login;
237+
238+
// Github App tokens won't have permissions set appropriately in the response, so we bypass that check
239+
if (this.authenticateAsGithubApp) {
240+
return true;
241+
}
230242
return result.permissions.push;
231243
} catch (error) {
232244
console.error('Problem fetching repo data from GitHub');

packages/core/src/backends/github/__tests__/API.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ describe('github API', () => {
3131
squashMerges: false,
3232
initialWorkflowStatus: WorkflowStatus.DRAFT,
3333
cmsLabelPrefix: '',
34+
getUser: () => Promise.reject('Unexpected call'),
35+
getRepo: () => Promise.reject('Unexpected call'),
3436
});
3537

3638
api.createTree = jest.fn().mockImplementation(() => Promise.resolve({ sha: 'newTreeSha' }));
@@ -83,6 +85,8 @@ describe('github API', () => {
8385
squashMerges: false,
8486
initialWorkflowStatus: WorkflowStatus.DRAFT,
8587
cmsLabelPrefix: '',
88+
getUser: () => Promise.reject('Unexpected call'),
89+
getRepo: () => Promise.reject('Unexpected call'),
8690
});
8791

8892
fetch.mockResolvedValue({
@@ -112,6 +116,8 @@ describe('github API', () => {
112116
squashMerges: false,
113117
initialWorkflowStatus: WorkflowStatus.DRAFT,
114118
cmsLabelPrefix: '',
119+
getUser: () => Promise.reject('Unexpected call'),
120+
getRepo: () => Promise.reject('Unexpected call'),
115121
});
116122

117123
fetch.mockResolvedValue({
@@ -140,6 +146,8 @@ describe('github API', () => {
140146
squashMerges: false,
141147
initialWorkflowStatus: WorkflowStatus.DRAFT,
142148
cmsLabelPrefix: '',
149+
getUser: () => Promise.reject('Unexpected call'),
150+
getRepo: () => Promise.reject('Unexpected call'),
143151
});
144152

145153
fetch.mockResolvedValue({
@@ -167,6 +175,8 @@ describe('github API', () => {
167175
squashMerges: false,
168176
initialWorkflowStatus: WorkflowStatus.DRAFT,
169177
cmsLabelPrefix: '',
178+
getUser: () => Promise.reject('Unexpected call'),
179+
getRepo: () => Promise.reject('Unexpected call'),
170180
});
171181

172182
api.requestHeaders = jest.fn().mockResolvedValue({
@@ -201,6 +211,8 @@ describe('github API', () => {
201211
squashMerges: false,
202212
initialWorkflowStatus: WorkflowStatus.DRAFT,
203213
cmsLabelPrefix: '',
214+
getUser: () => Promise.reject('Unexpected call'),
215+
getRepo: () => Promise.reject('Unexpected call'),
204216
});
205217

206218
const responses = {
@@ -305,6 +317,8 @@ describe('github API', () => {
305317
squashMerges: false,
306318
initialWorkflowStatus: WorkflowStatus.DRAFT,
307319
cmsLabelPrefix: '',
320+
getUser: () => Promise.reject('Unexpected call'),
321+
getRepo: () => Promise.reject('Unexpected call'),
308322
});
309323

310324
const tree = [
@@ -391,6 +405,8 @@ describe('github API', () => {
391405
squashMerges: false,
392406
initialWorkflowStatus: WorkflowStatus.DRAFT,
393407
cmsLabelPrefix: '',
408+
getUser: () => Promise.reject('Unexpected call'),
409+
getRepo: () => Promise.reject('Unexpected call'),
394410
});
395411

396412
const tree = [

packages/core/src/backends/github/implementation.tsx

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import type {
4040
import type { AsyncLock } from '@staticcms/core/lib/util';
4141
import type AssetProxy from '@staticcms/core/valueObjects/AssetProxy';
4242
import type { Semaphore } from 'semaphore';
43-
import type { GitHubUser } from './types';
43+
import type { GitHubUser, ReposGetResponse } from './types';
4444

4545
const MAX_CONCURRENT_DOWNLOADS = 10;
4646

@@ -77,9 +77,11 @@ export default class GitHub implements BackendClass {
7777
previewContext: string;
7878
token: string | null;
7979
authScheme: AuthScheme;
80+
authenticateAsGithubApp: boolean;
8081
squashMerges: boolean;
8182
cmsLabelPrefix: string;
8283
_currentUserPromise?: Promise<GitHubUser>;
84+
_getRepoPromise?: Promise<ReposGetResponse>;
8385
_userIsOriginMaintainerPromises?: {
8486
[key: string]: Promise<boolean>;
8587
};
@@ -117,6 +119,7 @@ export default class GitHub implements BackendClass {
117119
this.apiRoot = config.backend.api_root || 'https://api.github.com';
118120
this.token = '';
119121
this.authScheme = config.backend.auth_scheme || 'token';
122+
this.authenticateAsGithubApp = config.backend.authenticate_as_github_app || false;
120123
this.squashMerges = config.backend.squash_merges || false;
121124
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
122125
this.mediaFolder = config.media_folder;
@@ -145,8 +148,7 @@ export default class GitHub implements BackendClass {
145148
// no need to check auth if api is down
146149
if (api) {
147150
auth =
148-
(await this.api
149-
?.getUser()
151+
(await this.currentUser({ token: this.token || '' })
150152
.then(user => !!user)
151153
.catch(e => {
152154
console.warn('[StaticCMS] Failed getting GitHub user', e);
@@ -193,14 +195,46 @@ export default class GitHub implements BackendClass {
193195
return Promise.resolve();
194196
}
195197

196-
async currentUser({ token }: { token: string }) {
197-
if (!this._currentUserPromise) {
198-
this._currentUserPromise = fetch(`${this.apiRoot}/user`, {
198+
async getRepo({ token }: { token: string }) {
199+
if (!this._getRepoPromise) {
200+
this._getRepoPromise = fetch(`${this.apiRoot}/repos/${this.repo}`, {
199201
headers: {
200202
Authorization: `${this.authScheme} ${token}`,
201203
},
202204
}).then(res => res.json());
203205
}
206+
return this._getRepoPromise;
207+
}
208+
209+
async getApp({ token }: { token: string }) {
210+
return fetch(`${this.apiRoot}/app`, {
211+
headers: {
212+
Authorization: `${this.authScheme} ${token}`,
213+
},
214+
})
215+
.then(res => res.json())
216+
.then(
217+
res =>
218+
({
219+
name: res.name,
220+
login: res.slug,
221+
avatar_url: `https://avatars.githubusercontent.com/in/${res.id}?v=4`,
222+
}) as GitHubUser,
223+
);
224+
}
225+
226+
async currentUser({ token }: { token: string }) {
227+
if (!this._currentUserPromise) {
228+
if (this.authenticateAsGithubApp) {
229+
this._currentUserPromise = this.getApp({ token });
230+
} else {
231+
this._currentUserPromise = fetch(`${this.apiRoot}/user`, {
232+
headers: {
233+
Authorization: `${this.authScheme} ${token}`,
234+
},
235+
}).then(res => res.json());
236+
}
237+
}
204238
return this._currentUserPromise;
205239
}
206240

@@ -293,6 +327,7 @@ export default class GitHub implements BackendClass {
293327
this.api = new apiCtor({
294328
token: this.token,
295329
authScheme: this.authScheme,
330+
authenticateAsGithubApp: this.authenticateAsGithubApp,
296331
branch: this.branch,
297332
repo: this.repo,
298333
originRepo: this.originRepo,
@@ -302,8 +337,10 @@ export default class GitHub implements BackendClass {
302337
useOpenAuthoring: this.useOpenAuthoring,
303338
openAuthoringEnabled: this.openAuthoringEnabled,
304339
initialWorkflowStatus: this.options.initialWorkflowStatus,
340+
getUser: this.currentUser,
341+
getRepo: this.getRepo,
305342
});
306-
const user = await this.api!.user();
343+
const user = await this.currentUser({ token: this.token });
307344
const isCollab = await this.api!.hasWriteAccess().catch(error => {
308345
error.message = stripIndent`
309346
Repo "${this.repo}" not found.

packages/core/src/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,7 @@ export interface Backend {
10281028
gateway_url?: string;
10291029
auth_scope?: AuthScope;
10301030
auth_scheme?: AuthScheme;
1031+
authenticate_as_github_app?: boolean;
10311032
commit_messages?: {
10321033
create?: string;
10331034
update?: string;

0 commit comments

Comments
 (0)