Setup basic GitHub & GHE integration using CLI
Signed-off-by: Philipp Hugenroth <philipph@spotify.com>
This commit is contained in:
@@ -20,10 +20,30 @@ import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import fetch from 'node-fetch';
|
||||
import { Task } from '../../../../lib/tasks';
|
||||
import { addUserEntity, updateConfigFile } from '../config';
|
||||
import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files';
|
||||
import { addUserEntity, updateConfigFile } from '../../config';
|
||||
import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../../files';
|
||||
import { patch } from '../patch';
|
||||
|
||||
type Answers = {
|
||||
username: string;
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
hasEnterprise: boolean;
|
||||
enterpriseInstanceUrl?: string;
|
||||
};
|
||||
|
||||
const catalogUserLocation = {
|
||||
catalog: {
|
||||
locations: [
|
||||
{
|
||||
type: 'file',
|
||||
target: '../../user-info.yaml',
|
||||
rules: [{ allow: ['User'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const validateCredentials = async (clientId: string, clientSecret: string) => {
|
||||
try {
|
||||
const app = new OAuthApp({
|
||||
@@ -70,14 +90,6 @@ const getConfig = (answers: Answers) => {
|
||||
};
|
||||
};
|
||||
|
||||
type Answers = {
|
||||
username: string;
|
||||
clientSecret: string;
|
||||
clientId: string;
|
||||
hasEnterprise: boolean;
|
||||
enterpriseInstanceUrl?: string;
|
||||
};
|
||||
|
||||
export const github = async () => {
|
||||
Task.log(`
|
||||
To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue(
|
||||
@@ -147,7 +159,11 @@ export const github = async () => {
|
||||
await Task.forItem(
|
||||
'Updating',
|
||||
APP_CONFIG_FILE,
|
||||
async () => await updateConfigFile(APP_CONFIG_FILE, config),
|
||||
async () =>
|
||||
await updateConfigFile(APP_CONFIG_FILE, {
|
||||
...config,
|
||||
...catalogUserLocation,
|
||||
}),
|
||||
);
|
||||
await Task.forItem(
|
||||
'Creating',
|
||||
|
||||
@@ -18,8 +18,8 @@ import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
import inquirer from 'inquirer';
|
||||
import { Task } from '../../../../lib/tasks';
|
||||
import { addUserEntity, updateConfigFile } from '../config';
|
||||
import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../files';
|
||||
import { addUserEntity, updateConfigFile } from '../../config';
|
||||
import { APP_CONFIG_FILE, PATCH_FOLDER, USER_ENTITY_FILE } from '../../files';
|
||||
import { patch } from '../patch';
|
||||
|
||||
const getConfig = (answers: Answers) => {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as differ from 'diff';
|
||||
import { PATCH_FOLDER } from './files';
|
||||
import { PATCH_FOLDER } from '../files';
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
|
||||
/* eslint-disable-next-line no-restricted-syntax */
|
||||
|
||||
+3
-33
@@ -18,44 +18,14 @@ import { UserEntity } from '@backstage/catalog-model';
|
||||
import * as fs from 'fs-extra';
|
||||
import yaml from 'yaml';
|
||||
|
||||
type AuthConfig = {
|
||||
auth: {
|
||||
providers: {
|
||||
[key: string]: {
|
||||
development: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
enterpriseInstanceUrl?: string;
|
||||
audience?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const catalogUserLocation = {
|
||||
catalog: {
|
||||
locations: [
|
||||
{
|
||||
type: 'file',
|
||||
target: '../../user-info.yaml',
|
||||
rules: [{ allow: ['User'] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const readYaml = async (file: string) => {
|
||||
return yaml.parse(await fs.readFile(file, 'utf8'));
|
||||
};
|
||||
|
||||
export const updateConfigFile = async (
|
||||
file: string,
|
||||
authConfig: AuthConfig,
|
||||
) => {
|
||||
export const updateConfigFile = async <T>(file: string, config: T) => {
|
||||
const content = fs.existsSync(file)
|
||||
? { ...(await readYaml(file)), ...authConfig, ...catalogUserLocation }
|
||||
: { ...authConfig, ...catalogUserLocation };
|
||||
? { ...(await readYaml(file)), ...config }
|
||||
: { ...config };
|
||||
|
||||
return await fs.writeFile(
|
||||
file,
|
||||
@@ -17,37 +17,82 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { Task } from '../../../../lib/tasks';
|
||||
import { updateConfigFile } from '../../config';
|
||||
import { APP_CONFIG_FILE } from '../../files';
|
||||
|
||||
type Answers = {
|
||||
isEnterprise: boolean;
|
||||
host: string;
|
||||
apiBaseUrl?: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
const getConfig = ({ isEnterprise, host, apiBaseUrl, token }: Answers) => ({
|
||||
integrations: {
|
||||
github: isEnterprise
|
||||
? [
|
||||
{
|
||||
host,
|
||||
apiBaseUrl,
|
||||
token,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
host,
|
||||
token,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const github = async () => {
|
||||
const host = 'https://github.com';
|
||||
const answers = await inquirer.prompt<{ hasEnterprise: boolean }>([
|
||||
let host = 'github.com';
|
||||
let apiBaseUrl: string | undefined;
|
||||
|
||||
// TODO(tudi2d): Is GitHub Enterprise a valid setup if there is no Authentication?
|
||||
const { isEnterprise } = await inquirer.prompt<{
|
||||
isEnterprise: Answers['isEnterprise'];
|
||||
}>([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'hasEnterprise',
|
||||
name: 'isEnterprise',
|
||||
message: 'Are you using GitHub Enterprise?',
|
||||
},
|
||||
]);
|
||||
|
||||
if (answers.hasEnterprise) {
|
||||
await inquirer.prompt<{ hasEnterprise: boolean }>([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'hasEnterprise',
|
||||
message: 'Are you using GitHub Enterprise?',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'enterpriseInstanceUrl',
|
||||
message: 'What is your GitHub Enterprise URL (e.g. ghe.example.net)?',
|
||||
when: ({ hasEnterprise }) => hasEnterprise,
|
||||
validate: (input: string) => Boolean(new URL(input)),
|
||||
},
|
||||
]);
|
||||
if (isEnterprise) {
|
||||
try {
|
||||
const answers = await inquirer.prompt<
|
||||
Pick<Answers, 'host' | 'apiBaseUrl'>
|
||||
>([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'apiBaseUrl',
|
||||
message:
|
||||
'What is your GitHub Enterprise REST API URL (e.g. https://ghe.example.net/api/v3)?',
|
||||
// TODO(tudi2d): Fetch API using OAuth Token if Auth was set up
|
||||
validate: (input: string) => Boolean(new URL(input)),
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'host',
|
||||
message:
|
||||
'What is your GitHub Enterprise Host (e.g. ghe.example.net)?',
|
||||
// TODO(tudi2d): validate: Must the host be part of the REST API URL?
|
||||
},
|
||||
]);
|
||||
|
||||
host = answers.host;
|
||||
apiBaseUrl = answers.apiBaseUrl;
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Task.log(`
|
||||
To create new repositories in GitHub using Software Templates you first need to create a personal access token: ${chalk.blue(
|
||||
`${host}/settings/tokens/new',
|
||||
`https://${host}/settings/tokens/new`,
|
||||
)}
|
||||
|
||||
Select the following scopes:
|
||||
@@ -68,6 +113,32 @@ export const github = async () => {
|
||||
You can find the full documentation page here: ${chalk.blue(
|
||||
'https://backstage.io/docs/integrations/github/locations',
|
||||
)}
|
||||
`,
|
||||
)}`);
|
||||
`);
|
||||
|
||||
const { token } = await inquirer.prompt<{
|
||||
token: Answers['token'];
|
||||
}>([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'token',
|
||||
message:
|
||||
'Please insert your personal access token to setup the GitHub Integration',
|
||||
// TODO(tudi2d): validate: Must the host be part of the REST API URL?
|
||||
},
|
||||
]);
|
||||
|
||||
const config = getConfig({
|
||||
token,
|
||||
isEnterprise,
|
||||
host,
|
||||
apiBaseUrl,
|
||||
});
|
||||
|
||||
Task.log('Setting up GitHub Integration for you...');
|
||||
|
||||
await Task.forItem(
|
||||
'Updating',
|
||||
APP_CONFIG_FILE,
|
||||
async () => await updateConfigFile(APP_CONFIG_FILE, config),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function integrations(): Promise<void> {
|
||||
integration?: Integration;
|
||||
}>([
|
||||
{
|
||||
// TODO(tudi2d): Should be multiple choice
|
||||
// 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:',
|
||||
|
||||
Reference in New Issue
Block a user