Make the credentials provider optional..
..and other review comments improvements. Signed-off-by: Brian Fletcher <brian@roadie.io>
This commit is contained in:
@@ -15,28 +15,24 @@
|
||||
*/
|
||||
|
||||
import { ScmIntegrations } from '../ScmIntegrations';
|
||||
|
||||
const octokit = {
|
||||
paginate: async (fn: any) => (await fn()).data,
|
||||
apps: {
|
||||
listInstallations: jest.fn(),
|
||||
createInstallationAccessToken: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
jest.doMock('@octokit/rest', () => {
|
||||
class Octokit {
|
||||
constructor() {
|
||||
return octokit;
|
||||
}
|
||||
}
|
||||
return { Octokit };
|
||||
});
|
||||
import { GitHubIntegrationConfig } from './config';
|
||||
import { SingleInstanceGithubCredentialsProvider } from './SingleInstanceGithubCredentialsProvider';
|
||||
|
||||
import { DefaultGithubCredentialsProvider } from './DefaultGithubCredentialsProvider';
|
||||
import { RestEndpointMethodTypes } from '@octokit/rest';
|
||||
import { DateTime } from 'luxon';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { GithubCredentials } from './types';
|
||||
|
||||
const resultBuilder = (host: string): GithubCredentials => {
|
||||
return {
|
||||
type: 'token',
|
||||
token: `${host}-token`,
|
||||
headers: {
|
||||
token: `${host}-token`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
jest.mock('./SingleInstanceGithubCredentialsProvider');
|
||||
|
||||
let integrations: ScmIntegrations;
|
||||
|
||||
@@ -59,289 +55,69 @@ describe('DefaultGithubCredentialsProvider tests', () => {
|
||||
],
|
||||
token: 'hardcoded_token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('create repository specific tokens', async () => {
|
||||
octokit.apps.listInstallations.mockResolvedValue({
|
||||
headers: {
|
||||
etag: '123',
|
||||
},
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
repository_selection: 'selected',
|
||||
account: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
repository_selection: 'selected',
|
||||
account: {
|
||||
login: 'backstage',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as RestEndpointMethodTypes['apps']['listInstallations']['response']);
|
||||
|
||||
octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({
|
||||
data: {
|
||||
expires_at: DateTime.local().plus({ hours: 1 }).toString(),
|
||||
token: 'secret_token',
|
||||
},
|
||||
} as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']);
|
||||
|
||||
const github =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
|
||||
const { token, headers, type } = await github.getCredentials({
|
||||
url: 'https://github.com/backstage/foobar',
|
||||
});
|
||||
expect(type).toEqual('app');
|
||||
expect(token).toEqual('secret_token');
|
||||
expect(headers).toEqual({ Authorization: 'Bearer secret_token' });
|
||||
|
||||
// fallback to the configured token if no application is matching
|
||||
await expect(
|
||||
github.getCredentials({
|
||||
url: 'https://github.com/404/foobar',
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
headers: {
|
||||
Authorization: 'Bearer hardcoded_token',
|
||||
},
|
||||
token: 'hardcoded_token',
|
||||
type: 'token',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates tokens for an organization', async () => {
|
||||
octokit.apps.listInstallations.mockResolvedValue({
|
||||
headers: {
|
||||
etag: '123',
|
||||
},
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
repository_selection: 'all',
|
||||
account: {
|
||||
login: 'backstage',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as RestEndpointMethodTypes['apps']['listInstallations']['response']);
|
||||
|
||||
octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({
|
||||
data: {
|
||||
expires_at: DateTime.local().plus({ hours: 1 }).toString(),
|
||||
token: 'secret_token',
|
||||
},
|
||||
} as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']);
|
||||
|
||||
const github =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
|
||||
const { token, headers } = await github.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
});
|
||||
|
||||
expect(headers).toEqual({ Authorization: 'Bearer secret_token' });
|
||||
expect(token).toEqual('secret_token');
|
||||
});
|
||||
|
||||
it('should not fail to issue tokens for an organization when the app is installed for a single repo', async () => {
|
||||
octokit.apps.listInstallations.mockResolvedValue({
|
||||
headers: {
|
||||
etag: '123',
|
||||
},
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
repository_selection: 'selected',
|
||||
account: {
|
||||
login: 'backstage',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as RestEndpointMethodTypes['apps']['listInstallations']['response']);
|
||||
|
||||
octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({
|
||||
data: {
|
||||
expires_at: DateTime.local().plus({ hours: 1 }).toString(),
|
||||
token: 'secret_token',
|
||||
},
|
||||
} as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']);
|
||||
|
||||
const github =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
const { token, headers } = await github.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
});
|
||||
const expectedToken = 'secret_token';
|
||||
expect(headers).toEqual({ Authorization: `Bearer ${expectedToken}` });
|
||||
expect(token).toEqual('secret_token');
|
||||
});
|
||||
|
||||
it('should throw if the app is suspended', async () => {
|
||||
octokit.apps.listInstallations.mockResolvedValue({
|
||||
headers: {
|
||||
etag: '123',
|
||||
},
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
suspended_by: {
|
||||
login: 'admin',
|
||||
},
|
||||
repository_selection: 'all',
|
||||
account: {
|
||||
login: 'backstage',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as RestEndpointMethodTypes['apps']['listInstallations']['response']);
|
||||
|
||||
const github =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
|
||||
await expect(
|
||||
github.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
}),
|
||||
).rejects.toThrow('The GitHub application for backstage is suspended');
|
||||
});
|
||||
|
||||
it('should return the default token when the call to github return a status that is not recognized', async () => {
|
||||
octokit.apps.listInstallations.mockRejectedValue({
|
||||
status: 404,
|
||||
message: 'NotFound',
|
||||
});
|
||||
|
||||
const github =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
await expect(
|
||||
github.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
}),
|
||||
).rejects.toEqual({ status: 404, message: 'NotFound' });
|
||||
});
|
||||
|
||||
it('should return the default token if no app is configured', async () => {
|
||||
integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
apps: [],
|
||||
token: 'fallback_token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const githubProvider =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
|
||||
await expect(
|
||||
githubProvider.getCredentials({
|
||||
url: 'https://github.com/404/foobar',
|
||||
}),
|
||||
).resolves.toEqual(expect.objectContaining({ token: 'fallback_token' }));
|
||||
});
|
||||
|
||||
it('should return the configured token if there are no installations', async () => {
|
||||
integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
apps: [
|
||||
{
|
||||
appId: 1,
|
||||
privateKey: 'privateKey',
|
||||
webhookSecret: '123',
|
||||
clientId: 'CLIENT_ID',
|
||||
clientSecret: 'CLIENT_SECRET',
|
||||
},
|
||||
],
|
||||
host: 'grithub.com',
|
||||
token: 'hardcoded_token',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
octokit.apps.listInstallations.mockResolvedValue({
|
||||
data: [],
|
||||
} as unknown as RestEndpointMethodTypes['apps']['listInstallations']['response']);
|
||||
|
||||
const githubProvider =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
await expect(
|
||||
githubProvider.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
}),
|
||||
).resolves.toEqual(expect.objectContaining({ token: 'hardcoded_token' }));
|
||||
jest.resetAllMocks();
|
||||
SingleInstanceGithubCredentialsProvider.create = (
|
||||
config: GitHubIntegrationConfig,
|
||||
) => {
|
||||
return {
|
||||
getCredentials: (_opts: { url: string }) => {
|
||||
return Promise.resolve(resultBuilder(config.host));
|
||||
},
|
||||
};
|
||||
};
|
||||
jest.spyOn(SingleInstanceGithubCredentialsProvider, 'create');
|
||||
});
|
||||
|
||||
it('should return undefined if no token or apps are configured', async () => {
|
||||
integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const githubProvider =
|
||||
describe('.create', () => {
|
||||
it('passes the config through to the single provider', () => {
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
|
||||
await expect(
|
||||
githubProvider.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
}),
|
||||
).resolves.toEqual({ headers: undefined, token: undefined, type: 'token' });
|
||||
});
|
||||
|
||||
it('should to create a token for the organization ignoring case sensitive', async () => {
|
||||
octokit.apps.listInstallations.mockResolvedValue({
|
||||
headers: {
|
||||
etag: '123',
|
||||
},
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
repository_selection: 'all',
|
||||
account: {
|
||||
login: 'BACKSTAGE',
|
||||
},
|
||||
},
|
||||
],
|
||||
} as RestEndpointMethodTypes['apps']['listInstallations']['response']);
|
||||
|
||||
octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({
|
||||
data: {
|
||||
expires_at: DateTime.local().plus({ hours: 1 }).toString(),
|
||||
token: 'secret_token',
|
||||
},
|
||||
} as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']);
|
||||
|
||||
const github =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
const { token, headers } = await github.getCredentials({
|
||||
url: 'https://github.com/backstage',
|
||||
const githubIntegration =
|
||||
integrations.github.byHost('github.com')?.config;
|
||||
const grithubIntegration =
|
||||
integrations.github.byHost('grithub.com')?.config;
|
||||
expect(
|
||||
SingleInstanceGithubCredentialsProvider.create,
|
||||
).toHaveBeenCalledWith(githubIntegration);
|
||||
expect(
|
||||
SingleInstanceGithubCredentialsProvider.create,
|
||||
).toHaveBeenCalledWith(grithubIntegration);
|
||||
});
|
||||
});
|
||||
|
||||
expect(headers).toEqual({ Authorization: 'Bearer secret_token' });
|
||||
expect(token).toEqual('secret_token');
|
||||
describe('#getCredentials', () => {
|
||||
it('returns the data verbatim from the creds provider', async () => {
|
||||
const provider =
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
const gitHubCredentials = await provider.getCredentials({
|
||||
url: 'https://github.com/blah',
|
||||
});
|
||||
const gritHubCredentials = await provider.getCredentials({
|
||||
url: 'https://grithub.com/blah',
|
||||
});
|
||||
|
||||
expect(gitHubCredentials).toEqual({
|
||||
type: 'token',
|
||||
token: 'github.com-token',
|
||||
headers: {
|
||||
token: 'github.com-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(gritHubCredentials).toEqual({
|
||||
type: 'token',
|
||||
token: 'grithub.com-token',
|
||||
headers: {
|
||||
token: 'grithub.com-token',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,10 +18,6 @@ import { GithubCredentials, GithubCredentialsProvider } from './types';
|
||||
import { ScmIntegrations } from '../ScmIntegrations';
|
||||
import { SingleInstanceGithubCredentialsProvider } from './SingleInstanceGithubCredentialsProvider';
|
||||
|
||||
type SingleInstanceGithubCredentialsProviderCollection = {
|
||||
[url: string]: GithubCredentialsProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the creation and caching of credentials for GitHub integrations.
|
||||
*
|
||||
@@ -34,19 +30,19 @@ export class DefaultGithubCredentialsProvider
|
||||
implements GithubCredentialsProvider
|
||||
{
|
||||
static fromIntegrations(integrations: ScmIntegrations) {
|
||||
const credentialsProviders: SingleInstanceGithubCredentialsProviderCollection =
|
||||
{};
|
||||
const credentialsProviders: Map<string, GithubCredentialsProvider> =
|
||||
new Map<string, GithubCredentialsProvider>();
|
||||
|
||||
integrations.github.list().forEach(integration => {
|
||||
const credentialsProvider =
|
||||
SingleInstanceGithubCredentialsProvider.create(integration.config);
|
||||
credentialsProviders[integration.config.host] = credentialsProvider;
|
||||
credentialsProviders.set(integration.config.host, credentialsProvider);
|
||||
});
|
||||
return new DefaultGithubCredentialsProvider(credentialsProviders);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly providers: SingleInstanceGithubCredentialsProviderCollection,
|
||||
private readonly providers: Map<string, GithubCredentialsProvider>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -71,13 +67,14 @@ export class DefaultGithubCredentialsProvider
|
||||
*/
|
||||
async getCredentials(opts: { url: string }): Promise<GithubCredentials> {
|
||||
const parsed = new URL(opts.url);
|
||||
const provider = this.providers.get(parsed.host);
|
||||
|
||||
if (!this.providers[parsed.host]) {
|
||||
if (!provider) {
|
||||
throw new Error(
|
||||
`There is no GitHub integration that matches ${opts.url}. Please add a configuration for an integration.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.providers[parsed.host].getCredentials(opts);
|
||||
return provider.getCredentials(opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,206 @@ import {
|
||||
GithubCredentialsProvider,
|
||||
GithubCredentialType,
|
||||
} from './types';
|
||||
import { GitHubIntegrationConfig } from './config';
|
||||
import { GithubAppCredentialsMux } from './GithubAppCredentialsMux';
|
||||
|
||||
import { GithubAppConfig, GitHubIntegrationConfig } from './config';
|
||||
import { Octokit, RestEndpointMethodTypes } from '@octokit/rest';
|
||||
import { createAppAuth } from '@octokit/auth-app';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
/**
|
||||
* This accept header is required when calling App APIs in GitHub Enterprise.
|
||||
* It has no effect on calls to github.com and can probably be removed entirely
|
||||
* once GitHub Apps is out of preview.
|
||||
*/
|
||||
const HEADERS = {
|
||||
Accept: 'application/vnd.github.machine-man-preview+json',
|
||||
};
|
||||
|
||||
type InstallationData = {
|
||||
installationId: number;
|
||||
suspended: boolean;
|
||||
};
|
||||
|
||||
class Cache {
|
||||
private readonly tokenCache = new Map<
|
||||
string,
|
||||
{ token: string; expiresAt: DateTime }
|
||||
>();
|
||||
|
||||
async getOrCreateToken(
|
||||
key: string,
|
||||
supplier: () => Promise<{ token: string; expiresAt: DateTime }>,
|
||||
): Promise<{ accessToken: string }> {
|
||||
const item = this.tokenCache.get(key);
|
||||
if (item && this.isNotExpired(item.expiresAt)) {
|
||||
return { accessToken: item.token };
|
||||
}
|
||||
|
||||
const result = await supplier();
|
||||
this.tokenCache.set(key, result);
|
||||
return { accessToken: result.token };
|
||||
}
|
||||
|
||||
// consider timestamps older than 50 minutes to be expired.
|
||||
private isNotExpired = (date: DateTime) =>
|
||||
date.diff(DateTime.local(), 'minutes').minutes > 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* GithubAppManager issues and caches tokens for a specific GitHub App.
|
||||
*/
|
||||
class GithubAppManager {
|
||||
private readonly appClient: Octokit;
|
||||
private readonly baseUrl?: string;
|
||||
private readonly baseAuthConfig: { appId: number; privateKey: string };
|
||||
private readonly cache = new Cache();
|
||||
private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations
|
||||
|
||||
constructor(config: GithubAppConfig, baseUrl?: string) {
|
||||
this.allowedInstallationOwners = config.allowedInstallationOwners;
|
||||
this.baseUrl = baseUrl;
|
||||
this.baseAuthConfig = {
|
||||
appId: config.appId,
|
||||
privateKey: config.privateKey.replace(/\\n/gm, '\n'),
|
||||
};
|
||||
this.appClient = new Octokit({
|
||||
baseUrl,
|
||||
headers: HEADERS,
|
||||
authStrategy: createAppAuth,
|
||||
auth: this.baseAuthConfig,
|
||||
});
|
||||
}
|
||||
|
||||
async getInstallationCredentials(
|
||||
owner: string,
|
||||
repo?: string,
|
||||
): Promise<{ accessToken: string }> {
|
||||
const { installationId, suspended } = await this.getInstallationData(owner);
|
||||
if (this.allowedInstallationOwners) {
|
||||
if (!this.allowedInstallationOwners?.includes(owner)) {
|
||||
throw new Error(
|
||||
`The GitHub application for ${owner} is not included in the allowed installation list (${installationId}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (suspended) {
|
||||
throw new Error(`The GitHub application for ${owner} is suspended`);
|
||||
}
|
||||
|
||||
const cacheKey = repo ? `${owner}/${repo}` : owner;
|
||||
|
||||
// Go and grab an access token for the app scoped to a repository if provided, if not use the organisation installation.
|
||||
return this.cache.getOrCreateToken(cacheKey, async () => {
|
||||
const result = await this.appClient.apps.createInstallationAccessToken({
|
||||
installation_id: installationId,
|
||||
headers: HEADERS,
|
||||
});
|
||||
if (repo && result.data.repository_selection === 'selected') {
|
||||
const installationClient = new Octokit({
|
||||
baseUrl: this.baseUrl,
|
||||
auth: result.data.token,
|
||||
});
|
||||
const repos = await installationClient.paginate(
|
||||
installationClient.apps.listReposAccessibleToInstallation,
|
||||
);
|
||||
const hasRepo = repos.some(repository => {
|
||||
return repository.name === repo;
|
||||
});
|
||||
if (!hasRepo) {
|
||||
throw new Error(
|
||||
`The Backstage GitHub application used in the ${owner} organization does not have access to a repository with the name ${repo}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
token: result.data.token,
|
||||
expiresAt: DateTime.fromISO(result.data.expires_at),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getInstallations(): Promise<
|
||||
RestEndpointMethodTypes['apps']['listInstallations']['response']['data']
|
||||
> {
|
||||
return this.appClient.paginate(this.appClient.apps.listInstallations);
|
||||
}
|
||||
|
||||
private async getInstallationData(owner: string): Promise<InstallationData> {
|
||||
const allInstallations = await this.getInstallations();
|
||||
const installation = allInstallations.find(
|
||||
inst =>
|
||||
inst.account?.login?.toLocaleLowerCase('en-US') ===
|
||||
owner.toLocaleLowerCase('en-US'),
|
||||
);
|
||||
if (installation) {
|
||||
return {
|
||||
installationId: installation.id,
|
||||
suspended: Boolean(installation.suspended_by),
|
||||
};
|
||||
}
|
||||
const notFoundError = new Error(
|
||||
`No app installation found for ${owner} in ${this.baseAuthConfig.appId}`,
|
||||
);
|
||||
notFoundError.name = 'NotFoundError';
|
||||
throw notFoundError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Corresponds to a Github installation which internally could hold several GitHub Apps.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class GithubAppCredentialsMux {
|
||||
private readonly apps: GithubAppManager[];
|
||||
|
||||
constructor(config: GitHubIntegrationConfig) {
|
||||
this.apps =
|
||||
config.apps?.map(ac => new GithubAppManager(ac, config.apiBaseUrl)) ?? [];
|
||||
}
|
||||
|
||||
async getAllInstallations(): Promise<
|
||||
RestEndpointMethodTypes['apps']['listInstallations']['response']['data']
|
||||
> {
|
||||
if (!this.apps.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const installs = await Promise.all(
|
||||
this.apps.map(app => app.getInstallations()),
|
||||
);
|
||||
|
||||
return installs.flat();
|
||||
}
|
||||
|
||||
async getAppToken(owner: string, repo?: string): Promise<string | undefined> {
|
||||
if (this.apps.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
this.apps.map(app =>
|
||||
app.getInstallationCredentials(owner, repo).then(
|
||||
credentials => ({ credentials, error: undefined }),
|
||||
error => ({ credentials: undefined, error }),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const result = results.find(resultItem => resultItem.credentials);
|
||||
if (result) {
|
||||
return result.credentials!.accessToken;
|
||||
}
|
||||
|
||||
const errors = results.map(r => r.error);
|
||||
const notNotFoundError = errors.find(err => err.name !== 'NotFoundError');
|
||||
if (notNotFoundError) {
|
||||
throw notNotFoundError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation and caching of credentials for GitHub integrations.
|
||||
|
||||
@@ -20,9 +20,11 @@ export {
|
||||
} from './config';
|
||||
export type { GithubAppConfig, GitHubIntegrationConfig } from './config';
|
||||
export { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core';
|
||||
export { GithubAppCredentialsMux } from './GithubAppCredentialsMux';
|
||||
export { DefaultGithubCredentialsProvider } from './DefaultGithubCredentialsProvider';
|
||||
export { SingleInstanceGithubCredentialsProvider } from './SingleInstanceGithubCredentialsProvider';
|
||||
export {
|
||||
SingleInstanceGithubCredentialsProvider,
|
||||
GithubAppCredentialsMux,
|
||||
} from './SingleInstanceGithubCredentialsProvider';
|
||||
|
||||
export type {
|
||||
GithubCredentials,
|
||||
|
||||
@@ -1012,14 +1012,14 @@ export class GithubDiscoveryProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
integrations: ScmIntegrations;
|
||||
logger: Logger_2;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
});
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger_2;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
},
|
||||
): GithubDiscoveryProcessor;
|
||||
// (undocumented)
|
||||
@@ -1036,14 +1036,14 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
|
||||
integrations: ScmIntegrations;
|
||||
logger: Logger_2;
|
||||
orgs: GithubMultiOrgConfig;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
});
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger_2;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
},
|
||||
): GithubMultiOrgReaderProcessor;
|
||||
// (undocumented)
|
||||
@@ -1074,7 +1074,7 @@ export class GitHubOrgEntityProvider implements EntityProvider {
|
||||
id: string;
|
||||
orgUrl: string;
|
||||
logger: Logger_2;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
},
|
||||
): GitHubOrgEntityProvider;
|
||||
// (undocumented)
|
||||
@@ -1090,14 +1090,14 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
integrations: ScmIntegrations;
|
||||
logger: Logger_2;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
});
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger_2;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
},
|
||||
): GithubOrgReaderProcessor;
|
||||
// (undocumented)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
DefaultGithubCredentialsProvider,
|
||||
GithubCredentialsProvider,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
@@ -49,7 +50,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
},
|
||||
) {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
@@ -63,11 +64,13 @@ export class GithubDiscoveryProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
integrations: ScmIntegrations;
|
||||
logger: Logger;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}) {
|
||||
this.integrations = options.integrations;
|
||||
this.logger = options.logger;
|
||||
this.githubCredentialsProvider = options.githubCredentialsProvider;
|
||||
this.githubCredentialsProvider =
|
||||
options.githubCredentialsProvider ||
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(this.integrations);
|
||||
}
|
||||
|
||||
async readLocation(
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
DefaultGithubCredentialsProvider,
|
||||
GithubAppCredentialsMux,
|
||||
GithubCredentialsProvider,
|
||||
GitHubIntegrationConfig,
|
||||
@@ -50,7 +51,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
},
|
||||
) {
|
||||
const c = config.getOptionalConfig('catalog.processors.githubMultiOrg');
|
||||
@@ -67,12 +68,14 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor {
|
||||
integrations: ScmIntegrations;
|
||||
logger: Logger;
|
||||
orgs: GithubMultiOrgConfig;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}) {
|
||||
this.integrations = options.integrations;
|
||||
this.logger = options.logger;
|
||||
this.orgs = options.orgs;
|
||||
this.githubCredentialsProvider = options.githubCredentialsProvider;
|
||||
this.githubCredentialsProvider =
|
||||
options.githubCredentialsProvider ||
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(this.integrations);
|
||||
}
|
||||
|
||||
async readLocation(
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
GithubCredentialType,
|
||||
ScmIntegrations,
|
||||
GithubCredentialsProvider,
|
||||
DefaultGithubCredentialsProvider,
|
||||
} from '@backstage/integration';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
import { Logger } from 'winston';
|
||||
@@ -46,7 +47,7 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
},
|
||||
) {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
@@ -60,10 +61,12 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
constructor(options: {
|
||||
integrations: ScmIntegrations;
|
||||
logger: Logger;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}) {
|
||||
this.integrations = options.integrations;
|
||||
this.githubCredentialsProvider = options.githubCredentialsProvider;
|
||||
this.githubCredentialsProvider =
|
||||
options.githubCredentialsProvider ||
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(this.integrations);
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
DefaultGithubCredentialsProvider,
|
||||
GithubCredentialsProvider,
|
||||
GitHubIntegrationConfig,
|
||||
ScmIntegrations,
|
||||
@@ -47,7 +48,7 @@ export class GitHubOrgEntityProvider implements EntityProvider {
|
||||
id: string;
|
||||
orgUrl: string;
|
||||
logger: Logger;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
},
|
||||
) {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
@@ -68,7 +69,9 @@ export class GitHubOrgEntityProvider implements EntityProvider {
|
||||
orgUrl: options.orgUrl,
|
||||
logger,
|
||||
gitHubConfig,
|
||||
githubCredentialsProvider: options.githubCredentialsProvider,
|
||||
githubCredentialsProvider:
|
||||
options.githubCredentialsProvider ||
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -123,17 +123,17 @@ export const createFilesystemRenameAction: () => TemplateAction<any>;
|
||||
//
|
||||
// @public (undocumented)
|
||||
export function createGithubActionsDispatchAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
integrations: ScmIntegrations;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}): TemplateAction<any>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createGithubWebhookAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export function createGithubWebhookAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
integrations: ScmIntegrations;
|
||||
defaultWebhookSecret?: string;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}): TemplateAction<any>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "createPublishAzureAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
@@ -161,9 +161,9 @@ export function createPublishFileAction(): TemplateAction<any>;
|
||||
//
|
||||
// @public (undocumented)
|
||||
export function createPublishGithubAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
integrations: ScmIntegrations;
|
||||
config: Config;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}): TemplateAction<any>;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "CreateGithubPullRequestActionOptions" needs to be exported by the entry point index.d.ts
|
||||
|
||||
+6
-4
@@ -14,20 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
DefaultGithubCredentialsProvider,
|
||||
GithubCredentialsProvider,
|
||||
ScmIntegrationRegistry,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { createTemplateAction } from '../../createTemplateAction';
|
||||
import { OctokitProvider } from './OctokitProvider';
|
||||
|
||||
export function createGithubActionsDispatchAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
integrations: ScmIntegrations;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}) {
|
||||
const { integrations, githubCredentialsProvider } = options;
|
||||
const octokitProvider = new OctokitProvider(
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
githubCredentialsProvider ||
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations),
|
||||
);
|
||||
|
||||
return createTemplateAction<{
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
DefaultGithubCredentialsProvider,
|
||||
GithubCredentialsProvider,
|
||||
ScmIntegrationRegistry,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import { createTemplateAction } from '../../createTemplateAction';
|
||||
import { OctokitProvider } from './OctokitProvider';
|
||||
@@ -25,15 +26,16 @@ import { assertError } from '@backstage/errors';
|
||||
type ContentType = 'form' | 'json';
|
||||
|
||||
export function createGithubWebhookAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
integrations: ScmIntegrations;
|
||||
defaultWebhookSecret?: string;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}) {
|
||||
const { integrations, defaultWebhookSecret, githubCredentialsProvider } =
|
||||
options;
|
||||
const octokitProvider = new OctokitProvider(
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
githubCredentialsProvider ??
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations),
|
||||
);
|
||||
const eventNames = emitterEventNames.filter(event => !event.includes('.'));
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
DefaultGithubCredentialsProvider,
|
||||
GithubCredentialsProvider,
|
||||
ScmIntegrationRegistry,
|
||||
ScmIntegrations,
|
||||
} from '@backstage/integration';
|
||||
import {
|
||||
enableBranchProtectionOnDefaultRepoBranch,
|
||||
@@ -31,14 +32,15 @@ type Permission = 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
|
||||
type Collaborator = { access: Permission; username: string };
|
||||
|
||||
export function createPublishGithubAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
integrations: ScmIntegrations;
|
||||
config: Config;
|
||||
githubCredentialsProvider: GithubCredentialsProvider;
|
||||
githubCredentialsProvider?: GithubCredentialsProvider;
|
||||
}) {
|
||||
const { integrations, config, githubCredentialsProvider } = options;
|
||||
const octokitProvider = new OctokitProvider(
|
||||
integrations,
|
||||
githubCredentialsProvider,
|
||||
githubCredentialsProvider ||
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations),
|
||||
);
|
||||
|
||||
return createTemplateAction<{
|
||||
|
||||
Reference in New Issue
Block a user