From 7c6306fc8a06ffba417ce6934e82ccaf21b90e8b Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 7 Sep 2022 10:55:31 -0300 Subject: [PATCH 01/10] Initializes git repository when creating an app Signed-off-by: Leonardo Maier --- .changeset/silly-rules-join.md | 5 + packages/create-app/src/createApp.test.ts | 3 + packages/create-app/src/createApp.ts | 7 + packages/create-app/src/lib/tasks.test.ts | 138 ++++++++++++++++-- packages/create-app/src/lib/tasks.ts | 31 ++++ .../templates/default-app/package.json.hbs | 6 +- 6 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 .changeset/silly-rules-join.md diff --git a/.changeset/silly-rules-join.md b/.changeset/silly-rules-join.md new file mode 100644 index 0000000000..7e4d3281d7 --- /dev/null +++ b/.changeset/silly-rules-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +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 8e7b3bae7d..95039fe9e7 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -32,6 +32,7 @@ const promptMock = jest.spyOn(inquirer, 'prompt'); 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 createTemporaryAppFolderMock = jest.spyOn( tasks, 'createTemporaryAppFolderTask', @@ -67,6 +68,7 @@ describe('command entrypoint', () => { await createApp(cmd); expect(checkAppExistsMock).toHaveBeenCalled(); expect(createTemporaryAppFolderMock).toHaveBeenCalled(); + expect(initGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(moveAppMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); @@ -76,6 +78,7 @@ describe('command entrypoint', () => { const cmd = { path: 'myDirectory' } as unknown as Command; await createApp(cmd); expect(checkPathExistsMock).toHaveBeenCalled(); + expect(initGitRepositoryMock).toHaveBeenCalled(); expect(templatingMock).toHaveBeenCalled(); expect(buildAppMock).toHaveBeenCalled(); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 7cf589e5f8..22f7424cda 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -28,6 +28,7 @@ import { createTemporaryAppFolderTask, moveAppTask, templatingTask, + initGitRepository, } from './lib/tasks'; export default async (opts: OptionValues): Promise => { @@ -79,6 +80,9 @@ export default async (opts: OptionValues): Promise => { Task.section('Checking that supplied path exists'); await checkPathExistsTask(appDir); + Task.section('Initializing git repository'); + await initGitRepository(appDir, answers); + Task.section('Preparing files'); await templatingTask(templateDir, opts.path, answers); } else { @@ -90,6 +94,9 @@ 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); + 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 decc89fbd4..3550fc330a 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -24,6 +24,7 @@ import { checkAppExistsTask, checkPathExistsTask, createTemporaryAppFolderTask, + initGitRepository, moveAppTask, templatingTask, } from './tasks'; @@ -86,28 +87,100 @@ jest.mock('./versions', () => ({ }, })); -describe('tasks', () => { - beforeEach(() => { - mockFs({ - 'projects/my-module.ts': '', - 'projects/dir/my-file.txt': '', - 'tmp/mockApp/.gitignore': '', - 'tmp/mockApp/package.json': '', - 'tmp/mockApp/packages/app/package.json': '', - // load templates into mock filesystem - 'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')), - }); +const mockExec = child_process.exec as unknown as jest.MockedFunction< + ( + command: string, + callback: (error: null, stdout: any, stderr: any) => void, + ) => void +>; + +beforeEach(() => { + mockFs({ + 'projects/my-module.ts': '', + 'projects/dir/my-file.txt': '', + 'tmp/mockApp/.gitignore': '', + 'tmp/mockApp/package.json': '', + 'tmp/mockApp/packages/app/package.json': '', + // load templates into mock filesystem + 'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')), + }); +}); + +afterEach(() => { + mockExec.mockRestore(); + mockFs.restore(); +}); + +describe('checkAppExistsTask', () => { + it('should do nothing if the directory does not exist', async () => { + const dir = 'projects/'; + const name = 'MyNewApp'; + await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow(); }); afterEach(() => { mockFs.restore(); }); - describe('checkAppExistsTask', () => { - it('should do nothing if the directory does not exist', async () => { - const dir = 'projects/'; - const name = 'MyNewApp'; - await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow(); + it('should throw an error when a directory of the same name exists', async () => { + const dir = 'projects/'; + const name = 'dir'; + await expect(checkAppExistsTask(dir, name)).rejects.toThrow( + 'already exists', + ); + }); +}); + +describe('checkPathExistsTask', () => { + it('should create a directory at the given path', async () => { + const appDir = 'projects/newProject'; + await expect(checkPathExistsTask(appDir)).resolves.not.toThrow(); + expect(fs.existsSync(appDir)).toBe(true); + }); + + it('should do nothing if a directory of the same name exists', async () => { + const appDir = 'projects/dir'; + await expect(checkPathExistsTask(appDir)).resolves.not.toThrow(); + expect(fs.existsSync(appDir)).toBe(true); + }); + + it('should fail if a file of the same name exists', async () => { + await expect(checkPathExistsTask('projects/my-module.ts')).rejects.toThrow( + 'already exists', + ); + }); +}); + +describe('createTemporaryAppFolderTask', () => { + it('should create a directory at a given path', async () => { + const tempDir = 'projects/tmpFolder'; + await expect(createTemporaryAppFolderTask(tempDir)).resolves.not.toThrow(); + expect(fs.existsSync(tempDir)).toBe(true); + }); + + it('should fail if a directory of the same name exists', async () => { + const tempDir = 'projects/dir'; + await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow( + 'file already exists', + ); + }); + + it('should fail if a file of the same name exists', async () => { + const tempDir = 'projects/dir/my-file.txt'; + await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow( + 'file already exists', + ); + }); +}); + +describe('buildAppTask', () => { + it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => { + const mockChdir = jest.spyOn(process, 'chdir'); + + // requires callback implementation to support `promisify` wrapper + // https://stackoverflow.com/a/60579617/10044859 + mockExec.mockImplementation((_command, callback) => { + callback(null, 'standard out', 'standard error'); }); it('should throw an error when a file of the same name exists', async () => { @@ -274,3 +347,36 @@ describe('tasks', () => { }); }); }); + +describe('initGitRepository', () => { + it('should initialize a git repository at the given path', async () => { + const destinationDir = 'tmp/mockApp/'; + const context = { + defaultBranch: '', + }; + + mockExec.mockImplementation((_command, callback) => { + callback(null, { stdout: 'main' }, 'standard error'); + }); + + await initGitRepository(destinationDir, context); + + expect(context.defaultBranch).toBe('main'); + expect(mockExec).toHaveBeenCalledTimes(3); + expect(mockExec).toHaveBeenNthCalledWith( + 1, + 'git init', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 2, + 'git commit --allow-empty -m "Initial commit"', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 3, + 'git branch --format="%(refname:short)"', + expect.any(Function), + ); + }); +}); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 1650432969..71575bbd4f 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -236,3 +236,34 @@ export async function moveAppTask( }); }); } + +/** + * Initializes a git repository in the destination folder + * + * @param dir - source path to initialize git repository in + * @param context - template parameters + * @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)}`); + }); + }); + return cmdResponse; + }; + + await runCmd('git init'); + await runCmd('git commit --allow-empty -m "Initial commit"'); + + const defaultBranch = await runCmd('git branch --format="%(refname:short)"'); + + context.defaultBranch = defaultBranch.stdout + ? defaultBranch.stdout.trim() + : 'master'; +} diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index dd4d937db0..19cb98ec09 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -14,9 +14,9 @@ "tsc": "tsc", "tsc:full": "tsc --skipLibCheck false --incremental false", "clean": "backstage-cli repo clean", - "test": "backstage-cli repo test", - "test:all": "backstage-cli repo test --coverage", - "lint": "backstage-cli repo lint --since origin/master", + "test": "backstage-cli test", + "test:all": "lerna run test -- --coverage", + "lint": "backstage-cli repo lint --since origin/{{defaultBranch}}", "lint:all": "backstage-cli repo lint", "prettier:check": "prettier --check .", "create-plugin": "backstage-cli create-plugin --scope internal", From 099ce48eefc0391fa74c75c96770985cdf54557e Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Tue, 20 Sep 2022 17:31:40 -0300 Subject: [PATCH 02/10] Checks if git is installed and has a user configured Signed-off-by: Leonardo Maier --- .changeset/silly-rules-join.md | 2 +- packages/create-app/src/createApp.test.ts | 9 ++++ packages/create-app/src/createApp.ts | 66 +++++++++++++---------- packages/create-app/src/lib/tasks.test.ts | 28 ++++++++++ packages/create-app/src/lib/tasks.ts | 59 ++++++++++++++------ 5 files changed, 118 insertions(+), 46 deletions(-) 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'; + }); } From 7838cd71ffee00dbcd6c2302e9fe34dec6221c30 Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Tue, 20 Sep 2022 18:00:51 -0300 Subject: [PATCH 03/10] Fix package/create-app tests Signed-off-by: Leonardo Maier --- packages/create-app/src/createApp.test.ts | 2 +- packages/create-app/src/lib/tasks.test.ts | 230 ++++++------------ .../templates/default-app/package.json.hbs | 4 +- 3 files changed, 83 insertions(+), 153 deletions(-) diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 47ac8b6be6..36552ae306 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -49,7 +49,7 @@ describe('command entrypoint', () => { }); }); - afterAll(() => { + afterEach(() => { mockFs.restore(); }); diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 45c63d3e02..ac5f9e063b 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -22,12 +22,12 @@ import { Task, buildAppTask, checkAppExistsTask, - checkForGitSetup, checkPathExistsTask, createTemporaryAppFolderTask, - initGitRepository, moveAppTask, templatingTask, + initGitRepository, + checkForGitSetup, } from './tasks'; jest.spyOn(Task, 'log').mockReturnValue(undefined); @@ -88,100 +88,36 @@ jest.mock('./versions', () => ({ }, })); -const mockExec = child_process.exec as unknown as jest.MockedFunction< - ( - command: string, - callback: (error: null, stdout: any, stderr: any) => void, - ) => void ->; +describe('tasks', () => { + const mockExec = child_process.exec as unknown as jest.MockedFunction< + ( + command: string, + callback: (error: null, stdout: any, stderr: any) => void, + ) => void + >; -beforeEach(() => { - mockFs({ - 'projects/my-module.ts': '', - 'projects/dir/my-file.txt': '', - 'tmp/mockApp/.gitignore': '', - 'tmp/mockApp/package.json': '', - 'tmp/mockApp/packages/app/package.json': '', - // load templates into mock filesystem - 'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')), - }); -}); - -afterEach(() => { - mockExec.mockRestore(); - mockFs.restore(); -}); - -describe('checkAppExistsTask', () => { - it('should do nothing if the directory does not exist', async () => { - const dir = 'projects/'; - const name = 'MyNewApp'; - await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow(); + beforeEach(() => { + mockFs({ + 'projects/my-module.ts': '', + 'projects/dir/my-file.txt': '', + 'tmp/mockApp/.gitignore': '', + 'tmp/mockApp/package.json': '', + 'tmp/mockApp/packages/app/package.json': '', + // load templates into mock filesystem + 'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')), + }); }); afterEach(() => { + mockExec.mockRestore(); mockFs.restore(); }); - it('should throw an error when a directory of the same name exists', async () => { - const dir = 'projects/'; - const name = 'dir'; - await expect(checkAppExistsTask(dir, name)).rejects.toThrow( - 'already exists', - ); - }); -}); - -describe('checkPathExistsTask', () => { - it('should create a directory at the given path', async () => { - const appDir = 'projects/newProject'; - await expect(checkPathExistsTask(appDir)).resolves.not.toThrow(); - expect(fs.existsSync(appDir)).toBe(true); - }); - - it('should do nothing if a directory of the same name exists', async () => { - const appDir = 'projects/dir'; - await expect(checkPathExistsTask(appDir)).resolves.not.toThrow(); - expect(fs.existsSync(appDir)).toBe(true); - }); - - it('should fail if a file of the same name exists', async () => { - await expect(checkPathExistsTask('projects/my-module.ts')).rejects.toThrow( - 'already exists', - ); - }); -}); - -describe('createTemporaryAppFolderTask', () => { - it('should create a directory at a given path', async () => { - const tempDir = 'projects/tmpFolder'; - await expect(createTemporaryAppFolderTask(tempDir)).resolves.not.toThrow(); - expect(fs.existsSync(tempDir)).toBe(true); - }); - - it('should fail if a directory of the same name exists', async () => { - const tempDir = 'projects/dir'; - await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow( - 'file already exists', - ); - }); - - it('should fail if a file of the same name exists', async () => { - const tempDir = 'projects/dir/my-file.txt'; - await expect(createTemporaryAppFolderTask(tempDir)).rejects.toThrow( - 'file already exists', - ); - }); -}); - -describe('buildAppTask', () => { - it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => { - const mockChdir = jest.spyOn(process, 'chdir'); - - // requires callback implementation to support `promisify` wrapper - // https://stackoverflow.com/a/60579617/10044859 - mockExec.mockImplementation((_command, callback) => { - callback(null, 'standard out', 'standard error'); + describe('checkAppExistsTask', () => { + it('should do nothing if the directory does not exist', async () => { + const dir = 'projects/'; + const name = 'MyNewApp'; + await expect(checkAppExistsTask(dir, name)).resolves.not.toThrow(); }); it('should throw an error when a file of the same name exists', async () => { @@ -248,12 +184,6 @@ describe('buildAppTask', () => { describe('buildAppTask', () => { it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => { const mockChdir = jest.spyOn(process, 'chdir'); - const mockExec = child_process.exec as unknown as jest.MockedFunction< - ( - command: string, - callback: (error: null, stdout: string, stderr: string) => void, - ) => void - >; // requires callback implementation to support `promisify` wrapper // https://stackoverflow.com/a/60579617/10044859 @@ -347,64 +277,64 @@ describe('buildAppTask', () => { ).toContain('sqlite3"'); }); }); -}); -describe('checkForGitSetup', () => { - it('should check if git package is installed and configured', async () => { - mockExec.mockImplementation((_command, callback) => { - callback(null, { stdout: 'main' }, 'standard error'); + 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), + ); }); + }); - await checkForGitSetup(); + describe('initGitRepository', () => { + it('should initialize a git repository at the given path', async () => { + const destinationDir = 'tmp/mockApp/'; + const context = { + defaultBranch: '', + }; - 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/'; - const context = { - defaultBranch: '', - }; - - mockExec.mockImplementation((_command, callback) => { - callback(null, { stdout: 'main' }, 'standard error'); - }); - - await initGitRepository(destinationDir, context); - - expect(context.defaultBranch).toBe('main'); - expect(mockExec).toHaveBeenCalledTimes(3); - expect(mockExec).toHaveBeenNthCalledWith( - 1, - 'git init', - expect.any(Function), - ); - expect(mockExec).toHaveBeenNthCalledWith( - 2, - 'git commit --allow-empty -m "Initial commit"', - expect.any(Function), - ); - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'git branch --format="%(refname:short)"', - expect.any(Function), - ); + mockExec.mockImplementation((_command, callback) => { + callback(null, { stdout: 'main' }, 'standard error'); + }); + + await initGitRepository(destinationDir, context); + + expect(context.defaultBranch).toBe('main'); + expect(mockExec).toHaveBeenCalledTimes(3); + expect(mockExec).toHaveBeenNthCalledWith( + 1, + 'git init', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 2, + 'git commit --allow-empty -m "Initial commit"', + expect.any(Function), + ); + expect(mockExec).toHaveBeenNthCalledWith( + 3, + 'git branch --format="%(refname:short)"', + expect.any(Function), + ); + }); }); }); diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 19cb98ec09..c5810d8880 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -14,8 +14,8 @@ "tsc": "tsc", "tsc:full": "tsc --skipLibCheck false --incremental false", "clean": "backstage-cli repo clean", - "test": "backstage-cli test", - "test:all": "lerna run test -- --coverage", + "test": "backstage-cli repo test", + "test:all": "backstage-cli repo test --coverage", "lint": "backstage-cli repo lint --since origin/{{defaultBranch}}", "lint:all": "backstage-cli repo lint", "prettier:check": "prettier --check .", From 3735b3177e3865a71d0008f1a1b4831f52dbfde5 Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 21 Sep 2022 16:59:34 -0300 Subject: [PATCH 04/10] Adds command-exists as a dependency Signed-off-by: Leonardo Maier --- packages/create-app/package.json | 2 ++ yarn.lock | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/create-app/package.json b/packages/create-app/package.json index de1f495abe..c6f430702a 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -34,6 +34,7 @@ "dependencies": { "@backstage/cli-common": "workspace:^", "chalk": "^4.0.0", + "command-exists": "^1.2.9", "commander": "^9.1.0", "fs-extra": "10.1.0", "handlebars": "^4.7.3", @@ -43,6 +44,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/inquirer": "^8.1.3", "@types/node": "^16.11.26", diff --git a/yarn.lock b/yarn.lock index bc0ca4bb6f..97e379e44d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3532,11 +3532,13 @@ __metadata: dependencies: "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" + "@types/command-exists": ^1.2.0 "@types/fs-extra": ^9.0.1 "@types/inquirer": ^8.1.3 "@types/node": ^16.11.26 "@types/recursive-readdir": ^2.2.0 chalk: ^4.0.0 + command-exists: ^1.2.9 commander: ^9.1.0 fs-extra: 10.1.0 handlebars: ^4.7.3 From c3498947c2b0742a3d3b0d9bc6d22e67e33678fc Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 21 Sep 2022 17:38:53 -0300 Subject: [PATCH 05/10] Change checkForGitSetup to use commandExists function for validating if git package is installed Signed-off-by: Leonardo Maier --- packages/create-app/src/lib/tasks.test.ts | 55 ++++++++++++++++++----- packages/create-app/src/lib/tasks.ts | 19 ++++---- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index ac5f9e063b..b1cdb293fc 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -30,6 +30,8 @@ import { checkForGitSetup, } from './tasks'; +const commandExists = jest.fn(); + jest.spyOn(Task, 'log').mockReturnValue(undefined); jest.spyOn(Task, 'error').mockReturnValue(undefined); jest.spyOn(Task, 'section').mockReturnValue(undefined); @@ -38,6 +40,12 @@ jest .mockImplementation((_a, _b, taskFunc) => taskFunc()); jest.mock('child_process'); +jest.mock( + 'command-exists', + () => + (...args: any[]) => + commandExists(...args), +); // By mocking this the filesystem mocks won't mess with reading all of the package.jsons jest.mock('./versions', () => ({ @@ -279,26 +287,49 @@ describe('tasks', () => { }); describe('checkForGitSetup', () => { - it('should check if git package is installed and configured', async () => { + it('should return true if git package is installed and git credentials are set', async () => { mockExec.mockImplementation((_command, callback) => { callback(null, { stdout: 'main' }, 'standard error'); }); - await checkForGitSetup(); + commandExists.mockResolvedValue(true); - expect(mockExec).toHaveBeenCalledTimes(3); - expect(mockExec).toHaveBeenNthCalledWith( - 1, - 'which git', - expect.any(Function), - ); - expect(mockExec).toHaveBeenNthCalledWith( - 2, + const isGitConfigured = await checkForGitSetup(); + + expect(isGitConfigured).toBe(true); + expect(mockExec).toHaveBeenCalledWith( 'git config user.name', expect.any(Function), ); - expect(mockExec).toHaveBeenNthCalledWith( - 3, + expect(mockExec).toHaveBeenCalledWith( + 'git config user.email', + expect.any(Function), + ); + }); + + it('should return false if git package is not installed', async () => { + commandExists.mockResolvedValue(false); + + const isGitConfigured = await checkForGitSetup(); + + expect(isGitConfigured).toBe(false); + }); + + it('should return false if git package is installed but git credentials are not set', async () => { + mockExec.mockImplementation((_command, callback) => { + callback(null, { stdout: null }, 'standard error'); + }); + + commandExists.mockResolvedValue(true); + + const isGitConfigured = await checkForGitSetup(); + + expect(isGitConfigured).toBe(false); + expect(mockExec).toHaveBeenCalledWith( + 'git config user.name', + expect.any(Function), + ); + expect(mockExec).toHaveBeenCalledWith( 'git config user.email', expect.any(Function), ); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 4a081106bb..d1c7b125f4 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -28,6 +28,7 @@ import { import { exec as execCb } from 'child_process'; import { packageVersions } from './versions'; import { promisify } from 'util'; +import commandExists from 'command-exists'; const TASK_NAME_MAX_LENGTH = 14; const exec = promisify(execCb); @@ -250,18 +251,16 @@ export async function checkForGitSetup(): Promise { throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); }); - try { - await runCmd('which git'); + const gitCommandExists = await commandExists('git'); - const [gitUsername, gitEmail] = await Promise.all([ - runCmd('git config user.name'), - runCmd('git config user.email'), - ]); + if (!gitCommandExists) return false; - return Boolean(gitUsername.stdout?.trim() && gitEmail.stdout?.trim()); - } catch (error) { - return false; - } + const [gitUsername, gitEmail] = await Promise.all([ + runCmd('git config user.name'), + runCmd('git config user.email'), + ]); + + return Boolean(gitUsername.stdout?.trim() && gitEmail.stdout?.trim()); } /** From b57a21a7fc2d7238f795305c0d52b84a3166d6a8 Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Fri, 23 Sep 2022 12:15:56 -0300 Subject: [PATCH 06/10] Refactor tasks to read git config first and then initialize git repo Signed-off-by: Leonardo Maier --- packages/create-app/src/createApp.test.ts | 12 +++-- packages/create-app/src/createApp.ts | 31 ++++++----- packages/create-app/src/lib/tasks.test.ts | 41 ++++++++------- packages/create-app/src/lib/tasks.ts | 63 +++++++++++++++++------ 4 files changed, 92 insertions(+), 55 deletions(-) diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 36552ae306..76b5d24b98 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -33,7 +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 readGitConfig = jest.spyOn(tasks, 'readGitConfig'); const createTemporaryAppFolderMock = jest.spyOn( tasks, 'createTemporaryAppFolderTask', @@ -58,7 +58,11 @@ describe('command entrypoint', () => { name: 'MyApp', dbType: 'PostgreSQL', }); - checkForGitSetup.mockResolvedValue(true); + readGitConfig.mockResolvedValue({ + name: 'git-user', + email: 'git-email', + defaultBranch: 'git-default-branch', + }); }); afterEach(() => { @@ -91,9 +95,9 @@ describe('command entrypoint', () => { expect(buildAppMock).not.toHaveBeenCalled(); }); - it('should not call `initGitRepository` when `isGitConfigured` is false', async () => { + it('should not call `initGitRepository` when `gitConfig` is undefined', async () => { const cmd = {} as unknown as Command; - checkForGitSetup.mockResolvedValue(false); + readGitConfig.mockResolvedValue(undefined); await createApp(cmd); expect(initGitRepositoryMock).not.toHaveBeenCalled(); }); diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index 74e32eeba2..fd70a4c238 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -29,9 +29,11 @@ import { moveAppTask, templatingTask, initGitRepository, - checkForGitSetup, + readGitConfig, } from './lib/tasks'; +const DEFAULT_BRANCH = 'master'; + export default async (opts: OptionValues): Promise => { /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); @@ -78,7 +80,7 @@ export default async (opts: OptionValues): Promise => { Task.log('Creating the app...'); try { - const isGitConfigured = await checkForGitSetup(); + const gitConfig = await readGitConfig(); if (opts.path) { // Template directly to specified path @@ -86,13 +88,11 @@ export default async (opts: OptionValues): Promise => { Task.section('Checking that supplied path exists'); await checkPathExistsTask(appDir); - if (isGitConfigured) { - Task.section('Initializing git repository'); - await initGitRepository(appDir, answers); - } - Task.section('Preparing files'); - await templatingTask(templateDir, opts.path, answers); + await templatingTask(templateDir, opts.path, { + ...answers, + defaultBranch: gitConfig?.defaultBranch ?? DEFAULT_BRANCH, + }); } else { // Template to temporary location, and then move files @@ -102,18 +102,21 @@ export default async (opts: OptionValues): Promise => { Task.section('Creating a temporary app directory'); await createTemporaryAppFolderTask(tempDir); - if (isGitConfigured) { - Task.section('Initializing git repository'); - await initGitRepository(tempDir, answers); - } - Task.section('Preparing files'); - await templatingTask(templateDir, tempDir, answers); + await templatingTask(templateDir, tempDir, { + ...answers, + defaultBranch: gitConfig?.defaultBranch ?? DEFAULT_BRANCH, + }); Task.section('Moving to final location'); await moveAppTask(tempDir, appDir, answers.name); } + if (gitConfig) { + Task.section('Initializing git repository'); + await initGitRepository(appDir); + } + if (!opts.skipInstall) { Task.section('Building the app'); await buildAppTask(appDir); diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index b1cdb293fc..b8d3202976 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -27,7 +27,7 @@ import { moveAppTask, templatingTask, initGitRepository, - checkForGitSetup, + readGitConfig, } from './tasks'; const commandExists = jest.fn(); @@ -286,17 +286,22 @@ describe('tasks', () => { }); }); - describe('checkForGitSetup', () => { - it('should return true if git package is installed and git credentials are set', async () => { + describe('readGitConfig', () => { + it('should return git config if git package is installed and git credentials are set', async () => { mockExec.mockImplementation((_command, callback) => { callback(null, { stdout: 'main' }, 'standard error'); }); commandExists.mockResolvedValue(true); - const isGitConfigured = await checkForGitSetup(); + const gitConfig = await readGitConfig(); - expect(isGitConfigured).toBe(true); + expect(gitConfig).toBeTruthy(); + expect(gitConfig).toEqual({ + name: 'main', + email: 'main', + defaultBranch: 'main', + }); expect(mockExec).toHaveBeenCalledWith( 'git config user.name', expect.any(Function), @@ -305,14 +310,19 @@ describe('tasks', () => { 'git config user.email', expect.any(Function), ); + expect(mockExec).toHaveBeenCalledWith('git init', expect.any(Function)); + expect(mockExec).toHaveBeenCalledWith( + 'git commit --allow-empty -m "Initial commit"', + expect.any(Function), + ); }); it('should return false if git package is not installed', async () => { commandExists.mockResolvedValue(false); - const isGitConfigured = await checkForGitSetup(); + const gitConfig = await readGitConfig(); - expect(isGitConfigured).toBe(false); + expect(gitConfig).toBeUndefined(); }); it('should return false if git package is installed but git credentials are not set', async () => { @@ -322,9 +332,9 @@ describe('tasks', () => { commandExists.mockResolvedValue(true); - const isGitConfigured = await checkForGitSetup(); + const gitConfig = await readGitConfig(); - expect(isGitConfigured).toBe(false); + expect(gitConfig).toBeUndefined(); expect(mockExec).toHaveBeenCalledWith( 'git config user.name', expect.any(Function), @@ -339,18 +349,14 @@ describe('tasks', () => { describe('initGitRepository', () => { it('should initialize a git repository at the given path', async () => { const destinationDir = 'tmp/mockApp/'; - const context = { - defaultBranch: '', - }; mockExec.mockImplementation((_command, callback) => { callback(null, { stdout: 'main' }, 'standard error'); }); - await initGitRepository(destinationDir, context); + await initGitRepository(destinationDir); - expect(context.defaultBranch).toBe('main'); - expect(mockExec).toHaveBeenCalledTimes(3); + expect(mockExec).toHaveBeenCalledTimes(2); expect(mockExec).toHaveBeenNthCalledWith( 1, 'git init', @@ -361,11 +367,6 @@ describe('tasks', () => { 'git commit --allow-empty -m "Initial commit"', expect.any(Function), ); - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'git branch --format="%(refname:short)"', - expect.any(Function), - ); }); }); }); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index d1c7b125f4..6b40ecac63 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -29,10 +29,17 @@ import { exec as execCb } from 'child_process'; import { packageVersions } from './versions'; import { promisify } from 'util'; import commandExists from 'command-exists'; +import os from 'os'; const TASK_NAME_MAX_LENGTH = 14; const exec = promisify(execCb); +export type GitConfig = { + name?: string; + email?: string; + defaultBranch?: string; +}; + export class Task { static log(name: string = '') { process.stdout.write(`${chalk.green(name)}\n`); @@ -239,11 +246,13 @@ export async function moveAppTask( } /** - * Checks if git package is installed and git credentials exists + * Read git configs by creating a temp folder and initializing a repo * * @throws if `exec` fails */ -export async function checkForGitSetup(): Promise { +export async function readGitConfig(): Promise { + const tempDir = resolvePath(os.tmpdir(), 'git-temp-dir'); + const runCmd = (cmd: string) => exec(cmd).catch(error => { process.stdout.write(error.stderr); @@ -251,26 +260,52 @@ export async function checkForGitSetup(): Promise { throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); }); - const gitCommandExists = await commandExists('git'); + const isGitAvailable = await commandExists('git').catch(() => false); - if (!gitCommandExists) return false; + if (!isGitAvailable) return; - const [gitUsername, gitEmail] = await Promise.all([ - runCmd('git config user.name'), - runCmd('git config user.email'), - ]); + try { + await fs.mkdir(tempDir); - return Boolean(gitUsername.stdout?.trim() && gitEmail.stdout?.trim()); + process.chdir(tempDir); + + const [gitUsername, gitEmail] = await Promise.all([ + runCmd('git config user.name'), + runCmd('git config user.email'), + ]); + + const gitCredentials = Boolean( + gitUsername.stdout?.trim() && gitEmail.stdout?.trim(), + ); + + if (!gitCredentials) return; + + await runCmd('git init'); + await runCmd('git commit --allow-empty -m "Initial commit"'); + + const gitDefaultBranch = await runCmd( + 'git branch --format="%(refname:short)"', + ); + + return { + name: gitUsername.stdout?.trim(), + email: gitEmail.stdout?.trim(), + defaultBranch: gitDefaultBranch.stdout?.trim(), + }; + } catch (error) { + throw new Error(`Failed to read git config, ${error}`); + } finally { + await fs.rm(tempDir, { recursive: true }); + } } /** * Initializes a git repository in the destination folder * * @param dir - source path to initialize git repository in - * @param context - template parameters * @throws if `exec` fails */ -export async function initGitRepository(dir: string, context: any) { +export async function initGitRepository(dir: string) { const runCmd = (cmd: string) => exec(cmd).catch(error => { process.stdout.write(error.stderr); @@ -283,11 +318,5 @@ export async function initGitRepository(dir: string, context: any) { await runCmd('git init'); await runCmd('git commit --allow-empty -m "Initial commit"'); - - const defaultBranch = await runCmd( - 'git branch --format="%(refname:short)"', - ); - - context.defaultBranch = defaultBranch.stdout?.trim() || 'master'; }); } From 02561f0a5b720e76c80d95e06c92b895218f94f5 Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 28 Sep 2022 08:54:25 -0300 Subject: [PATCH 07/10] Removes defaultBranch answer from inquirer and uses cwd to run commands in directory Signed-off-by: Leonardo Maier --- packages/create-app/src/createApp.ts | 53 +++++++++++------------ packages/create-app/src/lib/tasks.test.ts | 21 +++++++-- packages/create-app/src/lib/tasks.ts | 4 +- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/packages/create-app/src/createApp.ts b/packages/create-app/src/createApp.ts index fd70a4c238..182e043565 100644 --- a/packages/create-app/src/createApp.ts +++ b/packages/create-app/src/createApp.ts @@ -38,34 +38,31 @@ 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; - }, - when: (a: Answers) => { - const envName = process.env.BACKSTAGE_APP_NAME; - if (envName) { - a.name = envName; - return false; - } - 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; }, - ], - { defaultBranch: 'master' }, - ); + when: (a: Answers) => { + const envName = process.env.BACKSTAGE_APP_NAME; + if (envName) { + a.name = envName; + return false; + } + return true; + }, + }, + ]); const templateDir = paths.resolveOwn('templates/default-app'); const tempDir = resolvePath(os.tmpdir(), answers.name); @@ -112,7 +109,7 @@ export default async (opts: OptionValues): Promise => { await moveAppTask(tempDir, appDir, answers.name); } - if (gitConfig) { + if (gitConfig?.name && gitConfig?.email) { Task.section('Initializing git repository'); await initGitRepository(appDir); } diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index b8d3202976..0144f9f575 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -17,7 +17,8 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; import child_process from 'child_process'; -import path from 'path'; +import path, { resolve as resolvePath } from 'path'; +import os from 'os'; import { Task, buildAppTask, @@ -100,6 +101,7 @@ describe('tasks', () => { const mockExec = child_process.exec as unknown as jest.MockedFunction< ( command: string, + options: any, callback: (error: null, stdout: any, stderr: any) => void, ) => void >; @@ -287,8 +289,10 @@ describe('tasks', () => { }); describe('readGitConfig', () => { + const tmpDir = resolvePath(os.tmpdir(), 'git-temp-dir'); + it('should return git config if git package is installed and git credentials are set', async () => { - mockExec.mockImplementation((_command, callback) => { + mockExec.mockImplementation((_command, _options, callback) => { callback(null, { stdout: 'main' }, 'standard error'); }); @@ -304,15 +308,22 @@ describe('tasks', () => { }); expect(mockExec).toHaveBeenCalledWith( 'git config user.name', + { cwd: tmpDir }, expect.any(Function), ); expect(mockExec).toHaveBeenCalledWith( 'git config user.email', + { cwd: tmpDir }, + expect.any(Function), + ); + expect(mockExec).toHaveBeenCalledWith( + 'git init', + { cwd: tmpDir }, expect.any(Function), ); - expect(mockExec).toHaveBeenCalledWith('git init', expect.any(Function)); expect(mockExec).toHaveBeenCalledWith( 'git commit --allow-empty -m "Initial commit"', + { cwd: tmpDir }, expect.any(Function), ); }); @@ -326,7 +337,7 @@ describe('tasks', () => { }); it('should return false if git package is installed but git credentials are not set', async () => { - mockExec.mockImplementation((_command, callback) => { + mockExec.mockImplementation((_command, _options, callback) => { callback(null, { stdout: null }, 'standard error'); }); @@ -337,10 +348,12 @@ describe('tasks', () => { expect(gitConfig).toBeUndefined(); expect(mockExec).toHaveBeenCalledWith( 'git config user.name', + { cwd: tmpDir }, expect.any(Function), ); expect(mockExec).toHaveBeenCalledWith( 'git config user.email', + { cwd: tmpDir }, expect.any(Function), ); }); diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 6b40ecac63..5514b930fc 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -254,7 +254,7 @@ export async function readGitConfig(): Promise { const tempDir = resolvePath(os.tmpdir(), 'git-temp-dir'); const runCmd = (cmd: string) => - exec(cmd).catch(error => { + exec(cmd, { cwd: tempDir }).catch(error => { process.stdout.write(error.stderr); process.stdout.write(error.stdout); throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); @@ -267,8 +267,6 @@ export async function readGitConfig(): Promise { try { await fs.mkdir(tempDir); - process.chdir(tempDir); - const [gitUsername, gitEmail] = await Promise.all([ runCmd('git config user.name'), runCmd('git config user.email'), From c7fa044a54e4008b38d2390ad70d428330658d4f Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 28 Sep 2022 08:56:59 -0300 Subject: [PATCH 08/10] Adds git identity step on e2e tests workflow Signed-off-by: Leonardo Maier --- .github/workflows/verify_e2e-linux.yml | 5 +++++ .github/workflows/verify_e2e-windows.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 0eedfd9d6b..466656d6f7 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -41,6 +41,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Configure Git + run: | + git config --global user.email noreply@backstage.io + git config --global user.name 'GitHub e2e user' + - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 3bc41a9eeb..4e8025f7d5 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -39,6 +39,11 @@ jobs: - uses: actions/checkout@v3 + - name: Configure Git + run: | + git config --global user.email noreply@backstage.io + git config --global user.name 'GitHub e2e user' + - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: From 7219c3d53e7a40992528e664382d086947fcd481 Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 28 Sep 2022 09:47:12 -0300 Subject: [PATCH 09/10] Fix lint issues Signed-off-by: Leonardo Maier --- packages/create-app/src/lib/tasks.test.ts | 4 ++-- packages/create-app/src/lib/tasks.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 0144f9f575..b918fc0180 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -333,7 +333,7 @@ describe('tasks', () => { const gitConfig = await readGitConfig(); - expect(gitConfig).toBeUndefined(); + expect(gitConfig).toEqual({}); }); it('should return false if git package is installed but git credentials are not set', async () => { @@ -345,7 +345,7 @@ describe('tasks', () => { const gitConfig = await readGitConfig(); - expect(gitConfig).toBeUndefined(); + expect(gitConfig).toEqual({}); expect(mockExec).toHaveBeenCalledWith( 'git config user.name', { cwd: tmpDir }, diff --git a/packages/create-app/src/lib/tasks.ts b/packages/create-app/src/lib/tasks.ts index 5514b930fc..1e30f9d33a 100644 --- a/packages/create-app/src/lib/tasks.ts +++ b/packages/create-app/src/lib/tasks.ts @@ -250,7 +250,7 @@ export async function moveAppTask( * * @throws if `exec` fails */ -export async function readGitConfig(): Promise { +export async function readGitConfig(): Promise { const tempDir = resolvePath(os.tmpdir(), 'git-temp-dir'); const runCmd = (cmd: string) => @@ -262,7 +262,7 @@ export async function readGitConfig(): Promise { const isGitAvailable = await commandExists('git').catch(() => false); - if (!isGitAvailable) return; + if (!isGitAvailable) return {}; try { await fs.mkdir(tempDir); @@ -276,7 +276,7 @@ export async function readGitConfig(): Promise { gitUsername.stdout?.trim() && gitEmail.stdout?.trim(), ); - if (!gitCredentials) return; + if (!gitCredentials) return {}; await runCmd('git init'); await runCmd('git commit --allow-empty -m "Initial commit"'); From 168fb4adc0a23e7536463ce8d04a4c6542248980 Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 28 Sep 2022 10:17:52 -0300 Subject: [PATCH 10/10] Fix createApp test Signed-off-by: Leonardo Maier --- packages/create-app/src/createApp.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 76b5d24b98..ec8d15ead1 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -97,7 +97,7 @@ describe('command entrypoint', () => { it('should not call `initGitRepository` when `gitConfig` is undefined', async () => { const cmd = {} as unknown as Command; - readGitConfig.mockResolvedValue(undefined); + readGitConfig.mockResolvedValue({}); await createApp(cmd); expect(initGitRepositoryMock).not.toHaveBeenCalled(); });