diff --git a/.changeset/silly-rules-join.md b/.changeset/silly-rules-join.md index 7e4d3281d7..88d3898317 100644 --- a/.changeset/silly-rules-join.md +++ b/.changeset/silly-rules-join.md @@ -1,5 +1,5 @@ --- -'@backstage/create-app': minor +'@backstage/create-app': patch --- Initializes a git repository when creating an app using @packages/create-app diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 95039fe9e7..47ac8b6be6 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -33,6 +33,7 @@ const checkPathExistsMock = jest.spyOn(tasks, 'checkPathExistsTask'); const templatingMock = jest.spyOn(tasks, 'templatingTask'); const checkAppExistsMock = jest.spyOn(tasks, 'checkAppExistsTask'); const initGitRepositoryMock = jest.spyOn(tasks, 'initGitRepository'); +const checkForGitSetup = jest.spyOn(tasks, 'checkForGitSetup'); const createTemporaryAppFolderMock = jest.spyOn( tasks, 'createTemporaryAppFolderTask', @@ -57,6 +58,7 @@ describe('command entrypoint', () => { name: 'MyApp', dbType: 'PostgreSQL', }); + checkForGitSetup.mockResolvedValue(true); }); afterEach(() => { @@ -88,4 +90,11 @@ describe('command entrypoint', () => { await createApp(cmd); expect(buildAppMock).not.toHaveBeenCalled(); }); + + it('should not call `initGitRepository` when `isGitConfigured` is false', async () => { + const cmd = {} as unknown as Command; + checkForGitSetup.mockResolvedValue(false); + await createApp(cmd); + expect(initGitRepositoryMock).not.toHaveBeenCalled(); + }); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 22f7424cda..74e32eeba2 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -29,37 +29,41 @@ import { moveAppTask, templatingTask, initGitRepository, + checkForGitSetup, } from './lib/tasks'; export default async (opts: OptionValues): Promise => { /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); - const answers: Answers = await inquirer.prompt([ - { - type: 'input', - name: 'name', - message: chalk.blue('Enter a name for the app [required]'), - validate: (value: any) => { - if (!value) { - return chalk.red('Please enter a name for the app'); - } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { - return chalk.red( - 'App name must be lowercase and contain only letters, digits, and dashes.', - ); - } - return true; + const answers: Answers = await inquirer.prompt( + [ + { + type: 'input', + name: 'name', + message: chalk.blue('Enter a name for the app [required]'), + validate: (value: any) => { + if (!value) { + return chalk.red('Please enter a name for the app'); + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return chalk.red( + 'App name must be lowercase and contain only letters, digits, and dashes.', + ); + } + return true; + }, + when: (a: Answers) => { + const envName = process.env.BACKSTAGE_APP_NAME; + if (envName) { + a.name = envName; + return false; + } + return true; + }, }, - when: (a: Answers) => { - const envName = process.env.BACKSTAGE_APP_NAME; - if (envName) { - a.name = envName; - return false; - } - return true; - }, - }, - ]); + ], + { defaultBranch: 'master' }, + ); const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); @@ -74,14 +78,18 @@ export default async (opts: OptionValues): Promise => { Task.log('Creating the app...'); try { + const isGitConfigured = await checkForGitSetup(); + if (opts.path) { // Template directly to specified path Task.section('Checking that supplied path exists'); await checkPathExistsTask(appDir); - Task.section('Initializing git repository'); - await initGitRepository(appDir, answers); + if (isGitConfigured) { + Task.section('Initializing git repository'); + await initGitRepository(appDir, answers); + } Task.section('Preparing files'); await templatingTask(templateDir, opts.path, answers); @@ -94,8 +102,10 @@ export default async (opts: OptionValues): Promise => { Task.section('Creating a temporary app directory'); await createTemporaryAppFolderTask(tempDir); - Task.section('Initializing git repository'); - await initGitRepository(tempDir, answers); + if (isGitConfigured) { + Task.section('Initializing git repository'); + await initGitRepository(tempDir, answers); + } Task.section('Preparing files'); await templatingTask(templateDir, tempDir, answers); diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 3550fc330a..45c63d3e02 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -22,6 +22,7 @@ import { Task, buildAppTask, checkAppExistsTask, + checkForGitSetup, checkPathExistsTask, createTemporaryAppFolderTask, initGitRepository, @@ -348,6 +349,33 @@ describe('buildAppTask', () => { }); }); +describe('checkForGitSetup', () => { + it('should check if git package is installed and configured', async () => { + mockExec.mockImplementation((_command, callback) => { + callback(null, { stdout: 'main' }, 'standard error'); + }); + + await checkForGitSetup(); + + expect(mockExec).toHaveBeenCalledTimes(3); + expect(mockExec).toHaveBeenNthCalledWith( + 1, + 'which git', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 2, + 'git config user.name', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 3, + 'git config user.email', + expect.any(Function), + ); + }); +}); + describe('initGitRepository', () => { it('should initialize a git repository at the given path', async () => { const destinationDir = 'tmp/mockApp/'; diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 71575bbd4f..4a081106bb 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -237,6 +237,33 @@ export async function moveAppTask( }); } +/** + * Checks if git package is installed and git credentials exists + * + * @throws if `exec` fails + */ +export async function checkForGitSetup(): Promise { + const runCmd = (cmd: string) => + exec(cmd).catch(error => { + process.stdout.write(error.stderr); + process.stdout.write(error.stdout); + throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); + }); + + try { + await runCmd('which git'); + + const [gitUsername, gitEmail] = await Promise.all([ + runCmd('git config user.name'), + runCmd('git config user.email'), + ]); + + return Boolean(gitUsername.stdout?.trim() && gitEmail.stdout?.trim()); + } catch (error) { + return false; + } +} + /** * Initializes a git repository in the destination folder * @@ -245,25 +272,23 @@ export async function moveAppTask( * @throws if `exec` fails */ export async function initGitRepository(dir: string, context: any) { - const runCmd = async (cmd: string) => { - let cmdResponse = { stdout: '', stderr: '' }; - await Task.forItem('executing', cmd, async () => { - process.chdir(dir); - cmdResponse = await exec(cmd).catch(error => { - process.stdout.write(error.stderr); - process.stdout.write(error.stdout); - throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); - }); + const runCmd = (cmd: string) => + exec(cmd).catch(error => { + process.stdout.write(error.stderr); + process.stdout.write(error.stdout); + throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); }); - return cmdResponse; - }; - await runCmd('git init'); - await runCmd('git commit --allow-empty -m "Initial commit"'); + await Task.forItem('init', 'git repository', async () => { + process.chdir(dir); - const defaultBranch = await runCmd('git branch --format="%(refname:short)"'); + await runCmd('git init'); + await runCmd('git commit --allow-empty -m "Initial commit"'); - context.defaultBranch = defaultBranch.stdout - ? defaultBranch.stdout.trim() - : 'master'; + const defaultBranch = await runCmd( + 'git branch --format="%(refname:short)"', + ); + + context.defaultBranch = defaultBranch.stdout?.trim() || 'master'; + }); }