Merge pull request #28706 from benjidotsh/scaffolder/autocomplete-github
feat(scaffolder): add support for autocompletion to `GitHubRepoPicker`
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend-module-github': patch
|
||||
---
|
||||
|
||||
**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({
|
||||
integrations,
|
||||
credentialsProvider,
|
||||
token,
|
||||
- repoUrl,
|
||||
+ host,
|
||||
+ owner,
|
||||
+ repo,
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
'@backstage/plugin-scaffolder-backend-module-github': patch
|
||||
---
|
||||
|
||||
Added support for autocompletion to GithubRepoPicker component
|
||||
@@ -474,6 +474,16 @@ export const createPublishGithubPullRequestAction: (
|
||||
>;
|
||||
|
||||
// @public
|
||||
export function getOctokitOptions(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
credentialsProvider?: GithubCredentialsProvider;
|
||||
token?: string;
|
||||
host: string;
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
}): Promise<OctokitOptions>;
|
||||
|
||||
// @public @deprecated
|
||||
export function getOctokitOptions(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
credentialsProvider?: GithubCredentialsProvider;
|
||||
|
||||
@@ -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';
|
||||
@@ -225,20 +225,22 @@ export function createPublishGithubAction(options: {
|
||||
requiredLinearHistory = false,
|
||||
} = ctx.input;
|
||||
|
||||
const octokitOptions = await getOctokitOptions({
|
||||
integrations,
|
||||
credentialsProvider: githubCredentialsProvider,
|
||||
token: providedToken,
|
||||
repoUrl: repoUrl,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
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,
|
||||
token: providedToken,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
const newRepo = await createGithubRepoWithCollaboratorsAndTopics(
|
||||
client,
|
||||
repo,
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -115,19 +115,21 @@ export function createGithubBranchProtectionAction(options: {
|
||||
token: providedToken,
|
||||
} = ctx.input;
|
||||
|
||||
const octokitOptions = await getOctokitOptions({
|
||||
integrations,
|
||||
token: providedToken,
|
||||
repoUrl: repoUrl,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
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,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
const repository = await client.rest.repos.get({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
|
||||
@@ -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';
|
||||
@@ -107,18 +107,20 @@ export function createGithubDeployKeyAction(options: {
|
||||
token: providedToken,
|
||||
} = ctx.input;
|
||||
|
||||
const octokitOptions = await getOctokitOptions({
|
||||
integrations,
|
||||
token: providedToken,
|
||||
repoUrl: repoUrl,
|
||||
});
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
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,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
await client.rest.repos.createDeployKey({
|
||||
|
||||
@@ -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';
|
||||
@@ -175,18 +175,20 @@ export function createGithubEnvironmentAction(options: {
|
||||
// Add a 2-second delay before initiating the steps in this action.
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const octokitOptions = await getOctokitOptions({
|
||||
integrations,
|
||||
token: providedToken,
|
||||
repoUrl: repoUrl,
|
||||
});
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
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,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
const client = new Octokit(octokitOptions);
|
||||
const repository = await client.rest.repos.get({
|
||||
owner: owner,
|
||||
|
||||
+2
-2
@@ -25,9 +25,9 @@ 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', () => {
|
||||
jest.mock('../util', () => {
|
||||
return {
|
||||
getOctokitOptions: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -23,9 +23,9 @@ 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', () => {
|
||||
jest.mock('../util', () => {
|
||||
return {
|
||||
getOctokitOptions: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
@@ -92,20 +92,22 @@ export function createGithubPagesEnableAction(options: {
|
||||
token: providedToken,
|
||||
} = ctx.input;
|
||||
|
||||
const octokitOptions = await getOctokitOptions({
|
||||
integrations,
|
||||
credentialsProvider: githubCredentialsProvider,
|
||||
token: providedToken,
|
||||
repoUrl: repoUrl,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
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,
|
||||
token: providedToken,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
ctx.logger.info(
|
||||
`Attempting to enable GitHub Pages for ${owner}/${repo} with "${buildType}" build type, on source branch "${sourceBranch}" and source path "${sourcePath}"`,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -182,20 +180,22 @@ export function createGithubRepoCreateAction(options: {
|
||||
token: providedToken,
|
||||
} = ctx.input;
|
||||
|
||||
const octokitOptions = await getOctokitOptions({
|
||||
integrations,
|
||||
credentialsProvider: githubCredentialsProvider,
|
||||
token: providedToken,
|
||||
repoUrl: repoUrl,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
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,
|
||||
token: providedToken,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
const newRepo = await createGithubRepoWithCollaboratorsAndTopics(
|
||||
client,
|
||||
repo,
|
||||
|
||||
@@ -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';
|
||||
@@ -142,7 +143,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 +153,9 @@ export function createGithubRepoPushAction(options: {
|
||||
integrations,
|
||||
credentialsProvider: githubCredentialsProvider,
|
||||
token: providedToken,
|
||||
repoUrl,
|
||||
host,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
const client = new Octokit(octokitOptions);
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -15,19 +15,12 @@
|
||||
*/
|
||||
|
||||
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 {
|
||||
getRepoSourceDirectory,
|
||||
initRepoAndPush,
|
||||
parseRepoUrl,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
|
||||
import Sodium from 'libsodium-wrappers';
|
||||
@@ -37,73 +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;
|
||||
token?: string;
|
||||
repoUrl: string;
|
||||
}): Promise<OctokitOptions> {
|
||||
const { integrations, credentialsProvider, repoUrl, token } = options;
|
||||
const { owner, repo, host } = parseRepoUrl(repoUrl, integrations);
|
||||
|
||||
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) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
const githubCredentialsProvider =
|
||||
credentialsProvider ??
|
||||
DefaultGithubCredentialsProvider.fromIntegrations(integrations);
|
||||
|
||||
// TODO(blam): Consider changing this API to take host and repo instead of repoUrl, as we end up parsing in this function
|
||||
// and then parsing in the `getCredentials` function too the other side
|
||||
const { token: credentialProviderToken } =
|
||||
await githubCredentialsProvider.getCredentials({
|
||||
url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent(
|
||||
repo,
|
||||
)}`,
|
||||
});
|
||||
|
||||
if (!credentialProviderToken) {
|
||||
throw new InputError(
|
||||
`No token available for host: ${host}, with owner ${owner}, and repo ${repo}. 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,
|
||||
|
||||
@@ -29,5 +29,3 @@ export { createPublishGithubAction } from './github';
|
||||
export { createGithubAutolinksAction } from './githubAutolinks';
|
||||
export { createGithubPagesEnableAction } from './githubPagesEnable';
|
||||
export { createGithubBranchProtectionAction } from './githubBranchProtection';
|
||||
|
||||
export { getOctokitOptions } from './helpers';
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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<string, string>;
|
||||
}): 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 'repositoriesWithOwner': {
|
||||
const repositoriesWithOwner = await client.paginate(
|
||||
client.rest.repos.listForAuthenticatedUser,
|
||||
);
|
||||
|
||||
const results = repositoriesWithOwner.map(r => ({ id: r.full_name }));
|
||||
|
||||
return { results };
|
||||
}
|
||||
default:
|
||||
throw new InputError(`Invalid resource: ${resource}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -22,3 +22,4 @@
|
||||
|
||||
export * from './actions';
|
||||
export { githubModule as default } from './module';
|
||||
export { getOctokitOptions } from './util';
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 { 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.
|
||||
* 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<OctokitOptions>;
|
||||
|
||||
/**
|
||||
* 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<OctokitOptions>;
|
||||
|
||||
export async function getOctokitOptions(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
credentialsProvider?: GithubCredentialsProvider;
|
||||
token?: string;
|
||||
host?: string;
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
repoUrl?: string;
|
||||
}): Promise<OctokitOptions> {
|
||||
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;
|
||||
|
||||
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'],
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -340,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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -14,22 +14,43 @@
|
||||
* 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 { 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<ScaffolderApi> = {
|
||||
autocomplete: jest.fn().mockImplementation(opts =>
|
||||
Promise.resolve({
|
||||
results: [
|
||||
{
|
||||
id:
|
||||
opts.resource === 'repositoriesWithOwner'
|
||||
? 'spotify/backstage'
|
||||
: `${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(
|
||||
<GithubRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GithubRepoPicker
|
||||
onChange={jest.fn()}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(await findByText('owner1')).toBeInTheDocument();
|
||||
@@ -40,12 +61,14 @@ describe('GithubRepoPicker', () => {
|
||||
const onChange = jest.fn();
|
||||
const allowedOwners = ['owner1', 'owner2'];
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await fireEvent.change(getByRole('combobox'), {
|
||||
@@ -59,12 +82,14 @@ describe('GithubRepoPicker', () => {
|
||||
const onChange = jest.fn();
|
||||
const allowedOwners = ['owner1'];
|
||||
const { getByRole } = await renderInTestApp(
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
allowedOwners={allowedOwners}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
expect(getByRole('combobox')).toBeDisabled();
|
||||
@@ -73,16 +98,76 @@ describe('GithubRepoPicker', () => {
|
||||
it('should display free text if no allowed owners are passed', async () => {
|
||||
const onChange = jest.fn();
|
||||
const { getAllByRole } = await renderInTestApp(
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
/>,
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ repoName: 'repo' }}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
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' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('autocompletion', () => {
|
||||
it('should populate owners if accessToken is provided', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
const { getAllByRole, getByText } = await renderInTestApp(
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'github.com', repoName: 'repo' }}
|
||||
accessToken="foo"
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
// 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 repositories if owner and accessToken are provided', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[scaffolderApiRef, scaffolderApiMock]]}>
|
||||
<GithubRepoPicker
|
||||
onChange={onChange}
|
||||
rawErrors={[]}
|
||||
state={{ host: 'github.com', owner: 'spotify' }}
|
||||
accessToken="foo"
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
// Verify that the available repos are updated
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
availableRepos: [{ name: 'backstage' }],
|
||||
}),
|
||||
{ timeout: 1500 },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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, 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';
|
||||
@@ -21,19 +21,79 @@ 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';
|
||||
import uniq from 'lodash/uniq';
|
||||
import map from 'lodash/map';
|
||||
|
||||
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 }))
|
||||
: [{ label: 'Loading...', value: 'loading' }];
|
||||
|
||||
const { owner } = state;
|
||||
const { host, owner } = state;
|
||||
|
||||
const scaffolderApi = useApi(scaffolderApiRef);
|
||||
|
||||
const [availableRepositoriesWithOwner, setAvailableRepositoriesWithOwner] =
|
||||
useState<{ owner: string; name: string }[]>([]);
|
||||
|
||||
// Update available repositories with owner when client is available
|
||||
const updateAvailableRepositoriesWithOwner = useCallback(() => {
|
||||
if (!scaffolderApi.autocomplete || !accessToken || !host) {
|
||||
setAvailableRepositoriesWithOwner([]);
|
||||
return;
|
||||
}
|
||||
|
||||
scaffolderApi
|
||||
.autocomplete({
|
||||
token: accessToken,
|
||||
resource: 'repositoriesWithOwner',
|
||||
provider: 'github',
|
||||
context: { host },
|
||||
})
|
||||
.then(({ results }) => {
|
||||
setAvailableRepositoriesWithOwner(
|
||||
results.map(r => {
|
||||
const [rOwner, rName] = r.id.split('/');
|
||||
return { owner: rOwner, name: rName };
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
setAvailableRepositoriesWithOwner([]);
|
||||
});
|
||||
}, [scaffolderApi, accessToken, host]);
|
||||
|
||||
useDebounce(updateAvailableRepositoriesWithOwner, 500, [
|
||||
updateAvailableRepositoriesWithOwner,
|
||||
]);
|
||||
|
||||
// Update available owners when available repositories with owner change
|
||||
const availableOwners = useMemo<string[]>(
|
||||
() => 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 (
|
||||
<>
|
||||
@@ -54,19 +114,28 @@ export const GithubRepoPicker = (
|
||||
selected={owner}
|
||||
items={ownerItems}
|
||||
/>
|
||||
<FormHelperText>
|
||||
{t('fields.githubRepoPicker.owner.description')}
|
||||
</FormHelperText>
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
id="ownerInput"
|
||||
label={t('fields.githubRepoPicker.owner.inputTitle')}
|
||||
onChange={e => onChange({ owner: e.target.value })}
|
||||
helperText={t('fields.githubRepoPicker.owner.description')}
|
||||
<Autocomplete
|
||||
value={owner}
|
||||
onChange={(_, newValue) => {
|
||||
onChange({ owner: newValue || '' });
|
||||
}}
|
||||
options={availableOwners}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('fields.githubRepoPicker.owner.inputTitle')}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
freeSolo
|
||||
autoSelect
|
||||
/>
|
||||
)}
|
||||
<FormHelperText>
|
||||
{t('fields.githubRepoPicker.owner.description')}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
Reference in New Issue
Block a user