From 7c6306fc8a06ffba417ce6934e82ccaf21b90e8b Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 7 Sep 2022 10:55:31 -0300 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 74e4a695bd7ac006d390640e24bc418ae6a10deb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 09:43:23 +0000 Subject: [PATCH 07/20] Update dependency @yarnpkg/parsers to v3.0.0-rc.21 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7e67c7174a..5cbd0e4b79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15319,12 +15319,12 @@ __metadata: linkType: hard "@yarnpkg/parsers@npm:^3.0.0-rc.4": - version: 3.0.0-rc.20 - resolution: "@yarnpkg/parsers@npm:3.0.0-rc.20" + version: 3.0.0-rc.21 + resolution: "@yarnpkg/parsers@npm:3.0.0-rc.21" dependencies: js-yaml: ^3.10.0 tslib: ^2.4.0 - checksum: a0dfc2d4d824322ae779363870ad7705b42161ad5e90c992404bf5dc0454a046a6f04144303c9bb047239e480b882b4105272127f2ba080b865180276c46deda + checksum: c0741ef01089a7d452dfb75b8c24a82ddcc3e218dac22b794a06f2e50c7070378c460f63f63fb7ed0f62f634114e448e3d85a75ccd4db84e1526242ae9b4a7d5 languageName: node linkType: hard From 02561f0a5b720e76c80d95e06c92b895218f94f5 Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 28 Sep 2022 08:54:25 -0300 Subject: [PATCH 08/20] 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 74e1d94c380625abc3209793fc32205a03b9e033 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 11:55:11 +0000 Subject: [PATCH 09/20] Update dependency core-js to v3.25.3 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7e67c7174a..0f995356d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18753,9 +18753,9 @@ __metadata: linkType: hard "core-js@npm:^3.6.5": - version: 3.25.2 - resolution: "core-js@npm:3.25.2" - checksum: e93c6c645d44d98973efb07750975552ad405f080f5a563a99972ff6b2c5c6ee25705f55accd363f5dccd51e9e5f56be25e2be6c14a7294da65763e0e5659c02 + version: 3.25.3 + resolution: "core-js@npm:3.25.3" + checksum: 26ca0a5e575e6da03dc30099f48bc00db50a1c0a3f81299165f111cb163869ae453de5e345f2cb7bc8be85a9adcee11fca94f0fbdbdf6d483c68e1c55a9efc5a languageName: node linkType: hard From c7fa044a54e4008b38d2390ad70d428330658d4f Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 28 Sep 2022 08:56:59 -0300 Subject: [PATCH 10/20] 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 bb629a9ae2cb8a9eb2a2549d42bc2ea940079c6e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 12:09:33 +0000 Subject: [PATCH 11/20] Update dependency google-auth-library to v8.5.2 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7e67c7174a..254cafbc0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23710,8 +23710,8 @@ __metadata: linkType: hard "google-auth-library@npm:^8.0.0": - version: 8.5.1 - resolution: "google-auth-library@npm:8.5.1" + version: 8.5.2 + resolution: "google-auth-library@npm:8.5.2" dependencies: arrify: ^2.0.0 base64-js: ^1.3.0 @@ -23722,7 +23722,7 @@ __metadata: gtoken: ^6.1.0 jws: ^4.0.0 lru-cache: ^6.0.0 - checksum: 0d80d7e136e672055e44fdb44f9e13843a554cc531a01deb1b0da6b78427778785faa7377ed85d840208bb61878cd7d69902172886476bd49ea67e645729570a + checksum: 5ab2904f5da3c119a7c241a1d5a11640468a7da58dfcec8a9cad181cc2723e6662b3c65997906069852d9fa066ba3e4b7f14e11cbb80d541e320b66ead6777dc languageName: node linkType: hard From c1638eb62b358a6344e03c5e9703fa485e455f52 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Sep 2022 12:10:30 +0000 Subject: [PATCH 12/20] Update dependency ldapjs to v2.3.3 Signed-off-by: Renovate Bot --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7e67c7174a..696d619ddf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13797,11 +13797,11 @@ __metadata: linkType: hard "@types/ldapjs@npm:^2.2.0": - version: 2.2.2 - resolution: "@types/ldapjs@npm:2.2.2" + version: 2.2.4 + resolution: "@types/ldapjs@npm:2.2.4" dependencies: "@types/node": "*" - checksum: e5da993f06240251dad1e095564c722124ed08d915781fb3046f7933cde94b5d2681030ab9f48d4aa5d72b655b501bfd6f2ad4ea41d557d0e1a08d574ed058bf + checksum: 3f240809927e1292380e5977579d7edffb6e5e41b35510c7091b51da19e7387d4ebff0f254c0b145f3cf5d5485b6bb39c77335d857d092e6f4e9ce5a6682a872 languageName: node linkType: hard @@ -27722,8 +27722,8 @@ __metadata: linkType: hard "ldapjs@npm:^2.2.0": - version: 2.3.2 - resolution: "ldapjs@npm:2.3.2" + version: 2.3.3 + resolution: "ldapjs@npm:2.3.3" dependencies: abstract-logging: ^2.0.0 asn1: ^0.2.4 @@ -27733,7 +27733,7 @@ __metadata: once: ^1.4.0 vasync: ^2.2.0 verror: ^1.8.1 - checksum: 4eb7b18839d8fe1d8acc0ba5d2aa71aefcb25ecb6f4f9582299d73faf546984326590c44366f8b148d3a4276707006b1aaffffdc548b3c4ca753e720403da4a5 + checksum: 791cf22812805db3888dfc8aa8e4db8337e67387b318522c9965d40e678ecdb64950a6ea5930083590ec3377038620a3e63f90e985173ae6aca40e7f485290e0 languageName: node linkType: hard From 7219c3d53e7a40992528e664382d086947fcd481 Mon Sep 17 00:00:00 2001 From: Leonardo Maier Date: Wed, 28 Sep 2022 09:47:12 -0300 Subject: [PATCH 13/20] 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 14/20] 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(); }); From 9ddff1d46fd5390c59e7d4844ebfec6b131c2327 Mon Sep 17 00:00:00 2001 From: Matteo Silvestri Date: Wed, 28 Sep 2022 15:51:43 +0200 Subject: [PATCH 15/20] add upgrade-backstage-app.sh script to contrib Signed-off-by: Matteo Silvestri --- .../scripts/upgrade-backstage-app/README.md | 30 +++++++++++++++++++ .../upgrade-backstage-app.sh | 15 ++++++++++ 2 files changed, 45 insertions(+) create mode 100644 contrib/scripts/upgrade-backstage-app/README.md create mode 100755 contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh diff --git a/contrib/scripts/upgrade-backstage-app/README.md b/contrib/scripts/upgrade-backstage-app/README.md new file mode 100644 index 0000000000..ea8b0a0e2f --- /dev/null +++ b/contrib/scripts/upgrade-backstage-app/README.md @@ -0,0 +1,30 @@ +# Upgrade Backstage App + +## Overview + +The script upgrades the version of Backstage created via the create-app plugin. It lets you resolve conflicts iteratively with your own merge [tool](https://www.git-scm.com/docs/git-mergetool). + +## Requirements + +You will need to have the following tools in your shell environment: + +- git +- curl +- jq + +## Usage + +Here's how to use the script: + +1. download the script +2. copy it in the root of your app +3. bootstrap a git repository (you may already have done so): + +```bash +git init +git add . +git commit -m "initial commit" +``` + +4. run `sh upgrade-backstage-app.sh [optional-backstage-version]` +5. resolve any conflicts iteratively diff --git a/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh b/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh new file mode 100755 index 0000000000..d68d9e31c4 --- /dev/null +++ b/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh @@ -0,0 +1,15 @@ +#!/bin/sh +CURRENT_VERSION="v$(cat backstage.json | jq -r '.version')" +TARGET_VERSION=${1:-"$(curl -s https://api.github.com/repos/backstage/backstage/releases/latest | jq -r '.tag_name')"} +CREATE_APP_CURRENT_VERSION=$(curl -s https://raw.githubusercontent.com/backstage/backstage/$CURRENT_VERSION/packages/create-app/package.json | jq -r '.version') +CREATE_APP_TARGET_VERSION=$(curl -s https://raw.githubusercontent.com/backstage/backstage/$TARGET_VERSION/packages/create-app/package.json | jq -r '.version') + +if [ "$CURRENT_VERSION" == "$TARGET_VERSION" ]; then + echo "Already up to date" +else + echo "Attempting upgrade from Backstage $CURRENT_VERSION (create-app $CREATE_APP_CURRENT_VERSION) to $TARGET_VERSION ($CREATE_APP_TARGET_VERSION)" + rm -rf .upgrade && mkdir .upgrade + curl -s https://raw.githubusercontent.com/backstage/upgrade-helper-diff/master/diffs/$CREATE_APP_CURRENT_VERSION..$CREATE_APP_TARGET_VERSION.diff > .upgrade/upgrade.diff + git apply -3 .upgrade/upgrade.diff + git mergetool +fi From a180dcd1687bca1dfab718be8f4f75a4393be6a2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 28 Sep 2022 18:27:25 +0200 Subject: [PATCH 16/20] github: switch docs-like-code label to techdocs Signed-off-by: Patrik Oldsberg --- .github/labeler.yml | 2 +- .github/workflows/sync_snyk-github-issues.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index bfba5eb321..587f29efa3 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -9,7 +9,7 @@ search: - plugins/search/**/* - plugins/search-*/**/* - packages/search-*/**/* -docs-like-code: +techdocs: - plugins/techdocs/**/* - plugins/techdocs-*/**/* - packages/techdocs-*/**/* diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index ae65e90ac0..46a6227f2c 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -22,7 +22,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.5 + uses: backstage/actions/yarn-install@v0.5.6 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} From bff6170f32b1f579d6c799d1f41d3523b7416ef3 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Thu, 29 Sep 2022 11:42:23 +0200 Subject: [PATCH 17/20] Pin mkdocs to latest known working version Signed-off-by: Eric Peterson --- .github/workflows/verify_e2e-techdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index e62a9040a6..4ed406f52e 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -39,7 +39,7 @@ jobs: run: yarn build - name: Install mkdocs & techdocs-core - run: python -m pip install mkdocs-techdocs-core + run: python -m pip install mkdocs-techdocs-core==1.1.6 mkdocs==1.3.1 - name: techdocs-cli e2e test working-directory: packages/techdocs-cli From 44c9a95dcfe55ef091e8a800ff5fd573a8c362ef Mon Sep 17 00:00:00 2001 From: ivgo Date: Thu, 29 Sep 2022 12:51:31 +0200 Subject: [PATCH 18/20] Add return statement in fallback 'set' Signed-off-by: ivgo --- .changeset/clever-ties-burn.md | 5 +++++ .../user-settings/src/apis/StorageApi/UserSettingsStorage.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/clever-ties-burn.md diff --git a/.changeset/clever-ties-burn.md b/.changeset/clever-ties-burn.md new file mode 100644 index 0000000000..cdd861e069 --- /dev/null +++ b/.changeset/clever-ties-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Prevent `.set()` to execute a request to the StorageClient if the user is `guest` diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts index 8905b4aeea..ef4d3ead5d 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts @@ -118,6 +118,7 @@ export class UserSettingsStorage implements StorageApi { if (!(await this.isSignedIn())) { await this.fallback.set(key, data); this.notifyChanges({ key, presence: 'present', value: data }); + return; } const fetchUrl = await this.getFetchUrl(key); From 63a0f358354dd8b22c1286fff3a2735463995e51 Mon Sep 17 00:00:00 2001 From: ivgo Date: Thu, 29 Sep 2022 13:24:37 +0200 Subject: [PATCH 19/20] Add tests for the fallback Signed-off-by: ivgo --- .../StorageApi/UserSettingsStorage.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts index 0ebd96d19c..a421c413dc 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts @@ -38,6 +38,10 @@ describe('Persistent Storage API', () => { const mockIdentityApi: Partial = { getCredentials: async () => ({ token: 'a-token' }), }; + const mockIdentityApiFallback: Partial = { + // This API recreates the guest mode, where the WebStorage is used as fallback + getCredentials: async () => ({}), + }; const createPersistentStorage = ( args?: Partial<{ @@ -56,6 +60,23 @@ describe('Persistent Storage API', () => { }); }; + const createPersistentStorageFallback = ( + args?: Partial<{ + fetchApi: FetchApi; + discoveryApi: DiscoveryApi; + errorApi: ErrorApi; + namespace?: string; + }>, + ): StorageApi => { + return UserSettingsStorage.create({ + errorApi: mockErrorApi, + fetchApi: new MockFetchApi(), + discoveryApi: mockDiscoveryApi, + identityApi: mockIdentityApiFallback as IdentityApi, + ...args, + }); + }; + afterEach(() => { jest.resetAllMocks(); }); @@ -124,6 +145,33 @@ describe('Persistent Storage API', () => { await storage.set('my-key', dummyValue); }); + it('should fallback set when user not logged in', async () => { + const storage = createPersistentStorageFallback(); + + const selectedKeyNextHandler = jest.fn(); + const dummyValue = 'my-value'; + + await new Promise(resolve => { + storage.observe$('my-key').subscribe({ + next: snapshot => { + selectedKeyNextHandler(snapshot); + if (snapshot.presence === 'present') { + resolve(); + } + }, + }); + + storage.set('my-key', dummyValue); + }); + + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'my-key', + value: dummyValue, + presence: 'present', + }); + }); + it('should subscribe to key changes when setting a new value', async () => { const storage = createPersistentStorage(); From 466f0d4123d174579646fc9397615e9a2cf4846a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:38:52 +0000 Subject: [PATCH 20/20] Update dependency @octokit/auth-app to v4.0.6 Signed-off-by: Renovate Bot --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b573c9102b..66cc98ff33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11142,8 +11142,8 @@ __metadata: linkType: hard "@octokit/auth-app@npm:^4.0.0": - version: 4.0.5 - resolution: "@octokit/auth-app@npm:4.0.5" + version: 4.0.6 + resolution: "@octokit/auth-app@npm:4.0.6" dependencies: "@octokit/auth-oauth-app": ^5.0.0 "@octokit/auth-oauth-user": ^2.0.0 @@ -11155,7 +11155,7 @@ __metadata: lru-cache: ^6.0.0 universal-github-app-jwt: ^1.0.1 universal-user-agent: ^6.0.0 - checksum: 26da7c1b279be68be17572897652091257a5ef7666fa5ac4c79dd42f2717aed560b117089c44ad766b4f90a87348572356ac3571c417fef385fdf31794281023 + checksum: 342fece6db4470ee489e710af8aa14ffd3a89e666815a15fadbabe3a382932a714d5bece24375f0ca39f3310f9d10bd28bc348210282cbc6701d27dd9b2ff7ed languageName: node linkType: hard