diff --git a/.changeset/poor-pens-complain.md b/.changeset/poor-pens-complain.md new file mode 100644 index 0000000000..333ec208ee --- /dev/null +++ b/.changeset/poor-pens-complain.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +- **DEPRECATED** - `OctokitProvider` has been deprecated and will be removed in upcoming versions + This helper doesn't make sense to be export from the `plugin-scaffolder-backend` and possibly will be moved into the `integrations` package at a later date. + All implementations have been moved over to a private implementation called `getOctokitOptions` which is then passed to the `Octokit` constructor. If you're using this API you should consider duplicating the logic that lives in `getOctokitOptions` and move away from the deprecated export. diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index bd507d892a..1c627759df 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -392,13 +392,15 @@ export function fetchContents({ // Warning: (ae-missing-release-tag) "OctokitProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated export class OctokitProvider { constructor( integrations: ScmIntegrationRegistry, githubCredentialsProvider?: GithubCredentialsProvider, ); // Warning: (ae-forgotten-export) The symbol "OctokitIntegration" needs to be exported by the entry point index.d.ts + // + // @deprecated getOctokit( repoUrl: string, options?: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts index 3ac66a0023..2f3c43a6b8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts @@ -32,6 +32,9 @@ export type OctokitIntegration = { /** * OctokitProvider provides Octokit client based on ScmIntegrationsRegistry configuration. * OctokitProvider supports GitHub credentials caching out of the box. + * + * @deprecated we are no longer providing a way from the scaffolder to generate octokit instances. + * Implement your own if you're using this method from an external package, or use the internal `getOctokitOptions` function instead */ export class OctokitProvider { private readonly integrations: ScmIntegrationRegistry; @@ -51,6 +54,9 @@ export class OctokitProvider { * gets standard Octokit client based on repository URL. * * @param repoUrl - Repository URL + * + * @deprecated we are no longer providing a way from the scaffolder to generate octokit instances. + * Implement your own if you're using this method from an external package, or use the internal `getOctokitOptions` function instead */ async getOctokit( repoUrl: string, 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 0c8b4d4aab..f2329c9ca1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -13,24 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { InputError } from '@backstage/errors'; import { - DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrations, } from '@backstage/integration'; +import { Octokit } from 'octokit'; import { createTemplateAction } from '../../createTemplateAction'; -import { OctokitProvider } from './OctokitProvider'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions } from './helpers'; export function createGithubActionsDispatchAction(options: { integrations: ScmIntegrations; githubCredentialsProvider?: GithubCredentialsProvider; }) { const { integrations, githubCredentialsProvider } = options; - const octokitProvider = new OctokitProvider( - integrations, - githubCredentialsProvider || - DefaultGithubCredentialsProvider.fromIntegrations(integrations), - ); return createTemplateAction<{ repoUrl: string; @@ -90,9 +87,19 @@ export function createGithubActionsDispatchAction(options: { `Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`, ); - const { client, owner, repo } = await octokitProvider.getOctokit( - repoUrl, - { token: providedToken }, + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const client = new Octokit( + await getOctokitOptions({ + integrations, + repoUrl, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + }), ); await client.rest.actions.createWorkflowDispatch({ 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 0c93ae1dd5..e2cf744845 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts @@ -14,14 +14,15 @@ * limitations under the License. */ import { - DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; import { createTemplateAction } from '../../createTemplateAction'; -import { OctokitProvider } from './OctokitProvider'; import { emitterEventNames } from '@octokit/webhooks'; -import { assertError } from '@backstage/errors'; +import { assertError, InputError } from '@backstage/errors'; +import { Octokit } from 'octokit'; +import { getOctokitOptions } from './helpers'; +import { parseRepoUrl } from '../publish/util'; export function createGithubWebhookAction(options: { integrations: ScmIntegrationRegistry; @@ -30,11 +31,7 @@ export function createGithubWebhookAction(options: { }) { const { integrations, defaultWebhookSecret, githubCredentialsProvider } = options; - const octokitProvider = new OctokitProvider( - integrations, - githubCredentialsProvider ?? - DefaultGithubCredentialsProvider.fromIntegrations(integrations), - ); + const eventNames = emitterEventNames.filter(event => !event.includes('.')); return createTemplateAction<{ @@ -127,10 +124,19 @@ export function createGithubWebhookAction(options: { } = ctx.input; ctx.logger.info(`Creating webhook ${webhookUrl} for repo ${repoUrl}`); + const { owner, repo } = parseRepoUrl(repoUrl, integrations); - const { client, owner, repo } = await octokitProvider.getOctokit( - repoUrl, - { token: providedToken }, + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const client = new Octokit( + await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + repoUrl: repoUrl, + token: providedToken, + }), ); try { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts new file mode 100644 index 0000000000..c310389488 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { InputError } from '@backstage/errors'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { OctokitOptions } from '@octokit/core/dist-types/types'; +import { parseRepoUrl } from '../publish/util'; + +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; + token?: string; + repoUrl: string; +}): Promise { + const { integrations, credentialsProvider, repoUrl, token } = options; + const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + + const integrationConfig = integrations.github.byHost(host)?.config; + + if (!integrationConfig) { + throw new InputError(`No integration for host ${host}`); + } + + // short circuit the `githubCredentialsProvider` if there is a token provided by the caller already + if (token) { + return { + auth: token, + baseUrl: integrationConfig.apiBaseUrl, + previews: ['nebula-preview'], + }; + } + + const githubCredentialsProvider = + credentialsProvider ?? + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + + // TODO(blam): Consider changing this API to take host and repo instead of repoUrl, as we end up parsing in this function + // and then parsing in the `getCredentials` function too the other side + const { token: credentialProviderToken } = + await githubCredentialsProvider.getCredentials({ + url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( + repo, + )}`, + }); + + if (!credentialProviderToken) { + throw new InputError( + `No token available for host: ${host}, with owner ${owner}, and repo ${repo}`, + ); + } + + return { + auth: credentialProviderToken, + baseUrl: integrationConfig.apiBaseUrl, + previews: ['nebula-preview'], + }; +} 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 cabe40986f..37e2ffb7d3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -14,7 +14,6 @@ * limitations under the License. */ import { - DefaultGithubCredentialsProvider, GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; @@ -22,11 +21,12 @@ import { enableBranchProtectionOnDefaultRepoBranch, initRepoAndPush, } from '../helpers'; -import { getRepoSourceDirectory } from './util'; +import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '../../createTemplateAction'; import { Config } from '@backstage/config'; -import { OctokitProvider } from '../github/OctokitProvider'; -import { assertError } from '@backstage/errors'; +import { assertError, InputError } from '@backstage/errors'; +import { getOctokitOptions } from '../github/helpers'; +import { Octokit } from 'octokit'; export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; @@ -34,11 +34,6 @@ export function createPublishGithubAction(options: { githubCredentialsProvider?: GithubCredentialsProvider; }) { const { integrations, config, githubCredentialsProvider } = options; - const octokitProvider = new OctokitProvider( - integrations, - githubCredentialsProvider || - DefaultGithubCredentialsProvider.fromIntegrations(integrations), - ); return createTemplateAction<{ repoUrl: string; @@ -160,10 +155,20 @@ export function createPublishGithubAction(options: { token: providedToken, } = ctx.input; - const { client, token, owner, repo } = await octokitProvider.getOctokit( + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, repoUrl, - { token: providedToken }, - ); + }); + + const client = new Octokit(octokitOptions); const user = await client.rest.users.getByUsername({ username: owner, @@ -253,7 +258,7 @@ export function createPublishGithubAction(options: { defaultBranch, auth: { username: 'x-access-token', - password: token, + password: octokitOptions.auth, }, logger: ctx.logger, commitMessage: config.getOptionalString( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index c329b6b6f3..9efc11210a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -20,7 +20,6 @@ import { parseRepoUrl, isExecutable } from './util'; import { GithubCredentialsProvider, ScmIntegrationRegistry, - SingleInstanceGithubCredentialsProvider, } from '@backstage/integration'; import { zipObject } from 'lodash'; import { createTemplateAction } from '../../createTemplateAction'; @@ -29,6 +28,7 @@ import { InputError, CustomErrorBase } from '@backstage/errors'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; import globby from 'globby'; import { resolveSafeChildPath } from '@backstage/backend-common'; +import { getOctokitOptions } from '../github/helpers'; export type Encoding = 'utf-8' | 'base64'; @@ -65,40 +65,19 @@ export const defaultClientFactory = async ({ host = 'github.com', token: providedToken, }: ClientFactoryInput): Promise => { - const integrationConfig = integrations.github.byHost(host)?.config; + const [encodedHost, encodedOwner, encodedRepo] = [host, owner, repo].map( + encodeURIComponent, + ); + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + repoUrl: `https://${encodedHost}/${encodedOwner}/${encodedRepo}`, + token: providedToken, + }); + const OctokitPR = Octokit.plugin(createPullRequest); - - if (!integrationConfig) { - throw new InputError(`No integration for host ${host}`); - } - - if (providedToken) { - return new OctokitPR({ - auth: providedToken, - baseUrl: integrationConfig.apiBaseUrl, - }); - } - - const credentialsProvider = - githubCredentialsProvider || - SingleInstanceGithubCredentialsProvider.create(integrationConfig); - - const { token } = await credentialsProvider.getCredentials({ - url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( - repo, - )}`, - }); - - if (!token) { - throw new InputError( - `No token available for host: ${host}, with owner ${owner}, and repo ${repo}`, - ); - } - - return new OctokitPR({ - auth: token, - baseUrl: integrationConfig.apiBaseUrl, - }); + return new OctokitPR(octokitOptions); }; interface CreateGithubPullRequestActionOptions {