From fda96db681f0e02be4d342d98da626774deab84f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 13:32:19 +0100 Subject: [PATCH 01/17] refactor(scaffolder): split getOctokitOptions repoUrl parameter into host, owner and repo parameters Signed-off-by: Benjamin Janssens --- .../report.api.md | 4 +++- .../src/actions/github.ts | 8 ++++--- .../src/actions/githubActionsDispatch.ts | 6 ++++-- .../src/actions/githubAutolinks.ts | 6 ++++-- .../src/actions/githubBranchProtection.ts | 8 ++++--- .../src/actions/githubDeployKey.ts | 8 ++++--- .../src/actions/githubEnvironment.ts | 8 ++++--- .../src/actions/githubIssuesLabel.ts | 6 ++++-- .../src/actions/githubPagesEnable.ts | 8 ++++--- .../src/actions/githubPullRequest.ts | 8 +++---- .../src/actions/githubRepoCreate.ts | 8 ++++--- .../src/actions/githubRepoPush.ts | 6 ++++-- .../src/actions/githubWebhook.ts | 6 ++++-- .../src/actions/helpers.ts | 21 ++++++++++--------- 14 files changed, 67 insertions(+), 44 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 935886bf14..782846dec7 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -477,8 +477,10 @@ export const createPublishGithubPullRequestAction: ( export function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + host: string; + owner?: string; + repo?: string; token?: string; - repoUrl: string; }): Promise; // @public diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index e3a9ff3352..da54b7c8cb 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -225,16 +225,18 @@ export function createPublishGithubAction(options: { requiredLinearHistory = false, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index 0d24b74927..4c83f538ce 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -96,7 +96,7 @@ export function createGithubActionsDispatchAction(options: { `Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`, ); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); @@ -105,7 +105,9 @@ export function createGithubActionsDispatchAction(options: { const client = new Octokit( await getOctokitOptions({ integrations, - repoUrl, + host, + owner, + repo, credentialsProvider: githubCredentialsProvider, token: providedToken, }), diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts index 4b0d0e1e1f..2037287fad 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts @@ -89,7 +89,7 @@ export function createGithubAutolinksAction(options: { ctx.logger.info(`Creating autolink reference for repo ${repoUrl}`); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); @@ -98,7 +98,9 @@ export function createGithubAutolinksAction(options: { const client = new Octokit( await getOctokitOptions({ integrations, - repoUrl, + host, + owner, + repo, credentialsProvider: githubCredentialsProvider, token, }), diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index a7555d3af1..b8716a3f8f 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -115,15 +115,17 @@ export function createGithubBranchProtectionAction(options: { token: providedToken, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError(`No owner provided for repo ${repoUrl}`); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts index ba55b6a1fb..c9a881c6b1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts @@ -107,14 +107,16 @@ export function createGithubDeployKeyAction(options: { token: providedToken, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError(`No owner provided for repo ${repoUrl}`); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index ef59b611fe..736b562d54 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -175,14 +175,16 @@ export function createGithubEnvironmentAction(options: { // Add a 2-second delay before initiating the steps in this action. await new Promise(resolve => setTimeout(resolve, 2000)); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError(`No owner provided for repo ${repoUrl}`); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts index 90f5a89dc7..7814429bd7 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts @@ -80,7 +80,7 @@ export function createGithubIssuesLabelAction(options: { async handler(ctx) { const { repoUrl, number, labels, token: providedToken } = ctx.input; - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); ctx.logger.info(`Adding labels to ${number} issue on repo ${repo}`); if (!owner) { @@ -91,7 +91,9 @@ export function createGithubIssuesLabelAction(options: { await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, - repoUrl: repoUrl, + host, + owner, + repo, token: providedToken, }), ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts index 42b9c47030..f8df207638 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts @@ -92,16 +92,18 @@ export function createGithubPagesEnableAction(options: { token: providedToken, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts index 7fe9d8e401..63496133f1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts @@ -49,14 +49,12 @@ export const defaultClientFactory: CreateGithubPullRequestActionOptions['clientF host = 'github.com', token: providedToken, }) => { - const [encodedHost, encodedOwner, encodedRepo] = [host, owner, repo].map( - encodeURIComponent, - ); - const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, - repoUrl: `${encodedHost}?owner=${encodedOwner}&repo=${encodedRepo}`, + host, + owner, + repo, token: providedToken, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index b4608e57de..a3a758e005 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -182,16 +182,18 @@ export function createGithubRepoCreateAction(options: { token: providedToken, } = ctx.input; + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl: repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); - if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); } diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts index 62624057c8..b11c4b8f5c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts @@ -142,7 +142,7 @@ export function createGithubRepoPushAction(options: { requiredLinearHistory = false, } = ctx.input; - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); @@ -152,7 +152,9 @@ export function createGithubRepoPushAction(options: { integrations, credentialsProvider: githubCredentialsProvider, token: providedToken, - repoUrl, + host, + owner, + repo, }); const client = new Octokit(octokitOptions); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts index e188cdf0cb..0289d3c69b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts @@ -134,7 +134,7 @@ export function createGithubWebhookAction(options: { } = ctx.input; ctx.logger.info(`Creating webhook ${webhookUrl} for repo ${repoUrl}`); - const { owner, repo } = parseRepoUrl(repoUrl, integrations); + const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); if (!owner) { throw new InputError('Invalid repository owner provided in repoUrl'); @@ -144,7 +144,9 @@ export function createGithubWebhookAction(options: { await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, - repoUrl: repoUrl, + host, + owner, + repo, token: providedToken, }), ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 242f95ce34..45e4229e73 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -27,7 +27,6 @@ import { Octokit } from 'octokit'; import { getRepoSourceDirectory, initRepoAndPush, - parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import Sodium from 'libsodium-wrappers'; @@ -47,21 +46,19 @@ const DEFAULT_TIMEOUT_MS = 60_000; export async function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + host: string; + owner?: string; + repo?: string; token?: string; - repoUrl: string; }): Promise { - const { integrations, credentialsProvider, repoUrl, token } = options; - const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); + const { integrations, credentialsProvider, host, owner, repo, token } = + options; const requestOptions = { // set timeout to 60 seconds timeout: DEFAULT_TIMEOUT_MS, }; - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const integrationConfig = integrations.github.byHost(host)?.config; if (!integrationConfig) { @@ -78,12 +75,16 @@ export async function getOctokitOptions(options: { }; } + if (!owner || !repo) { + throw new InputError( + `No owner and/or owner provided, which is required if a token is not provided`, + ); + } + 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( From 95439bbdad7aa08e41a8ef3f8eeae4ca63523a8f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 13:44:33 +0100 Subject: [PATCH 02/17] refactor(scaffolder): move getOctokitOptions to separate util file Signed-off-by: Benjamin Janssens --- .../src/actions/github.ts | 2 +- .../src/actions/githubActionsDispatch.ts | 2 +- .../src/actions/githubAutolinks.ts | 2 +- .../src/actions/githubBranchProtection.ts | 2 +- .../src/actions/githubDeployKey.ts | 2 +- .../src/actions/githubEnvironment.ts | 2 +- .../githubIssuesLabel.examples.test.ts | 2 +- .../src/actions/githubIssuesLabel.test.ts | 2 +- .../src/actions/githubIssuesLabel.ts | 2 +- .../src/actions/githubPagesEnable.ts | 2 +- .../src/actions/githubPullRequest.ts | 2 +- .../src/actions/githubRepoCreate.ts | 6 +- .../src/actions/githubRepoPush.ts | 3 +- .../src/actions/githubWebhook.ts | 2 +- .../src/actions/helpers.ts | 77 +--------------- .../src/actions/index.ts | 2 - .../src/index.ts | 1 + .../src/util.ts | 92 +++++++++++++++++++ 18 files changed, 110 insertions(+), 95 deletions(-) create mode 100644 plugins/scaffolder-backend-module-github/src/util.ts diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index da54b7c8cb..4c04087670 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -27,9 +27,9 @@ import { } from '@backstage/plugin-scaffolder-node'; import { createGithubRepoWithCollaboratorsAndTopics, - getOctokitOptions, initRepoPushAndProtect, } from './helpers'; +import { getOctokitOptions } from '../util'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; import { examples } from './github.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts index 4c83f538ce..93f89a565c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubActionsDispatch.ts @@ -24,7 +24,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { examples } from './githubActionsDispatch.examples'; /** diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts index 2037287fad..b55811630b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubAutolinks.ts @@ -25,7 +25,7 @@ import { } from '@backstage/plugin-scaffolder-node'; import { Octokit } from 'octokit'; import { examples } from './githubAutolinks.examples'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; /** * Create an autolink reference for a repository diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index b8716a3f8f..5d5366a7b1 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -22,7 +22,7 @@ import { import { ScmIntegrationRegistry } from '@backstage/integration'; import { examples } from './githubBranchProtection.examples'; import * as inputProps from './inputProperties'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { Octokit } from 'octokit'; import { enableBranchProtectionOnDefaultRepoBranch } from './gitHelpers'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts index c9a881c6b1..6a7a2f86c6 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts @@ -20,7 +20,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { Octokit } from 'octokit'; import Sodium from 'libsodium-wrappers'; import { examples } from './githubDeployKey.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 736b562d54..5d3817ee6d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -20,7 +20,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { Octokit } from 'octokit'; import Sodium from 'libsodium-wrappers'; import { examples } from './gitHubEnvironment.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts index 75ac6025ce..8230053fda 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts @@ -25,7 +25,7 @@ import { import { createGithubIssuesLabelAction } from './githubIssuesLabel'; import yaml from 'yaml'; import { examples } from './githubIssuesLabel.examples'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; jest.mock('./helpers', () => { return { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts index 13c7df68cb..1340a28d14 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts @@ -23,7 +23,7 @@ import { import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; jest.mock('./helpers', () => { return { diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts index 7814429bd7..0ce6642f39 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.ts @@ -24,7 +24,7 @@ import { } from '@backstage/plugin-scaffolder-node'; import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { examples } from './githubIssuesLabel.examples'; /** diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts index f8df207638..54bcfbcc43 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts @@ -25,7 +25,7 @@ import { parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; import { examples } from './githubPagesEnable.examples'; -import { getOctokitOptions } from '@backstage/plugin-scaffolder-backend-module-github'; +import { getOctokitOptions } from '../util'; /** * Creates a new action that enables GitHub Pages for a repository. diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts index 63496133f1..44830b3047 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPullRequest.ts @@ -28,7 +28,7 @@ import { import { Octokit } from 'octokit'; import { CustomErrorBase, InputError } from '@backstage/errors'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { examples } from './githubPullRequest.examples'; import { LoggerService, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index a3a758e005..b103dffb15 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -24,10 +24,8 @@ import { createTemplateAction, parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; -import { - createGithubRepoWithCollaboratorsAndTopics, - getOctokitOptions, -} from './helpers'; +import { createGithubRepoWithCollaboratorsAndTopics } from './helpers'; +import { getOctokitOptions } from '../util'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; import { examples } from './githubRepoCreate.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts index b11c4b8f5c..dcb159806c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts @@ -25,7 +25,8 @@ import { createTemplateAction, parseRepoUrl, } from '@backstage/plugin-scaffolder-node'; -import { getOctokitOptions, initRepoPushAndProtect } from './helpers'; +import { initRepoPushAndProtect } from './helpers'; +import { getOctokitOptions } from '../util'; import * as inputProps from './inputProperties'; import * as outputProps from './outputProperties'; import { examples } from './githubRepoPush.examples'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts index 0289d3c69b..13200a56d0 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubWebhook.ts @@ -25,7 +25,7 @@ import { import { emitterEventNames } from '@octokit/webhooks'; import { assertError, InputError } from '@backstage/errors'; import { Octokit } from 'octokit'; -import { getOctokitOptions } from './helpers'; +import { getOctokitOptions } from '../util'; import { examples } from './githubWebhook.examples'; /** diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index 45e4229e73..8e6e521c29 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -15,13 +15,7 @@ */ import { Config } from '@backstage/config'; -import { assertError, InputError, NotFoundError } from '@backstage/errors'; -import { - DefaultGithubCredentialsProvider, - GithubCredentialsProvider, - ScmIntegrationRegistry, -} from '@backstage/integration'; -import { OctokitOptions } from '@octokit/core/dist-types/types'; +import { assertError, NotFoundError } from '@backstage/errors'; import { Octokit } from 'octokit'; import { @@ -36,75 +30,6 @@ import { } from './gitHelpers'; import { LoggerService } from '@backstage/backend-plugin-api'; -const DEFAULT_TIMEOUT_MS = 60_000; - -/** - * Helper for generating octokit configuration options for given repoUrl. - * If no token is provided, it will attempt to get a token from the credentials provider. - * @public - */ -export async function getOctokitOptions(options: { - integrations: ScmIntegrationRegistry; - credentialsProvider?: GithubCredentialsProvider; - host: string; - owner?: string; - repo?: string; - token?: string; -}): Promise { - const { integrations, credentialsProvider, host, owner, repo, token } = - options; - - const requestOptions = { - // set timeout to 60 seconds - timeout: DEFAULT_TIMEOUT_MS, - }; - - 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'], - request: requestOptions, - }; - } - - if (!owner || !repo) { - throw new InputError( - `No owner and/or owner provided, which is required if a token is not provided`, - ); - } - - const githubCredentialsProvider = - credentialsProvider ?? - DefaultGithubCredentialsProvider.fromIntegrations(integrations); - - 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}. Make sure GitHub auth is configured correctly. See https://backstage.io/docs/auth/github/provider for more details.`, - ); - } - - return { - auth: credentialProviderToken, - baseUrl: integrationConfig.apiBaseUrl, - previews: ['nebula-preview'], - }; -} - export async function createGithubRepoWithCollaboratorsAndTopics( client: Octokit, repo: string, diff --git a/plugins/scaffolder-backend-module-github/src/actions/index.ts b/plugins/scaffolder-backend-module-github/src/actions/index.ts index c758415e1d..7cf4109e6b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/index.ts @@ -29,5 +29,3 @@ export { createPublishGithubAction } from './github'; export { createGithubAutolinksAction } from './githubAutolinks'; export { createGithubPagesEnableAction } from './githubPagesEnable'; export { createGithubBranchProtectionAction } from './githubBranchProtection'; - -export { getOctokitOptions } from './helpers'; diff --git a/plugins/scaffolder-backend-module-github/src/index.ts b/plugins/scaffolder-backend-module-github/src/index.ts index 219ba54dbb..cf0bc003cb 100644 --- a/plugins/scaffolder-backend-module-github/src/index.ts +++ b/plugins/scaffolder-backend-module-github/src/index.ts @@ -22,3 +22,4 @@ export * from './actions'; export { githubModule as default } from './module'; +export { getOctokitOptions } from './util'; diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts new file mode 100644 index 0000000000..575ad8c6ed --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2025 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'; + +const DEFAULT_TIMEOUT_MS = 60_000; + +/** + * Helper for generating octokit configuration options for given repoUrl. + * If no token is provided, it will attempt to get a token from the credentials provider. + * @public + */ +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; + host: string; + owner?: string; + repo?: string; + token?: string; +}): Promise { + const { integrations, credentialsProvider, host, owner, repo, token } = + options; + + const requestOptions = { + // set timeout to 60 seconds + timeout: DEFAULT_TIMEOUT_MS, + }; + + 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'], + request: requestOptions, + }; + } + + if (!owner || !repo) { + throw new InputError( + `No owner and/or owner provided, which is required if a token is not provided`, + ); + } + + const githubCredentialsProvider = + credentialsProvider ?? + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + + 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}. Make sure GitHub auth is configured correctly. See https://backstage.io/docs/auth/github/provider for more details.`, + ); + } + + return { + auth: credentialProviderToken, + baseUrl: integrationConfig.apiBaseUrl, + previews: ['nebula-preview'], + }; +} From 02445b1364989d349ea74b469fc407d117ad63a6 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 14:30:23 +0100 Subject: [PATCH 03/17] feat(scaffolder): add support for autocompleting GitHub owners to scaffolder Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.ts | 58 ++++++++++++++++++ .../src/module.ts | 14 ++++- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 61 ++++++++++++++++--- .../fields/RepoUrlPicker/RepoUrlPicker.tsx | 4 ++ 4 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts new file mode 100644 index 0000000000..a015067f52 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2024 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 { getOctokitOptions } from '../util'; +import { Octokit } from 'octokit'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +export function createHandleAutocompleteRequest(options: { + integrations: ScmIntegrationRegistry; +}) { + return async function handleAutocompleteRequest({ + resource, + token, + context, + }: { + resource: string; + token: string; + context: Record; + }): Promise<{ results: { title?: string; id: string }[] }> { + const { integrations } = options; + const octokitOptions = await getOctokitOptions({ + integrations, + token, + host: context.host ?? 'github.com', + }); + const client = new Octokit(octokitOptions); + + switch (resource) { + case 'owners': { + const organizations = await client.paginate( + client.rest.orgs.listForAuthenticatedUser, + ); + + const results = organizations.map(organization => ({ + id: organization.login, + })); + + return { results }; + } + default: + throw new InputError(`Invalid resource: ${resource}`); + } + }; +} diff --git a/plugins/scaffolder-backend-module-github/src/module.ts b/plugins/scaffolder-backend-module-github/src/module.ts index 0387473fdb..02dfc2d7f3 100644 --- a/plugins/scaffolder-backend-module-github/src/module.ts +++ b/plugins/scaffolder-backend-module-github/src/module.ts @@ -17,7 +17,10 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { + scaffolderActionsExtensionPoint, + scaffolderAutocompleteExtensionPoint, +} from '@backstage/plugin-scaffolder-node/alpha'; import { createGithubActionsDispatchAction, createGithubAutolinksAction, @@ -37,6 +40,7 @@ import { ScmIntegrations, } from '@backstage/integration'; import { CatalogClient } from '@backstage/catalog-client'; +import { createHandleAutocompleteRequest } from './autocomplete/autocomplete'; /** * @public @@ -52,8 +56,9 @@ export const githubModule = createBackendModule({ config: coreServices.rootConfig, discovery: coreServices.discovery, auth: coreServices.auth, + autocomplete: scaffolderAutocompleteExtensionPoint, }, - async init({ scaffolder, config, discovery, auth }) { + async init({ scaffolder, config, discovery, auth, autocomplete }) { const integrations = ScmIntegrations.fromConfig(config); const githubCredentialsProvider = DefaultGithubCredentialsProvider.fromIntegrations(integrations); @@ -109,6 +114,11 @@ export const githubModule = createBackendModule({ integrations, }), ); + + autocomplete.addAutocompleteProvider({ + id: 'github', + handler: createHandleAutocompleteRequest({ integrations }), + }); }, }); }, diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 6f6cbfb355..09850d85c0 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import TextField from '@material-ui/core/TextField'; @@ -21,13 +21,18 @@ import { Select, SelectItem } from '@backstage/core-components'; import { BaseRepoUrlPickerProps } from './types'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { scaffolderTranslationRef } from '../../../translation'; +import { useApi } from '@backstage/core-plugin-api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; +import useDebounce from 'react-use/esm/useDebounce'; +import Autocomplete from '@material-ui/lab/Autocomplete'; export const GithubRepoPicker = ( props: BaseRepoUrlPickerProps<{ allowedOwners?: string[]; + accessToken?: string; }>, ) => { - const { allowedOwners = [], rawErrors, state, onChange } = props; + const { allowedOwners = [], rawErrors, state, onChange, accessToken } = props; const { t } = useTranslationRef(scaffolderTranslationRef); const ownerItems: SelectItem[] = allowedOwners ? allowedOwners.map(i => ({ label: i, value: i })) @@ -35,6 +40,33 @@ export const GithubRepoPicker = ( const { owner } = state; + const scaffolderApi = useApi(scaffolderApiRef); + + const [availableOwners, setAvailableOwners] = useState([]); + + // Update available owners when client is available + const updateAvailableOwners = useCallback(() => { + if (!scaffolderApi.autocomplete || !accessToken) { + setAvailableOwners([]); + return; + } + + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'owners', + provider: 'github', + }) + .then(({ results }) => { + setAvailableOwners(results.map(r => r.id)); + }) + .catch(() => { + setAvailableOwners([]); + }); + }, [scaffolderApi, accessToken]); + + useDebounce(updateAvailableOwners, 500, [updateAvailableOwners]); + return ( <> - - {t('fields.githubRepoPicker.owner.description')} - ) : ( - onChange({ owner: e.target.value })} - helperText={t('fields.githubRepoPicker.owner.description')} + { + onChange({ owner: newValue || '' }); + }} + options={availableOwners} + renderInput={params => ( + + )} + freeSolo + autoSelect /> )} + + {t('fields.githubRepoPicker.owner.description')} + ); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index ef6654b60c..3c6eba1695 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -186,6 +186,10 @@ export const RepoUrlPicker = ( onChange={updateLocalState} rawErrors={rawErrors} state={state} + accessToken={ + uiSchema?.['ui:options']?.requestUserCredentials?.secretsKey && + secrets[uiSchema['ui:options'].requestUserCredentials.secretsKey] + } /> )} {hostType === 'gitea' && ( From 9f7b6ac9b1b6be5a08493bba271b34f38ee2c620 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 13:51:07 +0100 Subject: [PATCH 04/17] feat(scaffolder): add support for autocompleting GitHub repositories to scaffolder Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.ts | 10 ++--- .../fields/RepoUrlPicker/GithubRepoPicker.tsx | 45 +++++++++++++++---- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts index a015067f52..08041640d9 100644 --- a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts @@ -40,14 +40,12 @@ export function createHandleAutocompleteRequest(options: { const client = new Octokit(octokitOptions); switch (resource) { - case 'owners': { - const organizations = await client.paginate( - client.rest.orgs.listForAuthenticatedUser, + case 'repositoriesWithOwner': { + const repositoriesWithOwner = await client.paginate( + client.rest.repos.listForAuthenticatedUser, ); - const results = organizations.map(organization => ({ - id: organization.login, - })); + const results = repositoriesWithOwner.map(r => ({ id: r.full_name })); return { results }; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 09850d85c0..4aa9cc057b 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; import TextField from '@material-ui/core/TextField'; @@ -25,6 +25,8 @@ import { useApi } from '@backstage/core-plugin-api'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import useDebounce from 'react-use/esm/useDebounce'; import Autocomplete from '@material-ui/lab/Autocomplete'; +import uniq from 'lodash/uniq'; +import map from 'lodash/map'; export const GithubRepoPicker = ( props: BaseRepoUrlPickerProps<{ @@ -42,30 +44,55 @@ export const GithubRepoPicker = ( const scaffolderApi = useApi(scaffolderApiRef); - const [availableOwners, setAvailableOwners] = useState([]); + const [availableRepositoriesWithOwner, setAvailableRepositoriesWithOwner] = + useState<{ owner: string; name: string }[]>([]); - // Update available owners when client is available - const updateAvailableOwners = useCallback(() => { + // Update available repositories with owner when client is available + const updateAvailableRepositoriesWithOwner = useCallback(() => { if (!scaffolderApi.autocomplete || !accessToken) { - setAvailableOwners([]); + setAvailableRepositoriesWithOwner([]); return; } scaffolderApi .autocomplete({ token: accessToken, - resource: 'owners', + resource: 'repositoriesWithOwner', provider: 'github', }) .then(({ results }) => { - setAvailableOwners(results.map(r => r.id)); + setAvailableRepositoriesWithOwner( + results.map(r => { + const [rOwner, rName] = r.id.split('/'); + return { owner: rOwner, name: rName }; + }), + ); }) .catch(() => { - setAvailableOwners([]); + setAvailableRepositoriesWithOwner([]); }); }, [scaffolderApi, accessToken]); - useDebounce(updateAvailableOwners, 500, [updateAvailableOwners]); + useDebounce(updateAvailableRepositoriesWithOwner, 500, [ + updateAvailableRepositoriesWithOwner, + ]); + + // Update available owners when available repositories with owner change + const availableOwners = useMemo( + () => uniq(map(availableRepositoriesWithOwner, 'owner')), + [availableRepositoriesWithOwner], + ); + + // Update available repositories when available repositories with owner change or when owner changes + const updateAvailableRepositories = useCallback(() => { + const availableRepos = availableRepositoriesWithOwner.flatMap(r => + r.owner === owner ? [{ name: r.name }] : [], + ); + + onChange({ availableRepos }); + }, [availableRepositoriesWithOwner, owner, onChange]); + + useDebounce(updateAvailableRepositories, 500, [updateAvailableRepositories]); return ( <> From 19fb7261da7856acfda1a8c5441b609c7e502ad5 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 14:22:45 +0100 Subject: [PATCH 05/17] test(scaffolder): add tests for handleAutocompleteRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts new file mode 100644 index 0000000000..a107117129 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2024 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 { createHandleAutocompleteRequest } from './autocomplete'; +import { ScmIntegrationRegistry } from '@backstage/integration'; + +jest.mock('../util', () => { + return { + getOctokitOptions: jest.fn(), + }; +}); + +const mockOctokit = { + paginate: async (fn: any) => (await fn()).data, + rest: { + repos: { + listForAuthenticatedUser: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('handleAutocompleteRequest', () => { + const mockIntegrations = {} as ScmIntegrationRegistry; + + it('should return repositories with owner', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + mockOctokit.rest.repos.listForAuthenticatedUser.mockResolvedValue({ + data: [ + { + full_name: 'backstage/backstage', + }, + ], + }); + + const result = await handleAutocompleteRequest({ + resource: 'repositoriesWithOwner', + token: 'token', + context: {}, + }); + + expect(result).toEqual({ + results: [{ id: 'backstage/backstage' }], + }); + }); + + it('should throw an error for invalid resource', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + await expect( + handleAutocompleteRequest({ + resource: 'invalid', + token: 'token', + context: {}, + }), + ).rejects.toThrow(InputError); + }); + + it('should throw an error if context id is missing for repositories', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + await expect( + handleAutocompleteRequest({ + resource: 'repositories', + token: 'token', + context: {}, + }), + ).rejects.toThrow(InputError); + }); +}); From fc0eef2b67373de58acaaef397726b9078ebdba1 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 14:45:27 +0100 Subject: [PATCH 06/17] test(scaffolder): fix tests for GithubRepoPicker Signed-off-by: Benjamin Janssens --- .../RepoUrlPicker/GithubRepoPicker.test.tsx | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index 3a01c6de7c..a3e233c908 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -14,22 +14,35 @@ * limitations under the License. */ -import React from 'react'; +import React, { act } from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; import { fireEvent } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; describe('GithubRepoPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest.fn().mockImplementation(opts => + Promise.resolve({ + results: [{ title: `${opts.resource}_example` }], + }), + ), + }; describe('owner field', () => { it('renders a select if there is a list of allowed owners', async () => { const allowedOwners = ['owner1', 'owner2']; const { findByText } = await renderInTestApp( - , + + + , ); expect(await findByText('owner1')).toBeInTheDocument(); @@ -40,12 +53,14 @@ describe('GithubRepoPicker', () => { const onChange = jest.fn(); const allowedOwners = ['owner1', 'owner2']; const { getByRole } = await renderInTestApp( - , + + + , ); await fireEvent.change(getByRole('combobox'), { @@ -59,12 +74,14 @@ describe('GithubRepoPicker', () => { const onChange = jest.fn(); const allowedOwners = ['owner1']; const { getByRole } = await renderInTestApp( - , + + + , ); expect(getByRole('combobox')).toBeDisabled(); @@ -73,14 +90,20 @@ describe('GithubRepoPicker', () => { it('should display free text if no allowed owners are passed', async () => { const onChange = jest.fn(); const { getAllByRole } = await renderInTestApp( - , + + + , ); const ownerField = getAllByRole('textbox')[0]; - fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } }); + act(() => { + ownerField.focus(); + fireEvent.change(ownerField, { target: { value: 'my-mock-owner' } }); + ownerField.blur(); + }); expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' }); }); From c3f746ca731662c0b5473e8843298dd4bacaf8d2 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:18:19 +0100 Subject: [PATCH 07/17] test(scaffolder): fix more existing tests Signed-off-by: Benjamin Janssens --- .../src/actions/githubIssuesLabel.examples.test.ts | 2 +- .../src/actions/githubIssuesLabel.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts index 8230053fda..4aee57130c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.examples.test.ts @@ -27,7 +27,7 @@ import yaml from 'yaml'; import { examples } from './githubIssuesLabel.examples'; import { getOctokitOptions } from '../util'; -jest.mock('./helpers', () => { +jest.mock('../util', () => { return { getOctokitOptions: jest.fn(), }; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts index 1340a28d14..af1d7d552c 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubIssuesLabel.test.ts @@ -25,7 +25,7 @@ import { ConfigReader } from '@backstage/config'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { getOctokitOptions } from '../util'; -jest.mock('./helpers', () => { +jest.mock('../util', () => { return { getOctokitOptions: jest.fn(), }; From 5cab47abae58552b59e4891360be402c7b781f23 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:18:47 +0100 Subject: [PATCH 08/17] test(scaffolder): add autocompletion tests for GithubRepoPicker Signed-off-by: Benjamin Janssens --- .../RepoUrlPicker/GithubRepoPicker.test.tsx | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index a3e233c908..e9a7041fc5 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -16,18 +16,26 @@ import React, { act } from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { ScaffolderApi, scaffolderApiRef, } from '@backstage/plugin-scaffolder-react'; +import userEvent from '@testing-library/user-event'; describe('GithubRepoPicker', () => { const scaffolderApiMock: Partial = { autocomplete: jest.fn().mockImplementation(opts => Promise.resolve({ - results: [{ title: `${opts.resource}_example` }], + results: [ + { + id: + opts.resource === 'repositoriesWithOwner' + ? 'spotify/backstage' + : `${opts.resource}_example`, + }, + ], }), ), }; @@ -108,4 +116,58 @@ describe('GithubRepoPicker', () => { expect(onChange).toHaveBeenCalledWith({ owner: 'my-mock-owner' }); }); }); + + describe('autocompletion', () => { + it('should populate owners if accessToken is provided', async () => { + const onChange = jest.fn(); + + const { getAllByRole, getByText } = await renderInTestApp( + + + , + ); + + // Open the Autcomplete dropdown + const ownerInput = getAllByRole('textbox')[0]; + await userEvent.click(ownerInput); + + // Verify that the available owners are shown + await waitFor(() => expect(getByText('spotify')).toBeInTheDocument()); + + // Verify that selecting an option calls onChange + await userEvent.click(getByText('spotify')); + expect(onChange).toHaveBeenCalledWith({ + owner: 'spotify', + }); + }); + + it('should populate owners if owner and accessToken are provided', async () => { + const onChange = jest.fn(); + + await renderInTestApp( + + + , + ); + + // Verify that the available repos are updated + await waitFor( + () => + expect(onChange).toHaveBeenCalledWith({ + availableRepos: [{ name: 'backstage' }], + }), + { timeout: 1500 }, + ); + }); + }); }); From 8e67e4a6ee644e952b5387b175dec3ce943d9d27 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:22:22 +0100 Subject: [PATCH 09/17] chore(scaffolder): add changeset Signed-off-by: Benjamin Janssens --- .changeset/wet-bees-heal.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/wet-bees-heal.md diff --git a/.changeset/wet-bees-heal.md b/.changeset/wet-bees-heal.md new file mode 100644 index 0000000000..6708b42d16 --- /dev/null +++ b/.changeset/wet-bees-heal.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added support for autocompletion to GithubRepoPicker component From 2e296c403a6df6a8b663d14b290f3a5f596d16ab Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 16:10:20 +0100 Subject: [PATCH 10/17] test(scaffolder): remove invalid test Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts index a107117129..8556fc9428 100644 --- a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts @@ -80,18 +80,4 @@ describe('handleAutocompleteRequest', () => { }), ).rejects.toThrow(InputError); }); - - it('should throw an error if context id is missing for repositories', async () => { - const handleAutocompleteRequest = createHandleAutocompleteRequest({ - integrations: mockIntegrations, - }); - - await expect( - handleAutocompleteRequest({ - resource: 'repositories', - token: 'token', - context: {}, - }), - ).rejects.toThrow(InputError); - }); }); From 38543fa980dabef4a77cb382b1fb9a2905bcd295 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 17:01:20 +0100 Subject: [PATCH 11/17] chore(scaffolder): use host in GitHub autocompletion Signed-off-by: Benjamin Janssens --- .../fields/RepoUrlPicker/GithubRepoPicker.test.tsx | 6 +++--- .../components/fields/RepoUrlPicker/GithubRepoPicker.tsx | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx index e9a7041fc5..d5657606a7 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.test.tsx @@ -126,7 +126,7 @@ describe('GithubRepoPicker', () => { , @@ -146,7 +146,7 @@ describe('GithubRepoPicker', () => { }); }); - it('should populate owners if owner and accessToken are provided', async () => { + it('should populate repositories if owner and accessToken are provided', async () => { const onChange = jest.fn(); await renderInTestApp( @@ -154,7 +154,7 @@ describe('GithubRepoPicker', () => { , diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx index 4aa9cc057b..66a662b530 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GithubRepoPicker.tsx @@ -40,7 +40,7 @@ export const GithubRepoPicker = ( ? allowedOwners.map(i => ({ label: i, value: i })) : [{ label: 'Loading...', value: 'loading' }]; - const { owner } = state; + const { host, owner } = state; const scaffolderApi = useApi(scaffolderApiRef); @@ -49,7 +49,7 @@ export const GithubRepoPicker = ( // Update available repositories with owner when client is available const updateAvailableRepositoriesWithOwner = useCallback(() => { - if (!scaffolderApi.autocomplete || !accessToken) { + if (!scaffolderApi.autocomplete || !accessToken || !host) { setAvailableRepositoriesWithOwner([]); return; } @@ -59,6 +59,7 @@ export const GithubRepoPicker = ( token: accessToken, resource: 'repositoriesWithOwner', provider: 'github', + context: { host }, }) .then(({ results }) => { setAvailableRepositoriesWithOwner( @@ -71,7 +72,7 @@ export const GithubRepoPicker = ( .catch(() => { setAvailableRepositoriesWithOwner([]); }); - }, [scaffolderApi, accessToken]); + }, [scaffolderApi, accessToken, host]); useDebounce(updateAvailableRepositoriesWithOwner, 500, [ updateAvailableRepositoriesWithOwner, From 0a762d998bf9fda79faf8616f20cf733cccb9749 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 13:34:25 +0100 Subject: [PATCH 12/17] refactor(scaffolder): re-add previous signature for getOctokitOptions and make it deprecated Signed-off-by: Benjamin Janssens --- .../report.api.md | 8 +++++ .../src/util.ts | 34 ++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 782846dec7..476e8fde1e 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -477,10 +477,18 @@ export const createPublishGithubPullRequestAction: ( export function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + token?: string; host: string; owner?: string; repo?: string; +}): Promise; + +// @public @deprecated +export function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; token?: string; + repoUrl: string; }): Promise; // @public diff --git a/plugins/scaffolder-backend-module-github/src/util.ts b/plugins/scaffolder-backend-module-github/src/util.ts index 575ad8c6ed..c25ca99f5f 100644 --- a/plugins/scaffolder-backend-module-github/src/util.ts +++ b/plugins/scaffolder-backend-module-github/src/util.ts @@ -20,32 +20,58 @@ import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; +import { parseRepoUrl } from '@backstage/plugin-scaffolder-node'; import { OctokitOptions } from '@octokit/core/dist-types/types'; const DEFAULT_TIMEOUT_MS = 60_000; /** - * Helper for generating octokit configuration options for given repoUrl. + * Helper for generating octokit configuration options. * If no token is provided, it will attempt to get a token from the credentials provider. * @public */ export async function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; + token?: string; host: string; owner?: string; repo?: string; +}): Promise; + +/** + * Helper for generating octokit configuration options for given repoUrl. + * If no token is provided, it will attempt to get a token from the credentials provider. + * @public + * @deprecated Use options `host`, `owner` and `repo` instead of `repoUrl`. + */ +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; token?: string; + repoUrl: string; +}): Promise; + +export async function getOctokitOptions(options: { + integrations: ScmIntegrationRegistry; + credentialsProvider?: GithubCredentialsProvider; + token?: string; + host?: string; + owner?: string; + repo?: string; + repoUrl?: string; }): Promise { - const { integrations, credentialsProvider, host, owner, repo, token } = - options; + const { integrations, credentialsProvider, token, repoUrl } = options; + const { host, owner, repo } = repoUrl + ? parseRepoUrl(repoUrl, integrations) + : options; const requestOptions = { // set timeout to 60 seconds timeout: DEFAULT_TIMEOUT_MS, }; - const integrationConfig = integrations.github.byHost(host)?.config; + const integrationConfig = integrations.github.byHost(host!)?.config; if (!integrationConfig) { throw new InputError(`No integration for host ${host}`); From 9e0bd0a7883b54f5815cc6c7bb03053e985fb6b7 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 13:49:18 +0100 Subject: [PATCH 13/17] chore(scaffolder): improve GitHub scaffolder actions code Signed-off-by: Benjamin Janssens --- .../src/actions/github.ts | 8 ++++---- .../src/actions/githubBranchProtection.ts | 8 ++++---- .../src/actions/githubDeployKey.ts | 8 ++++---- .../src/actions/githubEnvironment.ts | 8 ++++---- .../src/actions/githubPagesEnable.ts | 8 ++++---- .../src/actions/githubRepoCreate.ts | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index 4c04087670..6c47a2dade 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -227,6 +227,10 @@ export function createPublishGithubAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -237,10 +241,6 @@ export function createPublishGithubAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - const newRepo = await createGithubRepoWithCollaboratorsAndTopics( client, repo, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts index 5d5366a7b1..6de447541b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubBranchProtection.ts @@ -117,6 +117,10 @@ export function createGithubBranchProtectionAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -126,10 +130,6 @@ export function createGithubBranchProtectionAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const repository = await client.rest.repos.get({ owner: owner, repo: repo, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts index 6a7a2f86c6..a91ac8ba5e 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubDeployKey.ts @@ -109,6 +109,10 @@ export function createGithubDeployKeyAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -117,10 +121,6 @@ export function createGithubDeployKeyAction(options: { repo, }); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const client = new Octokit(octokitOptions); await client.rest.repos.createDeployKey({ diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 5d3817ee6d..49bd96db9d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -177,6 +177,10 @@ export function createGithubEnvironmentAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + const octokitOptions = await getOctokitOptions({ integrations, token: providedToken, @@ -185,10 +189,6 @@ export function createGithubEnvironmentAction(options: { repo, }); - if (!owner) { - throw new InputError(`No owner provided for repo ${repoUrl}`); - } - const client = new Octokit(octokitOptions); const repository = await client.rest.repos.get({ owner: owner, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts index 54bcfbcc43..9212f77338 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts @@ -94,6 +94,10 @@ export function createGithubPagesEnableAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -104,10 +108,6 @@ export function createGithubPagesEnableAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - ctx.logger.info( `Attempting to enable GitHub Pages for ${owner}/${repo} with "${buildType}" build type, on source branch "${sourceBranch}" and source path "${sourcePath}"`, ); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts index b103dffb15..d32f7447a6 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.ts @@ -182,6 +182,10 @@ export function createGithubRepoCreateAction(options: { const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + const octokitOptions = await getOctokitOptions({ integrations, credentialsProvider: githubCredentialsProvider, @@ -192,10 +196,6 @@ export function createGithubRepoCreateAction(options: { }); const client = new Octokit(octokitOptions); - if (!owner) { - throw new InputError('Invalid repository owner provided in repoUrl'); - } - const newRepo = await createGithubRepoWithCollaboratorsAndTopics( client, repo, From 5c187f9313510d55b9eba0b65fc18dd198cea44c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 14:08:58 +0100 Subject: [PATCH 14/17] chore(scaffolder): add changeset for getOctokitOptions function signature change Signed-off-by: Benjamin Janssens --- .changeset/fast-rabbits-unite.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/fast-rabbits-unite.md diff --git a/.changeset/fast-rabbits-unite.md b/.changeset/fast-rabbits-unite.md new file mode 100644 index 0000000000..f94e68449c --- /dev/null +++ b/.changeset/fast-rabbits-unite.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +The `getOctokitOptions` function signature with `repoUrl` option has been deprecated in favour of a function signature with individual `host`, `owner`, and `repo` parameters: + +```diff + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider, + token, +- repoUrl, ++ host, ++ owner, ++ repo, + }); +``` From af0a23d8b4070e9baec6fb9a0bda94397db86dc3 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 14:33:27 +0100 Subject: [PATCH 15/17] chore(scaffolder): update API reports Signed-off-by: Benjamin Janssens --- plugins/scaffolder-backend/report.api.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 9d39f52d85..9bbe3cfec1 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -332,7 +332,11 @@ export const createPublishGithubPullRequestAction: ( commitMessage?: string | undefined; update?: boolean | undefined; forceFork?: boolean | undefined; - gitAuthorName?: string | undefined; + gitAuthorName?: string | undefined + /** + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead + */; gitAuthorEmail?: string | undefined; forceEmptyGitAuthor?: boolean | undefined; }, From 301ead8650a0cbbb093ad67e8932440838b82710 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 4 Feb 2025 14:42:44 +0100 Subject: [PATCH 16/17] chore: fixing issue with api-extractor reports Signed-off-by: blam --- plugins/scaffolder-backend/report.api.md | 66 +++++++------- .../src/scaffolder/actions/deprecated.ts | 87 +++++++++++-------- 2 files changed, 88 insertions(+), 65 deletions(-) diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 9bbe3cfec1..8fe17d26f8 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -9,14 +9,27 @@ import { ActionContext as ActionContext_2 } from '@backstage/plugin-scaffolder-n import { AuditorService } from '@backstage/backend-plugin-api'; import { AuthService } from '@backstage/backend-plugin-api'; import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha'; -import * as azure from '@backstage/plugin-scaffolder-backend-module-azure'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; -import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket'; -import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; -import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; +import { createGithubActionsDispatchAction as createGithubActionsDispatchAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubDeployKeyAction as createGithubDeployKeyAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubEnvironmentAction as createGithubEnvironmentAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubIssuesLabelAction as createGithubIssuesLabelAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { CreateGithubPullRequestActionOptions as CreateGithubPullRequestActionOptions_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubRepoCreateAction as createGithubRepoCreateAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubRepoPushAction as createGithubRepoPushAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createGithubWebhookAction as createGithubWebhookAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createPublishAzureAction as createPublishAzureAction_2 } from '@backstage/plugin-scaffolder-backend-module-azure'; +import { createPublishBitbucketAction as createPublishBitbucketAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket'; +import { createPublishBitbucketCloudAction as createPublishBitbucketCloudAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; +import { createPublishBitbucketServerAction as createPublishBitbucketServerAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; +import { createPublishBitbucketServerPullRequestAction as createPublishBitbucketServerPullRequestAction_2 } from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; +import { createPublishGerritAction as createPublishGerritAction_2 } from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { createPublishGerritReviewAction as createPublishGerritReviewAction_2 } from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { createPublishGithubAction as createPublishGithubAction_2 } from '@backstage/plugin-scaffolder-backend-module-github'; +import { createPublishGitlabAction as createPublishGitlabAction_2 } from '@backstage/plugin-scaffolder-backend-module-gitlab'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import { Duration } from 'luxon'; @@ -25,9 +38,6 @@ import { executeShellCommand as executeShellCommand_2 } from '@backstage/plugin- import { ExecuteShellCommandOptions } from '@backstage/plugin-scaffolder-node'; import express from 'express'; import { fetchContents as fetchContents_2 } from '@backstage/plugin-scaffolder-node'; -import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit'; -import * as github from '@backstage/plugin-scaffolder-backend-module-github'; -import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { HumanDuration } from '@backstage/types'; import { IdentityApi } from '@backstage/plugin-auth-node'; @@ -265,57 +275,57 @@ export const createFilesystemRenameAction: () => TemplateAction_2< >; // @public @deprecated (undocumented) -export const createGithubActionsDispatchAction: typeof github.createGithubActionsDispatchAction; +export const createGithubActionsDispatchAction: typeof createGithubActionsDispatchAction_2; // @public @deprecated (undocumented) -export const createGithubDeployKeyAction: typeof github.createGithubDeployKeyAction; +export const createGithubDeployKeyAction: typeof createGithubDeployKeyAction_2; // @public @deprecated (undocumented) -export const createGithubEnvironmentAction: typeof github.createGithubEnvironmentAction; +export const createGithubEnvironmentAction: typeof createGithubEnvironmentAction_2; // @public @deprecated (undocumented) -export const createGithubIssuesLabelAction: typeof github.createGithubIssuesLabelAction; +export const createGithubIssuesLabelAction: typeof createGithubIssuesLabelAction_2; // @public @deprecated (undocumented) export type CreateGithubPullRequestActionOptions = - github.CreateGithubPullRequestActionOptions; + CreateGithubPullRequestActionOptions_2; // @public @deprecated (undocumented) -export const createGithubRepoCreateAction: typeof github.createGithubRepoCreateAction; +export const createGithubRepoCreateAction: typeof createGithubRepoCreateAction_2; // @public @deprecated (undocumented) -export const createGithubRepoPushAction: typeof github.createGithubRepoPushAction; +export const createGithubRepoPushAction: typeof createGithubRepoPushAction_2; // @public @deprecated (undocumented) -export const createGithubWebhookAction: typeof github.createGithubWebhookAction; +export const createGithubWebhookAction: typeof createGithubWebhookAction_2; // @public @deprecated (undocumented) -export const createPublishAzureAction: typeof azure.createPublishAzureAction; +export const createPublishAzureAction: typeof createPublishAzureAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketAction: typeof bitbucket.createPublishBitbucketAction; +export const createPublishBitbucketAction: typeof createPublishBitbucketAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketCloudAction: typeof bitbucketCloud.createPublishBitbucketCloudAction; +export const createPublishBitbucketCloudAction: typeof createPublishBitbucketCloudAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketServerAction: typeof bitbucketServer.createPublishBitbucketServerAction; +export const createPublishBitbucketServerAction: typeof createPublishBitbucketServerAction_2; // @public @deprecated (undocumented) -export const createPublishBitbucketServerPullRequestAction: typeof bitbucketServer.createPublishBitbucketServerPullRequestAction; +export const createPublishBitbucketServerPullRequestAction: typeof createPublishBitbucketServerPullRequestAction_2; // @public @deprecated (undocumented) -export const createPublishGerritAction: typeof gerrit.createPublishGerritAction; +export const createPublishGerritAction: typeof createPublishGerritAction_2; // @public @deprecated (undocumented) -export const createPublishGerritReviewAction: typeof gerrit.createPublishGerritReviewAction; +export const createPublishGerritReviewAction: typeof createPublishGerritReviewAction_2; // @public @deprecated (undocumented) -export const createPublishGithubAction: typeof github.createPublishGithubAction; +export const createPublishGithubAction: typeof createPublishGithubAction_2; // @public @deprecated (undocumented) export const createPublishGithubPullRequestAction: ( - options: github.CreateGithubPullRequestActionOptions, + options: CreateGithubPullRequestActionOptions_2, ) => TemplateAction_2< { title: string; @@ -332,11 +342,7 @@ export const createPublishGithubPullRequestAction: ( commitMessage?: string | undefined; update?: boolean | undefined; forceFork?: boolean | undefined; - gitAuthorName?: string | undefined - /** - * @public - * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead - */; + gitAuthorName?: string | undefined; gitAuthorEmail?: string | undefined; forceEmptyGitAuthor?: boolean | undefined; }, @@ -344,7 +350,7 @@ export const createPublishGithubPullRequestAction: ( >; // @public @deprecated (undocumented) -export const createPublishGitlabAction: typeof gitlab.createPublishGitlabAction; +export const createPublishGitlabAction: typeof createPublishGitlabAction_2; // @public @deprecated (undocumented) export const createPublishGitlabMergeRequestAction: (options: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts index 5d037b44b6..324e9b43e1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts @@ -13,134 +13,151 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as github from '@backstage/plugin-scaffolder-backend-module-github'; -import * as gitlab from '@backstage/plugin-scaffolder-backend-module-gitlab'; -import * as azure from '@backstage/plugin-scaffolder-backend-module-azure'; -import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket'; -import * as bitbucketCloud from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; -import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; -import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit'; +import { + createGithubActionsDispatchAction as githubActionsDispatch, + createGithubDeployKeyAction as githubDeployKey, + createGithubEnvironmentAction as githubEnvironment, + createGithubIssuesLabelAction as githubIssuesLabel, + CreateGithubPullRequestActionOptions as GithubPullRequestActionOptions, + createGithubRepoCreateAction as githubRepoCreate, + createGithubRepoPushAction as githubRepoPush, + createGithubWebhookAction as githubWebhook, + createPublishGithubAction as publishGithub, + createPublishGithubPullRequestAction as publishGithubPullRequest, +} from '@backstage/plugin-scaffolder-backend-module-github'; + +import { + createPublishGitlabAction as publishGitlab, + createPublishGitlabMergeRequestAction as publishGitlabMergeRequest, +} from '@backstage/plugin-scaffolder-backend-module-gitlab'; + +import { createPublishAzureAction as publishAzure } from '@backstage/plugin-scaffolder-backend-module-azure'; + +import { createPublishBitbucketAction as publishBitbucket } from '@backstage/plugin-scaffolder-backend-module-bitbucket'; + +import { createPublishBitbucketCloudAction as publishBitbucketCloud } from '@backstage/plugin-scaffolder-backend-module-bitbucket-cloud'; + +import { + createPublishBitbucketServerAction as publishBitbucketServer, + createPublishBitbucketServerPullRequestAction as publishBitbucketServerPullRequest, +} from '@backstage/plugin-scaffolder-backend-module-bitbucket-server'; + +import { + createPublishGerritAction as publishGerrit, + createPublishGerritReviewAction as publishGerritReview, +} from '@backstage/plugin-scaffolder-backend-module-gerrit'; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubActionsDispatchAction = - github.createGithubActionsDispatchAction; +export const createGithubActionsDispatchAction = githubActionsDispatch; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubDeployKeyAction = github.createGithubDeployKeyAction; +export const createGithubDeployKeyAction = githubDeployKey; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubEnvironmentAction = - github.createGithubEnvironmentAction; +export const createGithubEnvironmentAction = githubEnvironment; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubIssuesLabelAction = - github.createGithubIssuesLabelAction; +export const createGithubIssuesLabelAction = githubIssuesLabel; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export type CreateGithubPullRequestActionOptions = - github.CreateGithubPullRequestActionOptions; + GithubPullRequestActionOptions; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubRepoCreateAction = github.createGithubRepoCreateAction; +export const createGithubRepoCreateAction = githubRepoCreate; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubRepoPushAction = github.createGithubRepoPushAction; +export const createGithubRepoPushAction = githubRepoPush; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createGithubWebhookAction = github.createGithubWebhookAction; +export const createGithubWebhookAction = githubWebhook; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createPublishGithubAction = github.createPublishGithubAction; +export const createPublishGithubAction = publishGithub; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ -export const createPublishGithubPullRequestAction = - github.createPublishGithubPullRequestAction; +export const createPublishGithubPullRequestAction = publishGithubPullRequest; /** * @public @deprecated use "createPublishBitbucketCloudAction" from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud or "createPublishBitbucketServerAction" from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead */ -export const createPublishBitbucketAction = - bitbucket.createPublishBitbucketAction; +export const createPublishBitbucketAction = publishBitbucket; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ -export const createPublishBitbucketCloudAction = - bitbucketCloud.createPublishBitbucketCloudAction; +export const createPublishBitbucketCloudAction = publishBitbucketCloud; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ -export const createPublishBitbucketServerAction = - bitbucketServer.createPublishBitbucketServerAction; +export const createPublishBitbucketServerAction = publishBitbucketServer; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerPullRequestAction = - bitbucketServer.createPublishBitbucketServerPullRequestAction; + publishBitbucketServerPullRequest; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-azure` instead */ -export const createPublishAzureAction = azure.createPublishAzureAction; +export const createPublishAzureAction = publishAzure; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ -export const createPublishGerritAction = gerrit.createPublishGerritAction; +export const createPublishGerritAction = publishGerrit; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ -export const createPublishGerritReviewAction = - gerrit.createPublishGerritReviewAction; +export const createPublishGerritReviewAction = publishGerritReview; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ -export const createPublishGitlabAction = gitlab.createPublishGitlabAction; +export const createPublishGitlabAction = publishGitlab; /** * @public * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ -export const createPublishGitlabMergeRequestAction = - gitlab.createPublishGitlabMergeRequestAction; +export const createPublishGitlabMergeRequestAction = publishGitlabMergeRequest; From 347835413c886a30279141f292348294ac95630b Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 4 Feb 2025 14:52:34 +0100 Subject: [PATCH 17/17] chore: added deprecation prefix Signed-off-by: blam --- .changeset/fast-rabbits-unite.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fast-rabbits-unite.md b/.changeset/fast-rabbits-unite.md index f94e68449c..f4686b32cc 100644 --- a/.changeset/fast-rabbits-unite.md +++ b/.changeset/fast-rabbits-unite.md @@ -2,7 +2,7 @@ '@backstage/plugin-scaffolder-backend-module-github': patch --- -The `getOctokitOptions` function signature with `repoUrl` option has been deprecated in favour of a function signature with individual `host`, `owner`, and `repo` parameters: +**DEPRECATION**: The `getOctokitOptions` function signature with `repoUrl` option has been deprecated in favour of a function signature with individual `host`, `owner`, and `repo` parameters: ```diff const octokitOptions = await getOctokitOptions({