Merge pull request #16526 from backstage/goa/cli-admin-command
cli: Add `onboard` command
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/cli': patch
|
||||
---
|
||||
|
||||
Add `onboard` command. While still in development, this command aims to guide users in setting up their Backstage App.
|
||||
@@ -13,6 +13,7 @@ yarn.lock @backstage/maintainers @back
|
||||
/docs/features/search @backstage/techdocs-core
|
||||
/docs/features/techdocs @backstage/techdocs-core
|
||||
/docs/plugins/integrating-search-into-plugins.md @backstage/techdocs-core
|
||||
/packages/cli/src/commands/onboard @backstage/sharks
|
||||
/packages/techdocs-cli @backstage/techdocs-core
|
||||
/packages/techdocs-cli-embedded-app @backstage/techdocs-core
|
||||
/plugins/adr @backstage/maintainers @kuangp
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@backstage/types": "workspace:^",
|
||||
"@esbuild-kit/cjs-loader": "^2.4.1",
|
||||
"@manypkg/get-packages": "^1.1.3",
|
||||
"@octokit/oauth-app": "^4.2.0",
|
||||
"@octokit/request": "^6.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
||||
"@rollup/plugin-commonjs": "^23.0.0",
|
||||
|
||||
@@ -25,6 +25,13 @@ 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]')
|
||||
@@ -360,6 +367,7 @@ export function registerCommands(program: Command) {
|
||||
registerRepoCommand(program);
|
||||
registerScriptCommand(program);
|
||||
registerMigrateCommand(program);
|
||||
registerOnboardCommand(program);
|
||||
|
||||
program
|
||||
.command('versions:bump')
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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
|
||||
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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');
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
--- 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,
|
||||
+ }}
|
||||
+ />
|
||||
+ ),
|
||||
+ },
|
||||
@@ -0,0 +1,25 @@
|
||||
--- 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,
|
||||
+ }}
|
||||
+ />
|
||||
+ ),
|
||||
+ },
|
||||
@@ -0,0 +1,5 @@
|
||||
--- 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({
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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;
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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;
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
export async function command(): Promise<void> {
|
||||
const answers = await inquirer.prompt<{
|
||||
shouldSetupAuth: boolean;
|
||||
shouldSetupScaffolder: 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,
|
||||
},
|
||||
]);
|
||||
|
||||
const { shouldSetupAuth, shouldSetupScaffolder } = answers;
|
||||
|
||||
let providerInfo;
|
||||
if (shouldSetupAuth) {
|
||||
providerInfo = await auth();
|
||||
}
|
||||
|
||||
if (shouldSetupScaffolder) {
|
||||
await integrations(providerInfo);
|
||||
}
|
||||
|
||||
if (!shouldSetupAuth && !shouldSetupScaffolder) {
|
||||
Task.log(
|
||||
chalk.yellow(
|
||||
'If you change your mind, feel free to re-run this command.',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Task.log();
|
||||
Task.log(
|
||||
`You can now start your app with ${chalk.inverse(
|
||||
chalk.italic('yarn dev'),
|
||||
)}`,
|
||||
);
|
||||
Task.log();
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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');
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 PATCH_FOLDER = path.join(
|
||||
ownDir,
|
||||
'src',
|
||||
'commands',
|
||||
'onboard',
|
||||
'auth',
|
||||
'patches',
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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';
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 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),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -3652,6 +3652,7 @@ __metadata:
|
||||
"@backstage/types": "workspace:^"
|
||||
"@esbuild-kit/cjs-loader": ^2.4.1
|
||||
"@manypkg/get-packages": ^1.1.3
|
||||
"@octokit/oauth-app": ^4.2.0
|
||||
"@octokit/request": ^6.0.0
|
||||
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.7
|
||||
"@rollup/plugin-commonjs": ^23.0.0
|
||||
@@ -12271,9 +12272,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/oauth-app@npm:^4.0.4, @octokit/oauth-app@npm:^4.0.6":
|
||||
version: 4.0.6
|
||||
resolution: "@octokit/oauth-app@npm:4.0.6"
|
||||
"@octokit/oauth-app@npm:^4.0.4, @octokit/oauth-app@npm:^4.0.6, @octokit/oauth-app@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "@octokit/oauth-app@npm:4.2.0"
|
||||
dependencies:
|
||||
"@octokit/auth-oauth-app": ^5.0.0
|
||||
"@octokit/auth-oauth-user": ^2.0.0
|
||||
@@ -12282,13 +12283,9 @@ __metadata:
|
||||
"@octokit/oauth-authorization-url": ^5.0.0
|
||||
"@octokit/oauth-methods": ^2.0.0
|
||||
"@types/aws-lambda": ^8.10.83
|
||||
aws-lambda: ^1.0.7
|
||||
fromentries: ^1.3.1
|
||||
universal-user-agent: ^6.0.0
|
||||
dependenciesMeta:
|
||||
aws-lambda:
|
||||
optional: true
|
||||
checksum: 0d7ceadda4668748a436f52f6c4ae3160a4a5c81a92592c0a6cfc75b0a065ed480880d8861efcabc6ca41b3ccf753d1c1044782a6c8d5fb481d6e8a0acf6d32e
|
||||
checksum: 6cf5638d27e589daa577653177f2b135c0c346c8f74cc420d9fd0dc0f559a84040073eee147bd555a55bb5756bd0940e6dc4c3a6b3a508a818e3d5bee3837095
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -17274,20 +17271,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aws-lambda@npm:^1.0.7":
|
||||
version: 1.0.7
|
||||
resolution: "aws-lambda@npm:1.0.7"
|
||||
dependencies:
|
||||
aws-sdk: ^2.814.0
|
||||
commander: ^3.0.2
|
||||
js-yaml: ^3.14.1
|
||||
watchpack: ^2.0.0-beta.10
|
||||
bin:
|
||||
lambda: bin/lambda
|
||||
checksum: 11316e87b5c4fc36e6bd0495742a3c0ed13befc9527a7b251a58180d141d9afd68b684f37aeb3b53d117d3c2f96747eace31826b683543f1edddc03f392865fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aws-os-connection@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "aws-os-connection@npm:0.2.0"
|
||||
@@ -17334,7 +17317,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aws-sdk@npm:^2.1231.0, aws-sdk@npm:^2.814.0, aws-sdk@npm:^2.840.0, aws-sdk@npm:^2.948.0":
|
||||
"aws-sdk@npm:^2.1231.0, aws-sdk@npm:^2.840.0, aws-sdk@npm:^2.948.0":
|
||||
version: 2.1279.0
|
||||
resolution: "aws-sdk@npm:2.1279.0"
|
||||
dependencies:
|
||||
@@ -19220,13 +19203,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "commander@npm:3.0.2"
|
||||
checksum: 6d14ad030d1904428139487ed31febcb04c1604db2b8d9fae711f60ee6718828dc0e11602249e91c8a97b0e721e9c6d53edbc166bad3cde1596851d59a8f824d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"commander@npm:^4.0.0":
|
||||
version: 4.1.1
|
||||
resolution: "commander@npm:4.1.1"
|
||||
@@ -27072,7 +27048,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1, js-yaml@npm:^3.6.1, js-yaml@npm:^3.8.3":
|
||||
"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.6.1, js-yaml@npm:^3.8.3":
|
||||
version: 3.14.1
|
||||
resolution: "js-yaml@npm:3.14.1"
|
||||
dependencies:
|
||||
@@ -38488,7 +38464,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"watchpack@npm:^2.0.0-beta.10, watchpack@npm:^2.4.0":
|
||||
"watchpack@npm:^2.4.0":
|
||||
version: 2.4.0
|
||||
resolution: "watchpack@npm:2.4.0"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user