cli: remove experimental onboard command

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-09-21 12:42:25 +02:00
parent b7ef1347b4
commit 84097c7da0
29 changed files with 1 additions and 1876 deletions
+1 -1
View File
@@ -11,4 +11,4 @@
- `versions:check`: Use `yarn dedupe` or `yarn-deduplicate` instead
- `clean`: Use `backstage-cli package clean` instead
In addition, the experimental `install` command has been removed since it has not received any updates since its introduction and we're expecting usage to be low. If you where relying on this command, please let us know by opening an issue towards the main Backstage repository.
In addition, the experimental `install` and `onboard` commands have been removed since they have not received any updates since their introduction and we're expecting usage to be low. If you where relying on these commands, please let us know by opening an issue towards the main Backstage repository.
-8
View File
@@ -25,13 +25,6 @@ const configOption = [
Array<string>(),
] as const;
export function registerOnboardCommand(program: Command) {
program
.command('onboard', { hidden: true })
.description('Get help setting up your Backstage App.')
.action(lazy(() => import('./onboard').then(m => m.command)));
}
export function registerRepoCommand(program: Command) {
const command = program
.command('repo [command]')
@@ -328,7 +321,6 @@ export function registerCommands(program: Command) {
registerRepoCommand(program);
registerScriptCommand(program);
registerMigrateCommand(program);
registerOnboardCommand(program);
program
.command('versions:bump')
@@ -1,22 +0,0 @@
# onboard
## Local dev
# Create a new app
```bash
cd ..
npx @backstage/create-app
cd new-backstage-app
# Run the modded cli in the new app
../RELATIVE_PATH_TO_PROJECT/backstage/packages/cli/bin/backstage-cli onboard
? Do you want to set up Authentication for this project? (Y/n)
...
# Try the app with the new changes
yarn dev
```
@@ -1,63 +0,0 @@
/*
* Copyright 2023 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 inquirer from 'inquirer';
import { Task } from '../../../lib/tasks';
import { github, Answers as GitHubAnswers } from './providers/github';
import { gitlab, Answers as GitLabAnswers } from './providers/gitlab';
export { type GitHubAnswers, type GitLabAnswers };
export async function auth(): Promise<{
provider: string;
answers: GitHubAnswers | GitLabAnswers;
}> {
const answers = await inquirer.prompt<{
provider: string;
}>([
{
type: 'list',
name: 'provider',
message: 'Please select an authentication provider:',
choices: ['GitHub', 'GitLab'],
},
]);
const { provider } = answers;
let providerAnswers;
switch (provider) {
case 'GitHub': {
providerAnswers = await github();
break;
}
case 'GitLab': {
providerAnswers = await gitlab();
break;
}
default:
throw new Error(`Provider ${provider} not implemented yet.`);
}
Task.log();
Task.log(`Done setting up ${provider} Authentication!`);
Task.log();
return {
provider,
answers: providerAnswers,
};
}
@@ -1,43 +0,0 @@
/*
* Copyright 2023 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 * as fs from 'fs-extra';
import * as path from 'path';
import * as differ from 'diff';
import { PATCH_FOLDER } from '../files';
import { findPaths } from '@backstage/cli-common';
/* eslint-disable-next-line no-restricted-syntax */
const { targetRoot } = findPaths(__dirname);
export const patch = async (patchFile: string) => {
const patchContent = await fs.readFile(
path.join(PATCH_FOLDER, patchFile),
'utf8',
);
const targetName = patchContent.split('\n')[0].replace('--- a', '');
const targetFile = path.join(targetRoot, targetName);
const oldContent = await fs.readFile(targetFile, 'utf8');
const newContent = differ.applyPatch(oldContent, patchContent);
if (!newContent) {
throw new Error(
`Patch ${patchFile} was not applied correctly.
Did you change ${targetName} manually before running this command?`,
);
}
return await fs.writeFile(targetFile, newContent, 'utf8');
};
@@ -1,25 +0,0 @@
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -30 +30,5 @@ import { Root } from './components/Root';
-import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
+import {
+ AlertDisplay,
+ OAuthRequestDialog,
+ SignInPage,
+} from '@backstage/core-components';
@@ -35,0 +40 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/
+import { githubAuthApiRef } from '@backstage/core-plugin-api';
@@ -38,0 +44,14 @@ const app = createApp({
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ provider={{
+ id: 'github-auth-provider',
+ title: 'GitHub',
+ message: 'Sign in using GitHub',
+ apiRef: githubAuthApiRef,
+ }}
+ />
+ ),
+ },
@@ -1,25 +0,0 @@
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -30 +30,5 @@ import { Root } from './components/Root';
-import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
+import {
+ AlertDisplay,
+ OAuthRequestDialog,
+ SignInPage,
+} from '@backstage/core-components';
@@ -35,0 +40 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/
+import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
@@ -38,0 +44,14 @@ const app = createApp({
+ components: {
+ SignInPage: props => (
+ <SignInPage
+ {...props}
+ provider={{
+ id: 'gitlab-auth-provider',
+ title: 'GitLab',
+ message: 'Sign in using GitLab',
+ apiRef: gitlabAuthApiRef,
+ }}
+ />
+ ),
+ },
@@ -1,5 +0,0 @@
--- a/packages/backend/src/plugins/auth.ts
+++ b/packages/backend/src/plugins/auth.ts
@@ -38 +38 @@ export default async function createPlugin(
- github: providers.github.create({
+ gitlab: providers.gitlab.create({
@@ -1,147 +0,0 @@
/*
* Copyright 2023 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 { OAuthApp } from '@octokit/oauth-app';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import { Task } from '../../../../lib/tasks';
import { updateConfigFile } from '../../config';
import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files';
import { patch } from '../patch';
export type Answers = {
clientSecret: string;
clientId: string;
hasEnterprise: boolean;
enterpriseInstanceUrl?: string;
};
const validateCredentials = async (clientId: string, clientSecret: string) => {
try {
const app = new OAuthApp({
clientId,
clientSecret,
});
await app.createToken({
code: '%NOT-VALID-CODE%',
});
} catch (error) {
// @octokit/request returns a error.response object when a request is rejected.
// We can check it to see what kind of error we received.
// If error.response is successful we can double-check that the error itself was due to the bad code.
// If that's the case then we can assume that the client id and secret exists as we otherwise would
// have gotten a 400/404.
if (
error.response.status !== 200 &&
error.response.data.error !== 'bad_verification_code'
) {
throw new Error(`Validating GitHub Credentials failed.`);
}
}
};
const getConfig = (answers: Answers) => {
const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } =
answers;
return {
auth: {
providers: {
github: {
development: {
clientId,
clientSecret,
...(hasEnterprise && {
enterpriseInstanceUrl,
}),
},
},
},
},
};
};
export const github = async (): Promise<Answers> => {
Task.log(`
To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue(
'https://github.com/settings/developers',
)}
The Homepage URL should point to Backstage's frontend, while the Authorization callback URL will point to the auth backend.
Settings for local development:
${chalk.cyan(`
Homepage URL: http://localhost:3000
Authorization callback URL: http://localhost:7007/api/auth/github/handler/frame`)}
You can find the full documentation page here: ${chalk.blue(
'https://backstage.io/docs/auth/github/provider',
)}
`);
const answers = await inquirer.prompt<Answers>([
{
type: 'input',
name: 'clientId',
message: 'What is your Client Id?',
validate: (input: string) => (input.length ? true : false),
},
{
type: 'input',
name: 'clientSecret',
message: 'What is your Client Secret?',
validate: (input: string) => (input.length ? true : false),
},
{
type: 'confirm',
name: 'hasEnterprise',
message: 'Are you using GitHub Enterprise?',
},
{
type: 'input',
name: 'enterpriseInstanceUrl',
message: 'What is your URL for GitHub Enterprise?',
when: ({ hasEnterprise }) => hasEnterprise,
validate: (input: string) => Boolean(new URL(input)),
},
]);
const { clientId, clientSecret } = answers;
const config = getConfig(answers);
Task.log('Setting up GitHub Authentication for you...');
await Task.forItem(
'Validating',
'credentials',
async () => await validateCredentials(clientId, clientSecret),
);
await Task.forItem(
'Updating',
APP_CONFIG_FILE,
async () => await updateConfigFile(APP_CONFIG_FILE, config),
);
const patches = await fs.readdir(PATCH_FOLDER);
for (const patchFile of patches.filter(p => p.includes('github'))) {
await Task.forItem('Patching', patchFile, async () => {
await patch(patchFile);
});
}
return answers;
};
@@ -1,115 +0,0 @@
/*
* Copyright 2023 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 chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import { Task } from '../../../../lib/tasks';
import { updateConfigFile } from '../../config';
import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files';
import { patch } from '../patch';
const getConfig = (answers: Answers) => {
const { clientId, clientSecret, hasAudience, audience } = answers;
return {
auth: {
providers: {
gitlab: {
development: {
clientId,
clientSecret,
...(hasAudience && {
audience,
}),
},
},
},
},
};
};
export type Answers = {
clientSecret: string;
clientId: string;
hasAudience: boolean;
audience?: string;
};
export const gitlab = async (): Promise<Answers> => {
Task.log(`
To add GitLab authentication, you must create an Application from the GitLab Settings: ${chalk.blue(
'https://gitlab.com/-/profile/applications',
)}
The Redirect URI should point to your Backstage backend auth handler.
Settings for local development:
${chalk.cyan(`
Name: Backstage (or your custom app name)
Redirect URI: http://localhost:7007/api/auth/gitlab/handler/frame
Scopes: read_api and read_user`)}
You can find the full documentation page here: ${chalk.blue(
'https://backstage.io/docs/auth/gitlab/provider',
)}
`);
const answers = await inquirer.prompt<Answers>([
{
type: 'input',
name: 'clientId',
message: 'What is your Application Id?',
validate: (input: string) => (input.length ? true : false),
},
{
type: 'input',
name: 'clientSecret',
message: 'What is your Application Secret?',
validate: (input: string) => (input.length ? true : false),
},
{
type: 'confirm',
name: 'hasAudience',
message: 'Do you have a self-hosted instance of GitLab?',
},
{
type: 'input',
name: 'audience',
message: 'What is the URL for your GitLab instance?',
when: ({ hasAudience }) => hasAudience,
validate: (input: string) => Boolean(new URL(input)),
},
]);
const config = getConfig(answers);
Task.log('Setting up GitLab Authentication for you...');
await Task.forItem(
'Updating',
APP_CONFIG_FILE,
async () => await updateConfigFile(APP_CONFIG_FILE, config),
);
const patches = await fs.readdir(PATCH_FOLDER);
for (const patchFile of patches.filter(p => p.includes('gitlab'))) {
await Task.forItem('Patching', patchFile, async () => {
await patch(patchFile);
});
}
return answers;
};
@@ -1,83 +0,0 @@
/*
* Copyright 2023 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 chalk from 'chalk';
import inquirer from 'inquirer';
import { Task } from '../../lib/tasks';
import { auth } from './auth';
import { integrations } from './integrations';
import { discover } from './discovery';
export async function command(): Promise<void> {
const answers = await inquirer.prompt<{
shouldSetupAuth: boolean;
shouldSetupScaffolder: boolean;
shouldDiscoverEntities: boolean;
}>([
{
type: 'confirm',
name: 'shouldSetupAuth',
message: 'Do you want to set up Authentication for this project?',
default: true,
},
{
type: 'confirm',
name: 'shouldSetupScaffolder',
message: 'Do you want to use Software Templates in this project?',
default: true,
},
{
type: 'confirm',
name: 'shouldDiscoverEntities',
message:
'Do you want to discover entities and add them to the Software Catalog?',
default: true,
},
]);
const { shouldSetupAuth, shouldSetupScaffolder, shouldDiscoverEntities } =
answers;
if (!shouldSetupAuth && !shouldSetupScaffolder && !shouldDiscoverEntities) {
Task.log(
chalk.yellow(
'If you change your mind, feel free to re-run this command.',
),
);
return;
}
let providerInfo;
if (shouldSetupAuth) {
providerInfo = await auth();
}
if (shouldSetupScaffolder) {
await integrations(providerInfo);
}
if (shouldDiscoverEntities) {
await discover(providerInfo);
}
Task.log();
Task.log(
`You can now start your app with ${chalk.inverse(
chalk.italic('yarn dev'),
)}`,
);
Task.log();
}
@@ -1,45 +0,0 @@
/*
* Copyright 2023 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 * as fs from 'fs-extra';
import yaml from 'yaml';
const readYaml = async (file: string) => {
return yaml.parse(await fs.readFile(file, 'utf8'));
};
export const updateConfigFile = async <T>(file: string, config: T) => {
const staticContent =
'# Backstage override configuration for your local development environment \n';
const content = fs.existsSync(file)
? yaml.stringify(
{ ...(await readYaml(file)), ...config },
{
indent: 2,
},
)
: staticContent.concat(
yaml.stringify(
{ ...config },
{
indent: 2,
},
),
);
return await fs.writeFile(file, content, 'utf8');
};
@@ -1,71 +0,0 @@
/*
* Copyright 2023 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 chalk from 'chalk';
import { Entity } from '@backstage/catalog-model';
import { Analyzer } from './analyzers/types';
import { Provider } from './providers/types';
import { DefaultAnalysisOutputs } from './analyzers/DefaultAnalysisOutputs';
import { Task } from '../../../lib/tasks';
export class Discovery {
readonly #providers: Provider[] = [];
readonly #analyzers: Analyzer[] = [];
addProvider(provider: Provider) {
this.#providers.push(provider);
}
addAnalyzer(analyzer: Analyzer) {
this.#analyzers.push(analyzer);
}
async run(url: string): Promise<{ entities: Entity[] }> {
Task.log(`Running discovery for ${chalk.cyan(url)}`);
const result: Entity[] = [];
for (const provider of this.#providers) {
const repositories = await provider.discover(url);
if (repositories && repositories.length) {
Task.log(
`Discovered ${chalk.cyan(
repositories.length,
)} repositories for ${chalk.cyan(provider.name())}`,
);
for (const repository of repositories) {
await Task.forItem('Analyzing', repository.name, async () => {
const output = new DefaultAnalysisOutputs();
for (const analyzer of this.#analyzers) {
await analyzer.analyzeRepository({ repository, output });
}
output
.list()
.filter(entry => entry.type === 'entity')
.forEach(({ entity }) => result.push(entity));
});
}
Task.log(`Produced ${chalk.cyan(result.length || 'no')} entities`);
}
}
return {
entities: result,
};
}
}
@@ -1,56 +0,0 @@
/*
* Copyright 2023 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 { ComponentEntity } from '@backstage/catalog-model';
import { AnalysisOutputs, Analyzer } from './types';
import { Repository } from '../providers/types';
/**
* Naive analyzer that produces a single entity that represents the repository
* as a whole.
*/
export class BasicRepositoryAnalyzer implements Analyzer {
name(): string {
return BasicRepositoryAnalyzer.name;
}
async analyzeRepository(options: {
repository: Repository;
output: AnalysisOutputs;
}): Promise<void> {
const entity: ComponentEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: options.repository.name,
...(options.repository.description
? { description: options.repository.description }
: {}),
},
spec: {
type: 'service',
lifecycle: 'production',
owner: 'user:guest',
},
};
options.output.produce({
type: 'entity',
path: '/',
entity: entity,
});
}
}
@@ -1,29 +0,0 @@
/*
* Copyright 2023 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 { AnalysisOutput, AnalysisOutputs } from './types';
export class DefaultAnalysisOutputs implements AnalysisOutputs {
readonly #outputs = new Map<string, AnalysisOutput>();
produce(output: AnalysisOutput) {
this.#outputs.set(output.entity.metadata.name, output);
}
list() {
return Array.from(this.#outputs).map(([_, output]) => output);
}
}
@@ -1,128 +0,0 @@
/*
* Copyright 2023 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 {
ANNOTATION_SOURCE_LOCATION,
ComponentEntity,
} from '@backstage/catalog-model';
import z from 'zod';
import { AnalysisOutputs, Analyzer } from './types';
import { Repository, RepositoryFile } from '../providers/types';
export class PackageJsonAnalyzer implements Analyzer {
name(): string {
return PackageJsonAnalyzer.name;
}
async analyzeRepository(options: {
repository: Repository;
output: AnalysisOutputs;
}): Promise<void> {
const packageJson = await options.repository.file('package.json');
if (!packageJson) {
return;
}
const content = await readPackageJson(packageJson);
if (!content) {
return;
}
const name = sanitizeName(content?.name) ?? options.repository.name;
const entity: ComponentEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name,
...(options.repository.description
? { description: options.repository.description }
: {}),
tags: ['javascript'],
annotations: {
[ANNOTATION_SOURCE_LOCATION]: `url:${options.repository.url}`,
},
},
spec: {
type: 'website',
lifecycle: 'production',
owner: 'user:guest',
},
};
const decorate = options.output
.list()
.find(entry => entry.entity.metadata.name === name);
if (decorate) {
decorate.entity.spec = {
...decorate.entity.spec,
type: 'website',
};
decorate.entity.metadata.tags = [
...(decorate.entity.metadata.tags ?? []),
'javascript',
];
decorate.entity.metadata.annotations = {
...decorate.entity.metadata.annotations,
[ANNOTATION_SOURCE_LOCATION]: `url:${options.repository.url}`,
};
return;
}
options.output.produce({
type: 'entity',
path: '/',
entity,
});
}
}
const packageSchema = z.object({
name: z.string().optional(),
});
/**
* Makes sure that a name retrieved from a package.json file
* is reasonable and conforms to the catalog naming format.
*
* Read more about the naming format here:
* ADR002: Default Software Catalog File Format
* https://backstage.io/docs/architecture-decisions/adrs-adr002/
*/
function sanitizeName(name?: string) {
return name && name !== 'root'
? name.replace(/[^a-z0-9A-Z]/g, '_').substring(0, 62)
: undefined;
}
async function readPackageJson(
file: RepositoryFile,
): Promise<z.infer<typeof packageSchema> | undefined> {
try {
const text = await file.text();
const result = packageSchema.safeParse(JSON.parse(text));
if (!result.success) {
return undefined;
}
return { name: result.data.name };
} catch (e) {
return undefined;
}
}
@@ -1,37 +0,0 @@
/*
* Copyright 2023 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 { Entity } from '@backstage/catalog-model';
import { Repository } from '../providers/types';
export type AnalysisOutput = {
type: 'entity';
path: string;
entity: Entity;
};
export interface AnalysisOutputs {
produce(output: AnalysisOutput): void;
list(): AnalysisOutput[];
}
export interface Analyzer {
name(): string;
analyzeRepository(options: {
repository: Repository;
output: AnalysisOutputs;
}): Promise<void>;
}
@@ -1,141 +0,0 @@
/*
* Copyright 2023 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 fs from 'fs-extra';
import yaml from 'yaml';
import inquirer from 'inquirer';
import chalk from 'chalk';
import { loadCliConfig } from '../../../lib/config';
import { updateConfigFile } from '../config';
import { APP_CONFIG_FILE, DISCOVERED_ENTITIES_FILE } from '../files';
import { Discovery } from './Discovery';
import { BasicRepositoryAnalyzer } from './analyzers/BasicRepositoryAnalyzer';
import { PackageJsonAnalyzer } from './analyzers/PackageJsonAnalyzer';
import { GithubDiscoveryProvider } from './providers/github/GithubDiscoveryProvider';
import { GitlabDiscoveryProvider } from './providers/gitlab/GitlabDiscoveryProvider';
import { GitHubAnswers, GitLabAnswers } from '../auth';
import { Task } from '../../../lib/tasks';
export async function discover(providerInfo?: {
provider: string;
answers: GitHubAnswers | GitLabAnswers;
}) {
Task.log(`
Would you like to scan for - and create - Software Catalog entities?
You will need to select which SCM (Source Code Management) provider you are using,
and then which repository or organization you want to scan.
This will generate a new file in the root of your app containing discovered entities,
which will be included in the Software Catalog when you start up Backstage next time.
Note that this command requires an access token, which can be either added through the integration config or
provided as an environment variable.
`);
const answers = await inquirer.prompt<{
shouldContinue: boolean;
provider: string;
url: string;
}>([
{
type: 'confirm',
name: 'shouldContinue',
message: 'Do you want to continue?',
},
{
type: 'list',
name: 'provider',
message: 'Please select which SCM provider you want to use:',
choices: ['GitHub', 'GitLab'],
default: providerInfo?.provider,
when: ({ shouldContinue }) => shouldContinue,
},
{
type: 'input',
name: 'url',
message: `Which repository do you want to scan?`,
when: ({ shouldContinue }) => shouldContinue,
filter: (input, { provider }) => {
if (provider === 'GitLab') {
return `https://gitlab.com/${input}`;
}
if (provider === 'GitHub') {
return `https://github.com/${input}`;
}
return false;
},
},
]);
if (!answers.shouldContinue) {
Task.log(
chalk.yellow(
'If you change your mind, feel free to re-run this command.',
),
);
return;
}
const { fullConfig: config } = await loadCliConfig({ args: [] });
const discovery = new Discovery();
if (answers.provider === 'GitHub') {
discovery.addProvider(GithubDiscoveryProvider.fromConfig(config));
}
if (answers.provider === 'GitLab') {
discovery.addProvider(GitlabDiscoveryProvider.fromConfig(config));
}
discovery.addAnalyzer(new BasicRepositoryAnalyzer());
discovery.addAnalyzer(new PackageJsonAnalyzer());
const { entities } = await discovery.run(answers.url);
if (!entities.length) {
Task.log(
chalk.yellow(`
We could not find enough information to be able to generate any Software Catalog entities for you.
Perhaps you can try again with a different repository?`),
);
return;
}
await Task.forItem('Creating', DISCOVERED_ENTITIES_FILE, async () => {
const payload: string[] = [];
for (const entity of entities) {
payload.push('---\n', yaml.stringify(entity));
}
await fs.writeFile(DISCOVERED_ENTITIES_FILE, payload.join(''));
});
await Task.forItem(
'Updating',
APP_CONFIG_FILE,
async () =>
await updateConfigFile(APP_CONFIG_FILE, {
catalog: {
locations: [
{
type: 'file',
target: DISCOVERED_ENTITIES_FILE,
},
],
},
}),
);
}
@@ -1,163 +0,0 @@
/*
* Copyright 2023 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 { Config } from '@backstage/config';
import {
DefaultGithubCredentialsProvider,
GithubCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import { graphql } from '@octokit/graphql';
import {
Repository as GraphqlRepository,
Query as GraphqlQuery,
} from '@octokit/graphql-schema';
import parseGitUrl from 'git-url-parse';
import { Provider, Repository } from '../types';
import { GithubRepository } from './GithubRepository';
export class GithubDiscoveryProvider implements Provider {
readonly #envToken: string | undefined;
readonly #scmIntegrations: ScmIntegrations;
readonly #credentialsProvider: GithubCredentialsProvider;
static fromConfig(config: Config): GithubDiscoveryProvider {
const envToken = process.env.GITHUB_TOKEN || undefined;
const scmIntegrations = ScmIntegrations.fromConfig(config);
const credentialsProvider =
DefaultGithubCredentialsProvider.fromIntegrations(scmIntegrations);
return new GithubDiscoveryProvider(
envToken,
scmIntegrations,
credentialsProvider,
);
}
private constructor(
envToken: string | undefined,
integrations: ScmIntegrations,
credentialsProvider: GithubCredentialsProvider,
) {
this.#envToken = envToken;
this.#scmIntegrations = integrations;
this.#credentialsProvider = credentialsProvider;
}
name(): string {
return 'GitHub';
}
async discover(url: string): Promise<Repository[] | false> {
if (!url.startsWith('https://github.com/')) {
return false;
}
const scmIntegration = this.#scmIntegrations.github.byUrl(url);
if (!scmIntegration) {
throw new Error(`No GitHub integration found for ${url}`);
}
const parsed = parseGitUrl(url);
const { name, organization } = parsed;
const org = organization || name; // depends on if it's a repo url or an org url...
const client = graphql.defaults({
baseUrl: scmIntegration.config.apiBaseUrl,
headers: await this.#getRequestHeaders(url),
});
const { repositories } = await this.#getOrganizationRepositories(
client,
org,
);
return repositories
.filter(repo => repo.url.startsWith(url))
.map(repo => new GithubRepository(client, repo, org));
}
async #getRequestHeaders(url: string): Promise<Record<string, string>> {
const credentials = await this.#credentialsProvider.getCredentials({
url,
});
if (credentials.headers) {
return credentials.headers;
} else if (credentials.token) {
return { authorization: `token ${credentials.token}` };
}
if (this.#envToken) {
return { authorization: `token ${this.#envToken}` };
}
throw new Error(
'No token available for GitHub, please configure your integrations or set a GITHUB_TOKEN env variable',
);
}
async #getOrganizationRepositories(client: typeof graphql, org: string) {
const query = `query repositories($org: String!, $cursor: String) {
repositoryOwner(login: $org) {
login
repositories(first: 50, after: $cursor) {
nodes {
name
url
description
isArchived
isFork
}
pageInfo {
hasNextPage
endCursor
}
}
}
}`;
const result: GraphqlRepository[] = [];
let cursor: string | undefined | null = undefined;
let hasNextPage = true;
while (hasNextPage) {
const response: GraphqlQuery = await client(query, {
org,
cursor,
});
const { repositories: connection } = response.repositoryOwner ?? {};
if (!connection) {
throw new Error(`Found no repositories for ${org}`);
}
for (const repository of connection.nodes ?? []) {
if (repository && !repository.isArchived && !repository.isFork) {
result.push(repository);
}
}
cursor = connection.pageInfo.endCursor;
hasNextPage = connection.pageInfo.hasNextPage;
}
return {
repositories: result,
};
}
}
@@ -1,35 +0,0 @@
/*
* Copyright 2023 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 { RepositoryFile } from '../types';
export class GithubFile implements RepositoryFile {
readonly #path: string;
readonly #content: string;
constructor(path: string, content: string) {
this.#path = path;
this.#content = content;
}
get path(): string {
return this.#path;
}
async text(): Promise<string> {
return this.#content;
}
}
@@ -1,84 +0,0 @@
/*
* Copyright 2023 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 { graphql } from '@octokit/graphql';
import {
Repository as GraphqlRepository,
Blob as GraphqlBlob,
} from '@octokit/graphql-schema';
import { Repository, RepositoryFile } from '../types';
import { GithubFile } from './GithubFile';
export class GithubRepository implements Repository {
readonly #client: typeof graphql;
readonly #repo: GraphqlRepository;
readonly #org: string;
constructor(client: typeof graphql, repo: GraphqlRepository, org: string) {
this.#client = client;
this.#repo = repo;
this.#org = org;
}
get url(): string {
return this.#repo.url;
}
get name(): string {
return this.#repo.name;
}
get owner(): string {
return this.#org;
}
get description(): string | undefined {
return this.#repo.description ?? undefined;
}
async file(filename: string): Promise<RepositoryFile | undefined> {
const content = await this.#getFileContent(filename);
if (!content || content.isBinary || !content.text) {
return undefined;
}
return new GithubFile(filename, content.text ?? '');
}
async #getFileContent(filename: string) {
const query = `query RepoFiles($owner: String!, $name: String!, $expr: String!) {
repository(owner: $owner, name: $name) {
object(expression: $expr) {
...on Blob {
text
isBinary
}
}
}
}`;
const response = await this.#client<{ repository: GraphqlRepository }>(
query,
{
name: this.#repo.name,
owner: this.#org,
expr: `HEAD:${filename}`,
},
);
return response.repository.object as GraphqlBlob;
}
}
@@ -1,106 +0,0 @@
/*
* Copyright 2023 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 { Config } from '@backstage/config';
import {
DefaultGitlabCredentialsProvider,
GitlabCredentialsProvider,
ScmIntegrations,
} from '@backstage/integration';
import fetch from 'node-fetch';
import { Provider } from '../types';
import { GitlabProject, ProjectResponse } from './GitlabProject';
export class GitlabDiscoveryProvider implements Provider {
readonly #envToken: string | undefined;
readonly #scmIntegrations: ScmIntegrations;
readonly #credentialsProvider: GitlabCredentialsProvider;
static fromConfig(config: Config): GitlabDiscoveryProvider {
const envToken = process.env.GITLAB_TOKEN || undefined;
const scmIntegrations = ScmIntegrations.fromConfig(config);
const credentialsProvider =
DefaultGitlabCredentialsProvider.fromIntegrations(scmIntegrations);
return new GitlabDiscoveryProvider(
envToken,
scmIntegrations,
credentialsProvider,
);
}
private constructor(
envToken: string | undefined,
integrations: ScmIntegrations,
credentialsProvider: GitlabCredentialsProvider,
) {
this.#envToken = envToken;
this.#scmIntegrations = integrations;
this.#credentialsProvider = credentialsProvider;
}
name(): string {
return 'GitLab';
}
async discover(url: string): Promise<false | GitlabProject[]> {
const { origin, pathname } = new URL(url);
const [, user] = pathname.split('/');
const scmIntegration = this.#scmIntegrations.gitlab.byUrl(origin);
if (!scmIntegration) {
throw new Error(`No GitLab integration found for ${origin}`);
}
const headers = await this.#getRequestHeaders(origin);
const response = await fetch(
`${scmIntegration.config.apiBaseUrl}/users/${user}/projects`,
{ headers },
);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
const projects: ProjectResponse[] = await response.json();
return projects.map(
project =>
new GitlabProject(project, scmIntegration.config.apiBaseUrl, headers),
);
}
async #getRequestHeaders(url: string): Promise<Record<string, string>> {
const credentials = await this.#credentialsProvider.getCredentials({
url,
});
if (credentials.headers) {
return credentials.headers;
} else if (credentials.token) {
return { authorization: `Bearer ${credentials.token}` };
}
if (this.#envToken) {
return { authorization: `Bearer ${this.#envToken}` };
}
throw new Error(
'No token available for GitLab, please set a GITLAB_TOKEN env variable',
);
}
}
@@ -1,38 +0,0 @@
/*
* Copyright 2023 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 { RepositoryFile } from '../types';
/**
* A single file in a GitLab repository.
*/
export class GitlabFile implements RepositoryFile {
readonly #path: string;
readonly #content: string;
constructor(path: string, content: string) {
this.#path = path;
this.#content = content;
}
get path(): string {
return this.#path;
}
async text(): Promise<string> {
return this.#content;
}
}
@@ -1,89 +0,0 @@
/*
* Copyright 2023 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 fetch from 'node-fetch';
import { GitlabFile } from './GitlabFile';
import { Repository, RepositoryFile } from '../types';
export type ProjectResponse = {
id: string;
name: string;
description: string;
owner: {
username: string;
};
web_url: string;
};
type BranchResponse = {
default: boolean;
name: string;
};
type FileContentResponse = {
content: string;
};
export class GitlabProject implements Repository {
constructor(
private readonly project: ProjectResponse,
private readonly apiBaseUrl: string,
private readonly headers: { [name: string]: string },
) {}
get url(): string {
return this.project.web_url;
}
get name(): string {
return this.project.name;
}
get owner(): string {
return this.project.owner.username;
}
get description(): string {
return this.project.description;
}
async file(filename: string): Promise<RepositoryFile | undefined> {
const mainBranch = await this.#getMainBranch();
const content = await this.#getFileContent(filename, mainBranch);
return new GitlabFile(filename, content);
}
async #getFileContent(path: string, mainBranch: string): Promise<string> {
const response = await fetch(
`${this.apiBaseUrl}/projects/${this.project.id}/repository/files/${path}?ref=${mainBranch}`,
{ headers: this.headers },
);
const { content }: FileContentResponse = await response.json();
return Buffer.from(content, 'base64').toString('ascii');
}
async #getMainBranch(): Promise<string> {
const response = await fetch(
`${this.apiBaseUrl}/projects/${this.project.id}/repository/branches`,
{ headers: this.headers },
);
const branches: BranchResponse[] = await response.json();
return branches.find(branch => branch.default)?.name ?? 'main';
}
}
@@ -1,53 +0,0 @@
/*
* Copyright 2023 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.
*/
/**
* Abstraction for a single repository.
*/
export interface Repository {
url: string;
name: string;
owner: string;
description?: string;
file(filename: string): Promise<RepositoryFile | undefined>;
}
/**
* Abstraction for a single repository file.
*/
export interface RepositoryFile {
/**
* The filepath of the data.
*/
path: string;
/**
* The textual contents of the file.
*/
text(): Promise<string>;
}
/**
* One integration that supports discovery of repositories.
*/
export interface Provider {
name(): string;
discover(url: string): Promise<Repository[] | false>;
}
@@ -1,36 +0,0 @@
/*
* Copyright 2023 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 { findPaths } from '@backstage/cli-common';
import * as path from 'path';
/* eslint-disable-next-line no-restricted-syntax */
const { targetRoot, ownDir } = findPaths(__dirname);
export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml');
export const DISCOVERED_ENTITIES_FILE = path.join(
targetRoot,
'examples',
'discovered-entities.yaml',
);
export const PATCH_FOLDER = path.join(
ownDir,
'src',
'commands',
'onboard',
'auth',
'patches',
);
@@ -1,17 +0,0 @@
/*
* Copyright 2023 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.
*/
export { command } from './command';
@@ -1,136 +0,0 @@
/*
* Copyright 2023 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 chalk from 'chalk';
import inquirer from 'inquirer';
import { Task } from '../../../lib/tasks';
import { updateConfigFile } from '../config';
import { APP_CONFIG_FILE } from '../files';
import { GitHubAnswers } from '../auth';
type Answers = {
hasEnterprise: boolean;
enterpriseInstanceUrl: string;
apiBaseUrl: string;
};
const getConfig = ({
hasEnterprise,
apiBaseUrl,
host,
token,
}: {
hasEnterprise: boolean;
apiBaseUrl: string;
host: string;
token: string;
}) => ({
integrations: {
github: [
{
host,
token,
...(hasEnterprise && {
apiBaseUrl,
}),
},
],
},
});
export const github = async (providerAnswers?: GitHubAnswers) => {
// TODO(tudi2d): Is GitHub Enterprise a valid setup if there is no Authentication?
const answers = await inquirer.prompt<Answers>([
{
type: 'confirm',
name: 'hasEnterprise',
message: 'Are you using GitHub Enterprise?',
when: () => typeof providerAnswers === 'undefined',
},
{
type: 'input',
name: 'enterpriseInstanceUrl',
message: 'What is your URL for GitHub Enterprise?',
when: ({ hasEnterprise }) => hasEnterprise,
validate: (input: string) => Boolean(new URL(input)),
},
{
type: 'input',
name: 'apiBaseUrl',
message: 'What is your GitHub Enterprise API path?',
default: '/api/v3',
when: ({ hasEnterprise }) =>
hasEnterprise || providerAnswers?.hasEnterprise,
// TODO(tudi2d): Fetch API using OAuth Token if Auth was set up
},
]);
const host = new URL(
providerAnswers?.enterpriseInstanceUrl ??
answers?.enterpriseInstanceUrl ??
'http://github.com',
);
Task.log(`
To create new repositories in GitHub using Software Templates you first need to create a personal access token: ${chalk.blue(
`${host.origin}/settings/tokens/new`,
)}
Select the following scopes:
Reading software components:${chalk.cyan(`
- "repo"`)}
Reading organization data:${chalk.cyan(`
- "read:org"
- "read:user"
- "user:email"`)}
Publishing software templates:${chalk.cyan(`
- "repo"
- "workflow" (if templates include GitHub workflows)
`)}
You can find the full documentation page here: ${chalk.blue(
'https://backstage.io/docs/integrations/github/locations',
)}
`);
const { token } = await inquirer.prompt<{ token: string }>([
{
type: 'input',
name: 'token',
message:
'Please insert your personal access token to setup the GitHub Integration',
// TODO(tudi2d): validate
},
]);
const config = getConfig({
hasEnterprise: providerAnswers?.hasEnterprise ?? answers.hasEnterprise,
apiBaseUrl: host.origin + answers.apiBaseUrl,
host: host.hostname,
token,
});
Task.log('Setting up Software Templates using GitHub integration for you...');
await Task.forItem(
'Updating',
APP_CONFIG_FILE,
async () => await updateConfigFile(APP_CONFIG_FILE, config),
);
};
@@ -1,75 +0,0 @@
/*
* Copyright 2023 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 inquirer from 'inquirer';
import { Task } from '../../../lib/tasks';
import { GitHubAnswers, GitLabAnswers } from '../auth';
import { github } from './github';
enum Integration {
GITHUB = 'GitHub',
}
const Integrations: Integration[] = [Integration.GITHUB];
export async function integrations(providerInfo?: {
provider: string;
answers: GitHubAnswers | GitLabAnswers;
}): Promise<void> {
const answers = await inquirer.prompt<{
integration?: Integration;
shouldUsePreviousProvider: boolean;
}>([
{
type: 'confirm',
name: 'shouldUsePreviousProvider',
message: `Do you want to keep using ${providerInfo?.provider} as your provider when setting up Software Templates?`,
when: () =>
providerInfo?.provider &&
Object.values(Integrations).includes(
providerInfo!.provider as Integration,
),
},
{
// TODO(tudi2d): Let's start with one, but it should be multiple choice in the future
type: 'list',
name: 'integration',
message: 'Please select an integration provider:',
choices: Integrations,
when: ({ shouldUsePreviousProvider }) => !shouldUsePreviousProvider,
},
]);
if (answers.shouldUsePreviousProvider) {
answers.integration = providerInfo!.provider as Integration;
}
switch (answers.integration) {
case Integration.GITHUB: {
const providerAnswers =
providerInfo?.provider === 'GitHub'
? (providerInfo!.answers as GitHubAnswers)
: undefined;
await github(providerAnswers);
break;
}
default:
}
Task.log();
Task.log(`Done setting up ${answers.integration} Integration!`);
Task.log();
}