diff --git a/packages/integration/src/github/DefaultGithubCredentialsProvider.test.ts b/packages/integration/src/github/DefaultGithubCredentialsProvider.test.ts index f537fee783..7c2b53e822 100644 --- a/packages/integration/src/github/DefaultGithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/DefaultGithubCredentialsProvider.test.ts @@ -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', + }, + }); + }); }); }); diff --git a/packages/integration/src/github/DefaultGithubCredentialsProvider.ts b/packages/integration/src/github/DefaultGithubCredentialsProvider.ts index 1d1b6db593..aabcac53c7 100644 --- a/packages/integration/src/github/DefaultGithubCredentialsProvider.ts +++ b/packages/integration/src/github/DefaultGithubCredentialsProvider.ts @@ -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 = + new Map(); 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, ) {} /** @@ -71,13 +67,14 @@ export class DefaultGithubCredentialsProvider */ async getCredentials(opts: { url: string }): Promise { 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); } } diff --git a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts index 5f04a3ed6e..6b5e3318db 100644 --- a/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts +++ b/packages/integration/src/github/SingleInstanceGithubCredentialsProvider.ts @@ -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 { + 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 { + 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. diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index 51d7bc9dd2..b9fbb845f1 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -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, diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 1fe38051c9..2d5e29fea0 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -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) diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts index 01ba40b1fd..3196c4e161 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -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( diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts index 7a25b35078..e07de8ac0c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts @@ -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( diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts index 4c56b30c90..474d08feee 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts @@ -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; } diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts b/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts index bc40ca1da5..c161074de4 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts +++ b/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts @@ -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), }); } diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index a292388909..e0710647de 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -123,17 +123,17 @@ export const createFilesystemRenameAction: () => TemplateAction; // // @public (undocumented) export function createGithubActionsDispatchAction(options: { - integrations: ScmIntegrationRegistry; - githubCredentialsProvider: GithubCredentialsProvider; + integrations: ScmIntegrations; + githubCredentialsProvider?: GithubCredentialsProvider; }): TemplateAction; // 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; // 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; // // @public (undocumented) export function createPublishGithubAction(options: { - integrations: ScmIntegrationRegistry; + integrations: ScmIntegrations; config: Config; - githubCredentialsProvider: GithubCredentialsProvider; + githubCredentialsProvider?: GithubCredentialsProvider; }): TemplateAction; // Warning: (ae-forgotten-export) The symbol "CreateGithubPullRequestActionOptions" needs to be exported by the entry point index.d.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts index a88698ed07..eed74f2d04 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -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<{ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts index d341b59011..95950627f9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts @@ -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('.')); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 66a45eaba1..24f3c5c48b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -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<{