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

Commit

Permalink
Support app tokens for Github backend
Browse files Browse the repository at this point in the history
  • Loading branch information
soceanainn committed Apr 3, 2024
1 parent 734cecd commit bbd1f99
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const createApi = (options: Partial<GitHubApiOptions> = {}) => {
cmsLabelPrefix: 'CMS',
isLargeMedia: () => Promise.resolve(false),
commitAuthor: { name: 'Bob' },
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
...options,
});
};
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/backends/git-gateway/implementation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,13 @@ export default class GitGateway implements BackendClass {
};

if (this.backendType === 'github') {
this.api = new GitHubAPI(apiConfig);
this.backend = new GitHubBackend(this.config, { ...this.options, API: this.api });
this.backend = new GitHubBackend(this.config, { ...this.options });
this.api = new GitHubAPI({
...apiConfig,
getUser: this.backend.currentUser,
getRepo: this.backend.getRepo,
});
this.backend.api = this.api;
} else if (this.backendType === 'gitlab') {
this.api = new GitLabAPI(apiConfig);
this.backend = new GitLabBackend(this.config, { ...this.options, API: this.api });
Expand Down
26 changes: 19 additions & 7 deletions packages/core/src/backends/github/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface Config {
apiRoot?: string;
token?: string;
authScheme?: AuthScheme;
authenticateAsGithubApp?: boolean;
branch?: string;
useOpenAuthoring?: boolean;
openAuthoringEnabled?: boolean;
Expand All @@ -84,6 +85,8 @@ export interface Config {
squashMerges: boolean;
initialWorkflowStatus: WorkflowStatus;
cmsLabelPrefix: string;
getUser: ({ token }: { token: string }) => Promise<GitHubUser>;
getRepo: ({ token }: { token: string }) => Promise<ReposGetResponse>;
}

type Override<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
Expand Down Expand Up @@ -164,6 +167,7 @@ export default class API {
apiRoot: string;
token: string;
authScheme: AuthScheme;
authenticateAsGithubApp: boolean;
branch: string;
useOpenAuthoring?: boolean;
openAuthoringEnabled?: boolean;
Expand All @@ -180,14 +184,18 @@ export default class API {
cmsLabelPrefix: string;

_userPromise?: Promise<GitHubUser>;
_repoPromise?: Promise<ReposGetResponse>;
_metadataSemaphore?: Semaphore;

getUser: ({ token }: { token: string }) => Promise<GitHubUser>;
getRepo: ({ token }: { token: string }) => Promise<ReposGetResponse>;
commitAuthor?: {};

constructor(config: Config) {
this.apiRoot = config.apiRoot || 'https://api.github.com';
this.token = config.token || '';
this.authScheme = config.authScheme || 'token';
this.authenticateAsGithubApp = config.authenticateAsGithubApp || false;
this.branch = config.branch || 'main';
this.useOpenAuthoring = config.useOpenAuthoring;
this.repo = config.repo || '';
Expand All @@ -207,26 +215,30 @@ export default class API {
this.cmsLabelPrefix = config.cmsLabelPrefix;
this.initialWorkflowStatus = config.initialWorkflowStatus;
this.openAuthoringEnabled = config.openAuthoringEnabled;

this.getUser = config.getUser;
this.getRepo = config.getRepo;
}

static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Static CMS';

user(): Promise<{ name: string; login: string }> {
user(): Promise<{ name: string; login: string; avatar_url?: string }> {

Check warning on line 225 in packages/core/src/backends/github/API.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L225 was not covered by tests
if (!this._userPromise) {
this._userPromise = this.getUser();
this._userPromise = this.getUser({ token: this.token });

Check warning on line 227 in packages/core/src/backends/github/API.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L227 was not covered by tests
}
return this._userPromise;
}

getUser() {
return this.request('/user') as Promise<GitHubUser>;
}

async hasWriteAccess() {
try {
const result: ReposGetResponse = await this.request(this.repoURL);
const result: ReposGetResponse = await this.getRepo({ token: this.token });

Check warning on line 234 in packages/core/src/backends/github/API.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L234 was not covered by tests
// update config repoOwner to avoid case sensitivity issues with GitHub
this.repoOwner = result.owner.login;

// Github App tokens won't have permissions set appropriately in the response, so we bypass that check
if (this.authenticateAsGithubApp) {
return true;

Check warning on line 240 in packages/core/src/backends/github/API.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L240 was not covered by tests
}
return result.permissions.push;
} catch (error) {
console.error('Problem fetching repo data from GitHub');
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/backends/github/__tests__/API.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ describe('github API', () => {
squashMerges: false,
initialWorkflowStatus: WorkflowStatus.DRAFT,
cmsLabelPrefix: '',
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
});

api.createTree = jest.fn().mockImplementation(() => Promise.resolve({ sha: 'newTreeSha' }));
Expand Down Expand Up @@ -83,6 +85,8 @@ describe('github API', () => {
squashMerges: false,
initialWorkflowStatus: WorkflowStatus.DRAFT,
cmsLabelPrefix: '',
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
});

fetch.mockResolvedValue({
Expand Down Expand Up @@ -112,6 +116,8 @@ describe('github API', () => {
squashMerges: false,
initialWorkflowStatus: WorkflowStatus.DRAFT,
cmsLabelPrefix: '',
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
});

fetch.mockResolvedValue({
Expand Down Expand Up @@ -140,6 +146,8 @@ describe('github API', () => {
squashMerges: false,
initialWorkflowStatus: WorkflowStatus.DRAFT,
cmsLabelPrefix: '',
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
});

fetch.mockResolvedValue({
Expand Down Expand Up @@ -167,6 +175,8 @@ describe('github API', () => {
squashMerges: false,
initialWorkflowStatus: WorkflowStatus.DRAFT,
cmsLabelPrefix: '',
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
});

api.requestHeaders = jest.fn().mockResolvedValue({
Expand Down Expand Up @@ -201,6 +211,8 @@ describe('github API', () => {
squashMerges: false,
initialWorkflowStatus: WorkflowStatus.DRAFT,
cmsLabelPrefix: '',
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
});

const responses = {
Expand Down Expand Up @@ -305,6 +317,8 @@ describe('github API', () => {
squashMerges: false,
initialWorkflowStatus: WorkflowStatus.DRAFT,
cmsLabelPrefix: '',
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
});

const tree = [
Expand Down Expand Up @@ -391,6 +405,8 @@ describe('github API', () => {
squashMerges: false,
initialWorkflowStatus: WorkflowStatus.DRAFT,
cmsLabelPrefix: '',
getUser: () => Promise.reject('Unexpected call'),
getRepo: () => Promise.reject('Unexpected call'),
});

const tree = [
Expand Down
51 changes: 44 additions & 7 deletions packages/core/src/backends/github/implementation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import type {
import type { AsyncLock } from '@staticcms/core/lib/util';
import type AssetProxy from '@staticcms/core/valueObjects/AssetProxy';
import type { Semaphore } from 'semaphore';
import type { GitHubUser } from './types';
import type { GitHubUser, ReposGetResponse } from './types';

const MAX_CONCURRENT_DOWNLOADS = 10;

Expand Down Expand Up @@ -77,9 +77,11 @@ export default class GitHub implements BackendClass {
previewContext: string;
token: string | null;
authScheme: AuthScheme;
authenticateAsGithubApp: boolean;
squashMerges: boolean;
cmsLabelPrefix: string;
_currentUserPromise?: Promise<GitHubUser>;
_getRepoPromise?: Promise<ReposGetResponse>;
_userIsOriginMaintainerPromises?: {
[key: string]: Promise<boolean>;
};
Expand Down Expand Up @@ -117,6 +119,7 @@ export default class GitHub implements BackendClass {
this.apiRoot = config.backend.api_root || 'https://api.github.com';
this.token = '';
this.authScheme = config.backend.auth_scheme || 'token';
this.authenticateAsGithubApp = config.backend.authenticate_as_github_app || false;
this.squashMerges = config.backend.squash_merges || false;
this.cmsLabelPrefix = config.backend.cms_label_prefix || '';
this.mediaFolder = config.media_folder;
Expand Down Expand Up @@ -145,8 +148,7 @@ export default class GitHub implements BackendClass {
// no need to check auth if api is down
if (api) {
auth =
(await this.api
?.getUser()
(await this.currentUser({ token: this.token || '' })
.then(user => !!user)
.catch(e => {
console.warn('[StaticCMS] Failed getting GitHub user', e);
Expand Down Expand Up @@ -193,14 +195,46 @@ export default class GitHub implements BackendClass {
return Promise.resolve();
}

async currentUser({ token }: { token: string }) {
if (!this._currentUserPromise) {
this._currentUserPromise = fetch(`${this.apiRoot}/user`, {
async getRepo({ token }: { token: string }) {

Check warning on line 198 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L198 was not covered by tests
if (!this._getRepoPromise) {
this._getRepoPromise = fetch(`${this.apiRoot}/repos/${this.repo}`, {

Check warning on line 200 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L200 was not covered by tests
headers: {
Authorization: `${this.authScheme} ${token}`,
},
}).then(res => res.json());
}
return this._getRepoPromise;

Check warning on line 206 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L206 was not covered by tests
}

async getApp({ token }: { token: string }) {
return fetch(`${this.apiRoot}/app`, {

Check warning on line 210 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

packages/core/src/backends/github/implementation.tsx#L209-L210

Added lines #L209 - L210 were not covered by tests
headers: {
Authorization: `${this.authScheme} ${token}`,
},
})
.then(res => res.json())

Check warning on line 215 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L215 was not covered by tests
.then(
res =>
({

Check warning on line 218 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L218 was not covered by tests
name: res.name,
login: res.slug,
avatar_url: `https://avatars.githubusercontent.com/in/${res.id}?v=4`,
}) as GitHubUser,
);
}

async currentUser({ token }: { token: string }) {

Check warning on line 226 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L226 was not covered by tests
if (!this._currentUserPromise) {
if (this.authenticateAsGithubApp) {
this._currentUserPromise = this.getApp({ token });
} else {
this._currentUserPromise = fetch(`${this.apiRoot}/user`, {

Check warning on line 231 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

packages/core/src/backends/github/implementation.tsx#L229-L231

Added lines #L229 - L231 were not covered by tests
headers: {
Authorization: `${this.authScheme} ${token}`,
},
}).then(res => res.json());

Check warning on line 235 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L235 was not covered by tests
}
}
return this._currentUserPromise;
}

Expand Down Expand Up @@ -293,6 +327,7 @@ export default class GitHub implements BackendClass {
this.api = new apiCtor({
token: this.token,
authScheme: this.authScheme,
authenticateAsGithubApp: this.authenticateAsGithubApp,
branch: this.branch,
repo: this.repo,
originRepo: this.originRepo,
Expand All @@ -302,8 +337,10 @@ export default class GitHub implements BackendClass {
useOpenAuthoring: this.useOpenAuthoring,
openAuthoringEnabled: this.openAuthoringEnabled,
initialWorkflowStatus: this.options.initialWorkflowStatus,
getUser: this.currentUser,
getRepo: this.getRepo,
});
const user = await this.api!.user();
const user = await this.currentUser({ token: this.token });

Check warning on line 343 in packages/core/src/backends/github/implementation.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L343 was not covered by tests
const isCollab = await this.api!.hasWriteAccess().catch(error => {
error.message = stripIndent`
Repo "${this.repo}" not found.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,7 @@ export interface Backend {
gateway_url?: string;
auth_scope?: AuthScope;
auth_scheme?: AuthScheme;
authenticate_as_github_app?: boolean;
commit_messages?: {
create?: string;
update?: string;
Expand Down

0 comments on commit bbd1f99

Please sign in to comment.