diff --git a/packages/cli/src/commands/admin/command.ts b/packages/cli/src/commands/admin/command.ts index 5e8256c14f..d7ea348571 100644 --- a/packages/cli/src/commands/admin/command.ts +++ b/packages/cli/src/commands/admin/command.ts @@ -19,6 +19,7 @@ import inquirer from 'inquirer'; import { Task } from '../../lib/tasks'; import { github } from './github'; import { updateConfigFile, updateEnvFile } from './file'; +import ghAuth from './gh-auth'; export async function command(): Promise { const answers = await inquirer.prompt<{ @@ -44,7 +45,7 @@ export async function command(): Promise { type: 'list', name: 'provider', message: 'Please select a provider:', - choices: ['github'], + choices: ['GitHub OAuth', 'GitHub App'], when: ({ shouldSetupAuth }) => shouldSetupAuth, }, ]); @@ -59,9 +60,10 @@ export async function command(): Promise { process.exit(1); } + const { useEnvForSecrets } = answers; + switch (answers.provider) { - case 'github': { - const { useEnvForSecrets } = answers; + case 'GitHub OAuth': { const config = await github(useEnvForSecrets); await updateConfigFile(config); if (useEnvForSecrets) { @@ -69,6 +71,24 @@ export async function command(): Promise { } break; } + case 'GitHub App': { + const config = await ghAuth(); + // TODO(tudi2d): Also change integrations + if (useEnvForSecrets) { + await updateEnvFile({ + auth: config.auth, + }); + config.auth.providers.github.clientId = '${AUTH_GITHUB_CLIENT_ID}'; + config.auth.providers.github.clientSecret = + '${AUTH_GITHUB_CLIENT_SECRET}'; + await updateConfigFile({ + auth: config.auth, + }); + } else { + await updateConfigFile({ auth: config.auth }); + } + break; + } default: throw new Error(`Provider ${answers.provider} not implemented yet.`); } diff --git a/packages/cli/src/commands/admin/gh-auth.ts b/packages/cli/src/commands/admin/gh-auth.ts index da3036d087..ee5bbfb2c6 100644 --- a/packages/cli/src/commands/admin/gh-auth.ts +++ b/packages/cli/src/commands/admin/gh-auth.ts @@ -15,10 +15,11 @@ */ import chalk from 'chalk'; import inquirer from 'inquirer'; -import createGithubApp from '../create-github-app'; +import { adminCli } from '../create-github-app'; +// TODO(tudi2d): Wrapper for admin CLI around `create-github-app` - potentially to be removed export default async () => { - // TODO: Make the GitHub Org optional + // TODO(tudi2d): Make the GitHub Org optional const input = await inquirer.prompt<{ org: string }>([ { type: 'input', @@ -26,5 +27,5 @@ export default async () => { message: chalk.blue('Enter a GitHub Org [required]'), }, ]); - await createGithubApp(input.org); + return await adminCli(input.org); }; diff --git a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts index ca5eb2c02a..cb2613c691 100644 --- a/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts +++ b/packages/cli/src/commands/create-github-app/GithubCreateAppServer.ts @@ -33,7 +33,7 @@ const FORM_PAGE = ` `; -type GithubAppConfig = { +export type GithubAppConfig = { appId: number; slug?: string; name?: string; diff --git a/packages/cli/src/commands/create-github-app/index.ts b/packages/cli/src/commands/create-github-app/index.ts index e04e39fdf6..16cb921ce4 100644 --- a/packages/cli/src/commands/create-github-app/index.ts +++ b/packages/cli/src/commands/create-github-app/index.ts @@ -14,62 +14,26 @@ * limitations under the License. */ -import fs from 'fs-extra'; import chalk from 'chalk'; -import { stringify as stringifyYaml } from 'yaml'; -import inquirer, { Question, Answers } from 'inquirer'; -import { paths } from '../../lib/paths'; -import { GithubCreateAppServer } from './GithubCreateAppServer'; +import fs from 'fs-extra'; +import inquirer, { Question } from 'inquirer'; import fetch from 'node-fetch'; import openBrowser from 'react-dev-utils/openBrowser'; +import { stringify as stringifyYaml } from 'yaml'; +import { GithubAppConfig } from '@backstage/integration'; + +import { paths } from '../../lib/paths'; +import { GithubCreateAppServer } from './GithubCreateAppServer'; + // This is an experimental command that at this point does not support GitHub Enterprise // due to lacking support for creating apps from manifests. // https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app-from-a-manifest export default async (org: string) => { - // Why is the Org check not done here? - const answers: Answers = await inquirer.prompt({ - name: 'appType', - type: 'checkbox', - message: - 'Select permissions [required] (these can be changed later but then require approvals in all installations)', - choices: [ - { - name: 'Read access to content (required by Software Catalog to ingest data from repositories)', - value: 'read', - checked: true, - }, - { - name: 'Read access to members (required by Software Catalog to ingest GitHub teams)', - value: 'members', - checked: true, - }, - { - name: 'Read and Write to content and actions (required by Software Templates to create new repositories)', - value: 'write', - }, - ], - }); - - if (answers.appType.length === 0) { - console.log(chalk.red('You must select at least one permission')); - process.exit(1); - } - + // TODO(tudi2d): Why is the Org check not done here? + const answers = await permissionAnswers(); await verifyGithubOrg(org); - const { slug, name, ...config } = await GithubCreateAppServer.run({ - org, - permissions: answers.appType, - }); + const { fileName } = await createGHAYaml(org, answers.appType); - const fileName = `github-app-${slug}-credentials.yaml`; - const content = `# Name: ${name}\n${stringifyYaml(config)}`; - await fs.writeFile(paths.resolveTargetRoot(fileName), content); - console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`); - console.log( - chalk.yellow( - 'This file contains sensitive credentials, it should not be committed to version control and handled with care!', - ), - ); console.log( "Here's an example on how to update the integrations section in app-config.yaml", ); @@ -83,6 +47,42 @@ integrations: ); }; +/** + * + * @param org string: GitHub Organisation + * @returns Promise: Partial App Config for Admin CLI + */ +export async function adminCli(org: string) { + await verifyGithubOrg(org); + const answers = await permissionAnswers({ + read: true, + members: true, + write: true, + }); + const { fileName, config } = await createGHAYaml(org, answers.appType); + const auth = { + providers: { + github: { + clientId: config.clientId, + clientSecret: config.clientSecret, + }, + }, + }; + const integration = { + github: { + host: 'github.com', + apps: { + $include: `${fileName}`, + }, + }, + }; + + return { + auth, + integration, + }; +} + async function verifyGithubOrg(org: string): Promise { let response; @@ -121,6 +121,7 @@ async function verifyGithubOrg(org: string): Promise { openBrowser('https://github.com/account/organizations/new'); + // TODO(tudi2d): Could we maybe re-run the command rather than exit? console.log( chalk.yellow( 'Please re-run this command when you have created your new organization', @@ -130,3 +131,68 @@ async function verifyGithubOrg(org: string): Promise { process.exit(0); } } + +async function permissionAnswers( + checked: { read: boolean; members: boolean; write: boolean } = { + read: true, + members: true, + write: false, + }, +) { + const answers = await inquirer.prompt<{ appType: string[] }>({ + name: 'appType', + type: 'checkbox', + message: + 'Select permissions [required] (these can be changed later but then require approvals in all installations)', + choices: [ + { + name: 'Read access to content (required by Software Catalog to ingest data from repositories)', + value: 'read', + checked: checked.read, + }, + { + name: 'Read access to members (required by Software Catalog to ingest GitHub teams)', + value: 'members', + checked: checked.members, + }, + { + name: 'Read and Write to content and actions (required by Software Templates to create new repositories)', + value: 'write', + checked: checked.write, + }, + ], + }); + + if (answers.appType.length === 0) { + console.log(chalk.red('You must select at least one permission')); + // TODO(tudi2d): Maybe re-do input rather than exit? + process.exit(1); + } + + return answers; +} + +async function createGHAYaml( + org: string, + permissions: string[], +): Promise<{ fileName: string; config: GithubAppConfig }> { + const { slug, name, ...config } = await GithubCreateAppServer.run({ + org, + permissions, + }); + + const fileName = `github-app-${slug}-credentials.yaml`; + const content = `# Name: ${name}\n${stringifyYaml(config)}`; + await fs.writeFile(paths.resolveTargetRoot(fileName), content); + console.log(`GitHub App configuration written to ${chalk.cyan(fileName)}`); + console.log( + chalk.yellow( + 'This file contains sensitive credentials, it should not be committed to version control and handled with care!', + ), + ); + + return { + fileName, + config, + }; +}