From a4563ffc608dd9d6ef12fc84c82eec77f5d8310b Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 13:32:19 +0100 Subject: [PATCH 01/16] 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 cfe3a7d5a5165788b6fce028ea6cce901d592e7c Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 13:44:33 +0100 Subject: [PATCH 02/16] 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 4317324e9e20465c5c98e8d4ea22989068cddb51 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Fri, 31 Jan 2025 14:30:23 +0100 Subject: [PATCH 03/16] 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 18da234012..7a985265d5 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 133700d8afb334a8e240c951f3e127c7852c3e5a Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 13:51:07 +0100 Subject: [PATCH 04/16] 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 cdab581dbe64b7836eca2225fe4c8b8236fd5bd2 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 14:22:45 +0100 Subject: [PATCH 05/16] 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 a9f921061dc403396ff7a0c47cbf369cb07fb721 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 14:45:27 +0100 Subject: [PATCH 06/16] 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 8466af00a1daac38e6be699e0c1e00df4b90e9ed Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:18:19 +0100 Subject: [PATCH 07/16] 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 1bea77386fc8928998dbab85652f42909c1093aa Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:18:47 +0100 Subject: [PATCH 08/16] 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 478c67228a05a9db8ba174cf7f552332ac62dbdd Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 15:22:22 +0100 Subject: [PATCH 09/16] 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 3323fc8bc74a0da7afdccae1739485def7d89982 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 16:09:44 +0100 Subject: [PATCH 10/16] feat(scaffolder): add support for autocompleting GitHub branches to handleAutocompleteRequest Signed-off-by: Benjamin Janssens --- .../src/autocomplete/autocomplete.test.ts | 32 +++++++++++++++++-- .../src/autocomplete/autocomplete.ts | 15 +++++++++ 2 files changed, 45 insertions(+), 2 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..9b9aaff9c2 100644 --- a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.test.ts @@ -29,6 +29,7 @@ const mockOctokit = { rest: { repos: { listForAuthenticatedUser: jest.fn(), + listBranches: jest.fn(), }, }, }; @@ -67,6 +68,33 @@ describe('handleAutocompleteRequest', () => { }); }); + it('should return branches', async () => { + const handleAutocompleteRequest = createHandleAutocompleteRequest({ + integrations: mockIntegrations, + }); + + mockOctokit.rest.repos.listBranches.mockResolvedValue({ + data: [ + { + name: 'main', + }, + ], + }); + + const result = await handleAutocompleteRequest({ + resource: 'branches', + token: 'token', + context: { + owner: 'backstage', + repository: 'backstage', + }, + }); + + expect(result).toEqual({ + results: [{ id: 'main' }], + }); + }); + it('should throw an error for invalid resource', async () => { const handleAutocompleteRequest = createHandleAutocompleteRequest({ integrations: mockIntegrations, @@ -81,15 +109,15 @@ describe('handleAutocompleteRequest', () => { ).rejects.toThrow(InputError); }); - it('should throw an error if context id is missing for repositories', async () => { + it('should throw an error when there are missing parameters', async () => { const handleAutocompleteRequest = createHandleAutocompleteRequest({ integrations: mockIntegrations, }); await expect( handleAutocompleteRequest({ - resource: 'repositories', token: 'token', + resource: 'branches', context: {}, }), ).rejects.toThrow(InputError); diff --git a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts index 08041640d9..35d6b4f930 100644 --- a/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts +++ b/plugins/scaffolder-backend-module-github/src/autocomplete/autocomplete.ts @@ -49,6 +49,21 @@ export function createHandleAutocompleteRequest(options: { return { results }; } + case 'branches': { + if (!context.owner || !context.repository) + throw new InputError( + 'Missing owner and/or repository context parameter', + ); + + const branches = await client.paginate(client.rest.repos.listBranches, { + owner: context.owner, + repo: context.repository, + }); + + const results = branches.map(r => ({ id: r.name })); + + return { results }; + } default: throw new InputError(`Invalid resource: ${resource}`); } From eead1a99f45c592ceddae10d0072aac8a39bf1a7 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 16:10:20 +0100 Subject: [PATCH 11/16] 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 2b32e64414e7213e83c1568bc7db1f452c908d9f Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 16:33:20 +0100 Subject: [PATCH 12/16] feat(scaffolder): add GitHubRepoBranchPicker Signed-off-by: Benjamin Janssens --- .../GitHubRepoBranchPicker.test.tsx | 104 ++++++++++++++++++ .../GitHubRepoBranchPicker.tsx | 93 ++++++++++++++++ .../RepoBranchPicker/RepoBranchPicker.tsx | 15 +++ .../fields/RepoBranchPicker/types.ts | 1 + 4 files changed, 213 insertions(+) create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx create mode 100644 plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx new file mode 100644 index 0000000000..afdfaf4d6e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx @@ -0,0 +1,104 @@ +/* + * 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 React from 'react'; +import { + ScaffolderApi, + scaffolderApiRef, +} from '@backstage/plugin-scaffolder-react'; +import { GitHubRepoBranchPicker } from './GitHubRepoBranchPicker'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import { TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; + +describe('GitHubRepoBranchPicker', () => { + const scaffolderApiMock: Partial = { + autocomplete: jest.fn().mockResolvedValue({ results: [{ id: 'branch1' }] }), + }; + + it('renders an input field', () => { + const { getByRole } = render( + + + , + ); + + expect(getByRole('textbox')).toBeInTheDocument(); + expect(getByRole('textbox')).toHaveValue('main'); + }); + + it('calls onChange when the input field changes', () => { + const onChange = jest.fn(); + + const { getByRole } = render( + + + , + ); + + const input = getByRole('textbox'); + + act(() => { + input.focus(); + fireEvent.change(input, { + target: { value: 'develop' }, + }); + input.blur(); + }); + + expect(onChange).toHaveBeenCalledWith({ branch: 'develop' }); + }); + + it('should populate branches', async () => { + const onChange = jest.fn(); + + const { getByRole, getByText } = render( + + + , + ); + + // Open the Autcomplete dropdown + const input = getByRole('textbox'); + await userEvent.click(input); + + // Verify that the available workspaces are shown + await waitFor(() => expect(getByText('branch1')).toBeInTheDocument()); + + // Verify that selecting an option calls onChange + await userEvent.click(getByText('branch1')); + expect(onChange).toHaveBeenCalledWith({ + branch: 'branch1', + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx new file mode 100644 index 0000000000..aaf448ae6c --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx @@ -0,0 +1,93 @@ +/* + * 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 { useApi } from '@backstage/core-plugin-api'; +import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import React, { useCallback, useState } from 'react'; +import useDebounce from 'react-use/esm/useDebounce'; +import { BaseRepoBranchPickerProps } from './types'; + +/** + * The underlying component that is rendered in the form for the `GitHubRepoBranchPicker` + * field extension. + * + * @public + * + */ +export const GitHubRepoBranchPicker = ({ + onChange, + state, + rawErrors, + accessToken, + required, +}: BaseRepoBranchPickerProps<{ + accessToken?: string; +}>) => { + const { owner, repository, branch } = state; + + const [availableBranches, setAvailableBranches] = useState([]); + + const scaffolderApi = useApi(scaffolderApiRef); + + const updateAvailableBranches = useCallback(() => { + if (!scaffolderApi.autocomplete || !owner || !repository || !accessToken) { + setAvailableBranches([]); + return; + } + + scaffolderApi + .autocomplete({ + token: accessToken, + resource: 'branches', + context: { owner, repository }, + provider: 'github', + }) + .then(({ results }) => { + setAvailableBranches(results.map(r => r.id)); + }) + .catch(() => { + setAvailableBranches([]); + }); + }, [owner, repository, accessToken, scaffolderApi]); + + useDebounce(updateAvailableBranches, 500, [updateAvailableBranches]); + + return ( + 0 && !branch} + > + { + onChange({ branch: newValue || '' }); + }} + options={availableBranches} + renderInput={params => ( + + )} + freeSolo + autoSelect + /> + The branch of the repository + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx index 4ca22c8f80..98b1d73b8d 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/RepoBranchPicker.tsx @@ -30,6 +30,7 @@ import { RepoBranchPickerProps } from './schema'; import { RepoBranchPickerState } from './types'; import { BitbucketRepoBranchPicker } from './BitbucketRepoBranchPicker'; import { DefaultRepoBranchPicker } from './DefaultRepoBranchPicker'; +import { GitHubRepoBranchPicker } from './GitHubRepoBranchPicker'; /** * The underlying component that is rendered in the form for the `RepoBranchPicker` @@ -102,6 +103,7 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { host: url.host, workspace: url.searchParams.get('workspace') || '', repository: url.searchParams.get('repo') || '', + owner: url.searchParams.get('owner') || '', })); } }, [repoUrl]); @@ -134,6 +136,19 @@ export const RepoBranchPicker = (props: RepoBranchPickerProps) => { required={required} /> ); + case 'github': + return ( + + ); default: return ( = T & { From 5d469c9986dba0c798a419e3b971ac1fe5fffe51 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 16:38:53 +0100 Subject: [PATCH 13/16] chore(scaffolder): add changeset Signed-off-by: Benjamin Janssens --- .changeset/pretty-apricots-poke.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/pretty-apricots-poke.md diff --git a/.changeset/pretty-apricots-poke.md b/.changeset/pretty-apricots-poke.md new file mode 100644 index 0000000000..97e3454fd4 --- /dev/null +++ b/.changeset/pretty-apricots-poke.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added support for autocompletion of GitHub branches in scaffolder From 35d1a0bff2a8d23c7bd1792efd5e86bd07471710 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 17:01:20 +0100 Subject: [PATCH 14/16] 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 1eb840ece8b4d9c0f72ad52a8fab4219a3f93bec Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Mon, 3 Feb 2025 17:05:10 +0100 Subject: [PATCH 15/16] chore(scaffolder): use host in GitHub autocompletion Signed-off-by: Benjamin Janssens --- .../GitHubRepoBranchPicker.test.tsx | 1 + .../RepoBranchPicker/GitHubRepoBranchPicker.tsx | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx index afdfaf4d6e..6467c4c81c 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx @@ -78,6 +78,7 @@ describe('GitHubRepoBranchPicker', () => { ) => { - const { owner, repository, branch } = state; + const { host, owner, repository, branch } = state; const [availableBranches, setAvailableBranches] = useState([]); const scaffolderApi = useApi(scaffolderApiRef); const updateAvailableBranches = useCallback(() => { - if (!scaffolderApi.autocomplete || !owner || !repository || !accessToken) { + if ( + !scaffolderApi.autocomplete || + !owner || + !repository || + !accessToken || + !host + ) { setAvailableBranches([]); return; } @@ -56,7 +62,7 @@ export const GitHubRepoBranchPicker = ({ .autocomplete({ token: accessToken, resource: 'branches', - context: { owner, repository }, + context: { host, owner, repository }, provider: 'github', }) .then(({ results }) => { @@ -65,7 +71,7 @@ export const GitHubRepoBranchPicker = ({ .catch(() => { setAvailableBranches([]); }); - }, [owner, repository, accessToken, scaffolderApi]); + }, [host, owner, repository, accessToken, scaffolderApi]); useDebounce(updateAvailableBranches, 500, [updateAvailableBranches]); From 85ed94bba8a376076440d78a08f79d74919d9a2e Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 4 Feb 2025 15:31:16 +0100 Subject: [PATCH 16/16] chore(scaffolder): remove incorrect merge leftovers; change new copyright notices to 2025 Signed-off-by: Benjamin Janssens --- plugins/scaffolder-backend-module-github/report.api.md | 3 --- .../fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx | 2 +- .../fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/plugins/scaffolder-backend-module-github/report.api.md b/plugins/scaffolder-backend-module-github/report.api.md index 82d7a66e92..476e8fde1e 100644 --- a/plugins/scaffolder-backend-module-github/report.api.md +++ b/plugins/scaffolder-backend-module-github/report.api.md @@ -477,9 +477,6 @@ export const createPublishGithubPullRequestAction: ( export function getOctokitOptions(options: { integrations: ScmIntegrationRegistry; credentialsProvider?: GithubCredentialsProvider; - host: string; - owner?: string; - repo?: string; token?: string; host: string; owner?: string; diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx index 6467c4c81c..3e89b13aa7 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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. diff --git a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx index f53249b5a7..e4995ceeaa 100644 --- a/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoBranchPicker/GitHubRepoBranchPicker.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * 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.