From 1d5b8e1be762006ac644c874f9bf0a01ebb85349 Mon Sep 17 00:00:00 2001 From: moritzraho Date: Thu, 25 May 2023 16:57:45 +0200 Subject: [PATCH 01/11] Add support for quickstart apps --- package.json | 1 + src/commands/app/init.js | 128 ++++++++++++++++++++++++++++++--------- 2 files changed, 101 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 457ee15f..612f2f31 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@adobe/generator-app-common-lib": "^0.3.3", "@adobe/inquirer-table-checkbox": "^1.2.0", "@oclif/core": "^1.15.0", + "@octokit/rest": "^19.0.11", "@parcel/core": "^2.7.0", "@parcel/reporter-cli": "^2.7.0", "ajv": "^8", diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 9c7eb26f..fe4e866a 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -23,6 +23,7 @@ const hyperlinker = require('hyperlinker') const { loadAndValidateConfigFile, importConfigJson } = require('../../lib/import-helper') const { SERVICE_API_KEY_ENV } = require('../../lib/defaults') +const { Octokit } = require('@octokit/rest') const DEFAULT_WORKSPACE = 'Stage' @@ -102,24 +103,28 @@ class InitCommand extends TemplatesCommand { this.log(chalk.green(`Loaded Adobe Developer Console configuration file for the Project '${consoleConfig.project.title}' in the Organization '${consoleConfig.project.org.name}'`)) } - // 2. prompt for templates to be installed - const templates = await this.getTemplatesForFlags(flags) - // If no templates selected, init a standalone app - if (templates.length <= 0) { - flags['standalone-app'] = true - } + if (flags['quick-start']) { + await this.withQuickstart(flags['quick-start']) + } else { + // 2. prompt for templates to be installed + const templates = await this.getTemplatesForFlags(flags) + // If no templates selected, init a standalone app + if (templates.length <= 0) { + flags['standalone-app'] = true + } - // 3. run base code generators - const projectName = (consoleConfig && consoleConfig.project.name) || path.basename(process.cwd()) - await this.runCodeGenerators(this.getInitialGenerators(flags), flags.yes, projectName) + // 3. run base code generators + const projectName = (consoleConfig && consoleConfig.project.name) || path.basename(process.cwd()) + await this.runCodeGenerators(this.getInitialGenerators(flags), flags.yes, projectName) - // 4. install templates - await this.installTemplates({ - useDefaultValues: flags.yes, - installNpm: flags.install, - installConfig: flags.login, - templates - }) + // 4. install templates + await this.installTemplates({ + useDefaultValues: flags.yes, + installNpm: flags.install, + installConfig: flags.login, + templates + }) + } // 5. import config - if any if (flags.import) { @@ -151,17 +156,25 @@ class InitCommand extends TemplatesCommand { const consoleConfig = await consoleCLI.getWorkspaceConfig(org.id, project.id, workspace.id, orgSupportedServices) // 7. run base code generators - await this.runCodeGenerators(this.getInitialGenerators(flags), flags.yes, consoleConfig.project.name) + if (!flags['quick-start']) { + await this.runCodeGenerators(this.getInitialGenerators(flags), flags.yes, consoleConfig.project.name) + } // 8. import config await this.importConsoleConfig(consoleConfig) // 9. install templates - await this.installTemplates({ - useDefaultValues: flags.yes, - installNpm: flags.install, - templates - }) + if (!flags['quick-start']) { + await this.installTemplates({ + useDefaultValues: flags.yes, + installNpm: flags.install, + templates + }) + } + + if (flags['quick-start']) { + await this.withQuickstart(flags['quick-start']) + } this.log(chalk.blue(chalk.bold(`Project initialized for Workspace ${workspace.name}, you can run 'aio app use -w ' to switch workspace.`))) } @@ -275,18 +288,18 @@ class InitCommand extends TemplatesCommand { return [searchCriteria, orderByCriteria, selection, selectionLabel] } - async selectConsoleOrg (consoleCLI) { + async selectConsoleOrg (consoleCLI, flags) { const organizations = await consoleCLI.getOrganizations() - const selectedOrg = await consoleCLI.promptForSelectOrganization(organizations) + const selectedOrg = await consoleCLI.promptForSelectOrganization(organizations, { orgId: flags.org, orgCode: flags.org }) await this.ensureDevTermAccepted(consoleCLI, selectedOrg.id) return selectedOrg } - async selectOrCreateConsoleProject (consoleCLI, org) { + async selectOrCreateConsoleProject (consoleCLI, org, flags) { const projects = await consoleCLI.getProjects(org.id) let project = await consoleCLI.promptForSelectProject( projects, - {}, + { projectId: flags.project, projectName: flags.project }, { allowCreate: true } ) if (!project) { @@ -353,7 +366,54 @@ class InitCommand extends TemplatesCommand { { [SERVICE_API_KEY_ENV]: serviceClientId } ) } + + async withQuickstart (fullRepo) { + const octokit = new Octokit({ + auth: '' + }) + + /** @private */ + function relative (basePath, filePath) { + filePath = filePath.replace(/^\/+/, '') // remove fist slash + basePath = basePath.replace(/\/+$/, '') // remove last slash + if (!filePath.startsWith(basePath + '/')) { + return filePath + } + return filePath.slice(`${basePath}/`.length) + } + + /** @private */ + async function downloadRepoDirRecursive (owner, repo, filePath, basePath = '') { + const { data } = await octokit.repos.getContent({ owner, repo, path: filePath }) + + for (const fileOrDir of data) { + if (fileOrDir.type === 'dir') { + const destDir = relative(basePath, fileOrDir.path) + + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }) + } + + await downloadRepoDirRecursive(owner, repo, fileOrDir.path, basePath) + } else { + const response = await fetch(fileOrDir.download_url) + const jsonResponse = await response.text() + await fs.promises.writeFile(relative(basePath, fileOrDir.path), jsonResponse) + } + } + } + + const [owner, repo, basePath] = fullRepo.split('/') + try { + await octokit.repos.getContent({ owner, repo, path: `${basePath}/app.config.yaml` }) + } catch (e) { + this.error('--quick-start does not point to a valid Adobe App Builder app') + } + + await downloadRepoDirRecursive(owner, repo, basePath, basePath) + } } + InitCommand.description = `Create a new Adobe I/O App ` @@ -377,18 +437,26 @@ InitCommand.flags = { description: 'Extension point(s) to implement', char: 'e', multiple: true, - exclusive: ['template'] + exclusive: ['template', 'quick-start'] }), 'standalone-app': Flags.boolean({ description: 'Create a stand-alone application', default: false, - exclusive: ['template'] + exclusive: ['template', 'quick-start'] }), template: Flags.string({ description: 'Specify a link to a template that will be installed', char: 't', multiple: true }), + org: Flags.string({ + description: 'Specify the Adobe Developer Console Org to init from', + exclusive: ['import'] // also no-login + }), + project: Flags.string({ + description: 'Specify the Adobe Developer Console Project to init from', + exclusive: ['import'] // also no-login + }), workspace: Flags.string({ description: 'Specify the Adobe Developer Console Workspace to init from, defaults to Stage', default: DEFAULT_WORKSPACE, @@ -398,6 +466,10 @@ InitCommand.flags = { 'confirm-new-workspace': Flags.boolean({ description: 'Skip and confirm prompt for creating a new workspace', default: false + }), + 'quick-start': Flags.string({ + description: 'Init from gh repo', + exclusive: ['template', 'extension', 'standalone-app'] }) } From 6d7c6ed8b6bed2cc9ebe55c74078eb4f130b670a Mon Sep 17 00:00:00 2001 From: moritzraho Date: Thu, 25 May 2023 18:22:59 +0200 Subject: [PATCH 02/11] fixes --- src/commands/app/init.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index fe4e866a..f539282d 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -137,11 +137,11 @@ class InitCommand extends TemplatesCommand { const consoleCLI = await this.getLibConsoleCLI() // 1. select org - const org = await this.selectConsoleOrg(consoleCLI) + const org = await this.selectConsoleOrg(consoleCLI, flags) // 2. get supported services const orgSupportedServices = await consoleCLI.getEnabledServicesForOrg(org.id) // 3. select or create project - const project = await this.selectOrCreateConsoleProject(consoleCLI, org) + const project = await this.selectOrCreateConsoleProject(consoleCLI, org, flags) // 4. retrieve workspace details, defaults to Stage const workspace = await this.retrieveWorkspaceFromName(consoleCLI, org, project, flags) From 23e6ad99dbd39551e80b787e95d34192f9befff4 Mon Sep 17 00:00:00 2001 From: moritzraho Date: Thu, 25 May 2023 18:31:07 +0200 Subject: [PATCH 03/11] more fixes --- src/commands/app/init.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index f539282d..2c48b69b 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -145,11 +145,14 @@ class InitCommand extends TemplatesCommand { // 4. retrieve workspace details, defaults to Stage const workspace = await this.retrieveWorkspaceFromName(consoleCLI, org, project, flags) - // 5. get list of templates to install - const templates = await this.getTemplatesForFlags(flags, orgSupportedServices) - // If no templates selected, init a standalone app - if (templates.length <= 0) { - flags['standalone-app'] = true + let templates + if (!flags['quick-start']) { + // 5. get list of templates to install + templates = await this.getTemplatesForFlags(flags, orgSupportedServices) + // If no templates selected, init a standalone app + if (templates.length <= 0) { + flags['standalone-app'] = true + } } // 6. download workspace config @@ -303,6 +306,9 @@ class InitCommand extends TemplatesCommand { { allowCreate: true } ) if (!project) { + if (flags.project) { + this.error(`--project ${flags.project} not found`) + } // user has escaped project selection prompt, let's create a new one const projectDetails = await consoleCLI.promptForCreateProjectDetails() project = await consoleCLI.createProject(org.id, projectDetails) From a9b03f98a6fd1d8d73411c4e1e16d0838edcb392 Mon Sep 17 00:00:00 2001 From: moritzraho Date: Tue, 6 Jun 2023 10:06:45 +0200 Subject: [PATCH 04/11] move quick-start to repo --- src/commands/app/init.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 2c48b69b..55f3f366 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -103,8 +103,8 @@ class InitCommand extends TemplatesCommand { this.log(chalk.green(`Loaded Adobe Developer Console configuration file for the Project '${consoleConfig.project.title}' in the Organization '${consoleConfig.project.org.name}'`)) } - if (flags['quick-start']) { - await this.withQuickstart(flags['quick-start']) + if (flags.repo) { + await this.withQuickstart(flags.repo) } else { // 2. prompt for templates to be installed const templates = await this.getTemplatesForFlags(flags) @@ -146,7 +146,7 @@ class InitCommand extends TemplatesCommand { const workspace = await this.retrieveWorkspaceFromName(consoleCLI, org, project, flags) let templates - if (!flags['quick-start']) { + if (!flags.repo) { // 5. get list of templates to install templates = await this.getTemplatesForFlags(flags, orgSupportedServices) // If no templates selected, init a standalone app @@ -159,7 +159,7 @@ class InitCommand extends TemplatesCommand { const consoleConfig = await consoleCLI.getWorkspaceConfig(org.id, project.id, workspace.id, orgSupportedServices) // 7. run base code generators - if (!flags['quick-start']) { + if (!flags.repo) { await this.runCodeGenerators(this.getInitialGenerators(flags), flags.yes, consoleConfig.project.name) } @@ -167,7 +167,7 @@ class InitCommand extends TemplatesCommand { await this.importConsoleConfig(consoleConfig) // 9. install templates - if (!flags['quick-start']) { + if (!flags.repo) { await this.installTemplates({ useDefaultValues: flags.yes, installNpm: flags.install, @@ -175,8 +175,8 @@ class InitCommand extends TemplatesCommand { }) } - if (flags['quick-start']) { - await this.withQuickstart(flags['quick-start']) + if (flags.repo) { + await this.withQuickstart(flags.repo) } this.log(chalk.blue(chalk.bold(`Project initialized for Workspace ${workspace.name}, you can run 'aio app use -w ' to switch workspace.`))) @@ -413,7 +413,7 @@ class InitCommand extends TemplatesCommand { try { await octokit.repos.getContent({ owner, repo, path: `${basePath}/app.config.yaml` }) } catch (e) { - this.error('--quick-start does not point to a valid Adobe App Builder app') + this.error('--repo does not point to a valid Adobe App Builder app') } await downloadRepoDirRecursive(owner, repo, basePath, basePath) @@ -443,12 +443,12 @@ InitCommand.flags = { description: 'Extension point(s) to implement', char: 'e', multiple: true, - exclusive: ['template', 'quick-start'] + exclusive: ['template', 'repo'] }), 'standalone-app': Flags.boolean({ description: 'Create a stand-alone application', default: false, - exclusive: ['template', 'quick-start'] + exclusive: ['template', 'repo'] }), template: Flags.string({ description: 'Specify a link to a template that will be installed', @@ -473,8 +473,8 @@ InitCommand.flags = { description: 'Skip and confirm prompt for creating a new workspace', default: false }), - 'quick-start': Flags.string({ - description: 'Init from gh repo', + repo: Flags.string({ + description: 'Init from gh quick-start repo', exclusive: ['template', 'extension', 'standalone-app'] }) } From 99dd4b27f783a8e3e88045492dca5ceb1b456b4a Mon Sep 17 00:00:00 2001 From: Jesse MacFadyen Date: Mon, 6 Nov 2023 17:05:05 -0800 Subject: [PATCH 05/11] nit: download and install repo first --- src/commands/app/init.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 55f3f366..f61f530a 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -133,6 +133,11 @@ class InitCommand extends TemplatesCommand { } async initWithLogin (flags) { + + if (flags.repo) { + await this.withQuickstart(flags.repo) + } + // this will trigger a login const consoleCLI = await this.getLibConsoleCLI() @@ -175,10 +180,6 @@ class InitCommand extends TemplatesCommand { }) } - if (flags.repo) { - await this.withQuickstart(flags.repo) - } - this.log(chalk.blue(chalk.bold(`Project initialized for Workspace ${workspace.name}, you can run 'aio app use -w ' to switch workspace.`))) } @@ -404,7 +405,7 @@ class InitCommand extends TemplatesCommand { } else { const response = await fetch(fileOrDir.download_url) const jsonResponse = await response.text() - await fs.promises.writeFile(relative(basePath, fileOrDir.path), jsonResponse) + fs.writeFileSync(relative(basePath, fileOrDir.path), jsonResponse) } } } @@ -474,7 +475,7 @@ InitCommand.flags = { default: false }), repo: Flags.string({ - description: 'Init from gh quick-start repo', + description: 'Init from gh quick-start repo. Expected to be of the form //', exclusive: ['template', 'extension', 'standalone-app'] }) } From 0afd9727eec624d53475d8329844270d5be2f251 Mon Sep 17 00:00:00 2001 From: Jesse MacFadyen Date: Tue, 7 Nov 2023 18:49:34 -0800 Subject: [PATCH 06/11] Update init.js missed a , --- src/commands/app/init.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index c3967584..807a8245 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -471,7 +471,7 @@ InitCommand.flags = { repo: Flags.string({ description: 'Init from gh quick-start repo. Expected to be of the form //', exclusive: ['template', 'extension', 'standalone-app'] - }) + }), 'use-jwt': Flags.boolean({ description: 'if the config has both jwt and OAuth Server to Server Credentials (while migrating), prefer the JWT credentials', default: false From 187d06364be70f08ea8daeced2aa9040e194e611 Mon Sep 17 00:00:00 2001 From: Jesse MacFadyen Date: Thu, 9 Nov 2023 18:42:16 -0800 Subject: [PATCH 07/11] use path.relative, specific error handling --- src/commands/app/init.js | 44 +++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 807a8245..2300fca6 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -373,45 +373,43 @@ class InitCommand extends TemplatesCommand { auth: '' }) - /** @private */ - function relative (basePath, filePath) { - filePath = filePath.replace(/^\/+/, '') // remove fist slash - basePath = basePath.replace(/\/+$/, '') // remove last slash - if (!filePath.startsWith(basePath + '/')) { - return filePath - } - return filePath.slice(`${basePath}/`.length) - } - /** @private */ async function downloadRepoDirRecursive (owner, repo, filePath, basePath = '') { + const { data } = await octokit.repos.getContent({ owner, repo, path: filePath }) for (const fileOrDir of data) { if (fileOrDir.type === 'dir') { - const destDir = relative(basePath, fileOrDir.path) - - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }) - } - + const destDir = path.relative(basePath, fileOrDir.path) + fs.ensureDirSync(destDir) await downloadRepoDirRecursive(owner, repo, fileOrDir.path, basePath) } else { + // todo: use a spinner + console.log(`Downloading ${fileOrDir.path}`) const response = await fetch(fileOrDir.download_url) const jsonResponse = await response.text() - fs.writeFileSync(relative(basePath, fileOrDir.path), jsonResponse) + fs.writeFileSync(path.relative(basePath, fileOrDir.path), jsonResponse) } } } - - const [owner, repo, basePath] = fullRepo.split('/') + // todo: this fails past the first level deep, /// + const [owner, repo, ...restOfPath] = fullRepo.split('/') + const basePath = restOfPath.join('/') try { - await octokit.repos.getContent({ owner, repo, path: `${basePath}/app.config.yaml` }) + const result = await octokit.repos.getContent({ owner, repo, path: `${basePath}/apple.config.yaml` }) + // console.log('result : ', result) + await downloadRepoDirRecursive(owner, repo, basePath, basePath) } catch (e) { - this.error('--repo does not point to a valid Adobe App Builder app') + if( e.status === 404) { + this.error('--repo does not point to a valid Adobe App Builder app') + } else if (e.status === 403) { + // todo: remove this, it is feature creep, helpful for debugging + // github rate limit is 60 requests per hour for unauthenticated users + // const resetTime = new Date(e.response.headers['x-ratelimit-reset'] * 1000) + // console.log('resetTime : ', resetTime.toLocaleTimeString()) + this.error('too many requests, please try again later') + } } - - await downloadRepoDirRecursive(owner, repo, basePath, basePath) } } From 64b38a4e3fb6b1a7e9fc6da963970ec7dd8b53af Mon Sep 17 00:00:00 2001 From: Jesse MacFadyen Date: Fri, 10 Nov 2023 12:23:17 -0800 Subject: [PATCH 08/11] coverage, proper mocking --- src/commands/app/init.js | 6 +-- test/commands/app/init.test.js | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 2300fca6..0144df1f 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -372,12 +372,10 @@ class InitCommand extends TemplatesCommand { const octokit = new Octokit({ auth: '' }) - /** @private */ async function downloadRepoDirRecursive (owner, repo, filePath, basePath = '') { const { data } = await octokit.repos.getContent({ owner, repo, path: filePath }) - for (const fileOrDir of data) { if (fileOrDir.type === 'dir') { const destDir = path.relative(basePath, fileOrDir.path) @@ -397,18 +395,18 @@ class InitCommand extends TemplatesCommand { const basePath = restOfPath.join('/') try { const result = await octokit.repos.getContent({ owner, repo, path: `${basePath}/apple.config.yaml` }) - // console.log('result : ', result) await downloadRepoDirRecursive(owner, repo, basePath, basePath) } catch (e) { if( e.status === 404) { this.error('--repo does not point to a valid Adobe App Builder app') } else if (e.status === 403) { - // todo: remove this, it is feature creep, helpful for debugging + // todo: remove this, it is feature creep, but helpful for debugging // github rate limit is 60 requests per hour for unauthenticated users // const resetTime = new Date(e.response.headers['x-ratelimit-reset'] * 1000) // console.log('resetTime : ', resetTime.toLocaleTimeString()) this.error('too many requests, please try again later') } + this.error(e) } } } diff --git a/test/commands/app/init.test.js b/test/commands/app/init.test.js index b031ee30..8751ade0 100644 --- a/test/commands/app/init.test.js +++ b/test/commands/app/init.test.js @@ -17,6 +17,7 @@ const importHelperLib = require('../../../src/lib/import-helper') const inquirer = require('inquirer') const savedDataDir = process.env.XDG_DATA_HOME const yeoman = require('yeoman-environment') +const { Octokit } = require('@octokit/rest') jest.mock('@adobe/aio-lib-core-config') jest.mock('fs-extra') @@ -72,6 +73,8 @@ yeoman.createEnv.mockReturnValue({ runGenerator: jest.fn() }) +jest.mock('@octokit/rest') + // FAKE DATA /////////////////////// // // some fake data @@ -166,6 +169,8 @@ beforeEach(() => { importHelperLib.importConfigJson.mockReset() importHelperLib.loadConfigFile.mockReturnValue({ values: fakeConfig }) + + Octokit.mockReset() }) afterAll(() => { @@ -285,6 +290,83 @@ describe('--no-login', () => { expect(importHelperLib.importConfigJson).not.toHaveBeenCalled() }) + test('--repo --no-login', async () => { + const getContent = () => new Promise((resolve, reject) => { + resolve({ status: 302, data: [] }); + }) + Octokit.mockImplementation(() => ({ repos: { getContent } })) + + command.argv = ['--no-login', '--repo=adobe/appbuilder-quickstarts/qr-code'] + await command.run() + + expect(command.installTemplates).not.toHaveBeenCalled() + expect(LibConsoleCLI.init).not.toHaveBeenCalled() + expect(importHelperLib.importConfigJson).not.toHaveBeenCalled() + }) + + test('--repo --login', async () => { + const getContent = ({owner, repo, path}) => new Promise((resolve, reject) => { + // console.log('args = ', owner, repo, path) + if (path == 'src') { + resolve({ data: []}) + } + else resolve({ + data:[{ + type: 'file', + path: '.gitignore', + download_url: 'https://raw.githubusercontent.com/adobe/appbuilder-quickstarts/master/qr-code/.gitignore' + },{ + type: 'dir', + path: 'src' + }] + }) + }) + Octokit.mockImplementation(() => ({ repos: { getContent } })) + + command.argv = ['--login', '--repo=adobe/appbuilder-quickstarts/qr-code'] + await command.run() + + expect(command.installTemplates).not.toHaveBeenCalled() + expect(LibConsoleCLI.init).toHaveBeenCalled() + expect(importHelperLib.importConfigJson).toHaveBeenCalled() + }) + + test('--repo not valid 404', async () => { + const getContent = () => new Promise((resolve, reject) => { + console.log('rejecting with 404') + reject({ status: 404, data: [] }); + }) + Octokit.mockImplementation(() => ({ repos: { getContent } })) + + command.error = jest.fn() + command.argv = ['--no-login', '--repo=adobe/appbuilder-quickstarts/dne'] + + await command.run() + + expect(command.error).toHaveBeenCalledWith('--repo does not point to a valid Adobe App Builder app') + expect(command.installTemplates).not.toHaveBeenCalled() + expect(LibConsoleCLI.init).not.toHaveBeenCalled() + expect(importHelperLib.importConfigJson).not.toHaveBeenCalled() + }) + + test('--repo not reachable 403', async () => { + const getContent = () => new Promise((resolve, reject) => { + console.log('rejecting with 403') + reject({ status: 403, data: [] }); + }) + Octokit.mockImplementation(() => ({ repos: { getContent } })) + + command.error = jest.fn() + command.argv = ['--no-login', '--repo=adobe/appbuilder-quickstarts/dne'] + + await command.run() + + expect(command.error).toHaveBeenCalledWith('too many requests, please try again later') + expect(command.installTemplates).not.toHaveBeenCalled() + expect(LibConsoleCLI.init).not.toHaveBeenCalled() + expect(importHelperLib.importConfigJson).not.toHaveBeenCalled() + }) + test('--yes --no-install, select excshell', async () => { const installOptions = { useDefaultValues: true, From 2219d9c419dea98b8c5adad38af4ce1188ebb84d Mon Sep 17 00:00:00 2001 From: Jesse MacFadyen Date: Fri, 10 Nov 2023 13:08:43 -0800 Subject: [PATCH 09/11] clean up, linting, coverage 100% --- src/commands/app/init.js | 15 +++++----- test/commands/app/init.test.js | 52 ++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 0144df1f..6cddd3c8 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -132,11 +132,9 @@ class InitCommand extends TemplatesCommand { } async initWithLogin (flags) { - if (flags.repo) { await this.withQuickstart(flags.repo) } - // this will trigger a login const consoleCLI = await this.getLibConsoleCLI() @@ -373,8 +371,7 @@ class InitCommand extends TemplatesCommand { auth: '' }) /** @private */ - async function downloadRepoDirRecursive (owner, repo, filePath, basePath = '') { - + async function downloadRepoDirRecursive (owner, repo, filePath, basePath) { const { data } = await octokit.repos.getContent({ owner, repo, path: filePath }) for (const fileOrDir of data) { if (fileOrDir.type === 'dir') { @@ -394,19 +391,21 @@ class InitCommand extends TemplatesCommand { const [owner, repo, ...restOfPath] = fullRepo.split('/') const basePath = restOfPath.join('/') try { - const result = await octokit.repos.getContent({ owner, repo, path: `${basePath}/apple.config.yaml` }) + await octokit.repos.getContent({ owner, repo, path: `${basePath}/app.config.yaml` }) await downloadRepoDirRecursive(owner, repo, basePath, basePath) } catch (e) { - if( e.status === 404) { + if (e.status === 404) { this.error('--repo does not point to a valid Adobe App Builder app') - } else if (e.status === 403) { + } + if (e.status === 403) { // todo: remove this, it is feature creep, but helpful for debugging // github rate limit is 60 requests per hour for unauthenticated users // const resetTime = new Date(e.response.headers['x-ratelimit-reset'] * 1000) // console.log('resetTime : ', resetTime.toLocaleTimeString()) this.error('too many requests, please try again later') + } else { + this.error(e) } - this.error(e) } } } diff --git a/test/commands/app/init.test.js b/test/commands/app/init.test.js index 8751ade0..9e5f727e 100644 --- a/test/commands/app/init.test.js +++ b/test/commands/app/init.test.js @@ -236,6 +236,17 @@ describe('bad args/flags', () => { }) }) +describe('--project', () => { + test('no value', async () => { + command.argv = ['--project'] + await expect(command.run()).rejects.toThrow('Flag --project expects a value') + }) + test('non-existent', async () => { + command.argv = ['--project=non-existent'] + await expect(command.run()).rejects.toThrow('--project non-existent not found') + }) +}) + describe('--no-login', () => { test('select excshell, arg: /otherdir', async () => { const installOptions = { @@ -292,7 +303,7 @@ describe('--no-login', () => { test('--repo --no-login', async () => { const getContent = () => new Promise((resolve, reject) => { - resolve({ status: 302, data: [] }); + resolve({ status: 302, data: [] }) }) Octokit.mockImplementation(() => ({ repos: { getContent } })) @@ -305,21 +316,22 @@ describe('--no-login', () => { }) test('--repo --login', async () => { - const getContent = ({owner, repo, path}) => new Promise((resolve, reject) => { + const getContent = ({ owner, repo, path }) => new Promise((resolve, reject) => { // console.log('args = ', owner, repo, path) - if (path == 'src') { - resolve({ data: []}) + if (path === 'src') { + resolve({ data: [] }) + } else { + resolve({ + data: [{ + type: 'file', + path: '.gitignore', + download_url: 'https://raw.githubusercontent.com/adobe/appbuilder-quickstarts/master/qr-code/.gitignore' + }, { + type: 'dir', + path: 'src' + }] + }) } - else resolve({ - data:[{ - type: 'file', - path: '.gitignore', - download_url: 'https://raw.githubusercontent.com/adobe/appbuilder-quickstarts/master/qr-code/.gitignore' - },{ - type: 'dir', - path: 'src' - }] - }) }) Octokit.mockImplementation(() => ({ repos: { getContent } })) @@ -333,8 +345,10 @@ describe('--no-login', () => { test('--repo not valid 404', async () => { const getContent = () => new Promise((resolve, reject) => { - console.log('rejecting with 404') - reject({ status: 404, data: [] }); + // console.log('rejecting with 404') + const error = new Error('the error message is not checked, just the status code') + error.status = 404 + reject(error) }) Octokit.mockImplementation(() => ({ repos: { getContent } })) @@ -351,8 +365,10 @@ describe('--no-login', () => { test('--repo not reachable 403', async () => { const getContent = () => new Promise((resolve, reject) => { - console.log('rejecting with 403') - reject({ status: 403, data: [] }); + // console.log('rejecting with 403') + const error = new Error('the error message is not checked, just the status code') + error.status = 403 + reject(error) }) Octokit.mockImplementation(() => ({ repos: { getContent } })) From 37b8e595b6e9d18da03a5595f90da74dc1cd99e0 Mon Sep 17 00:00:00 2001 From: Jesse MacFadyen Date: Mon, 13 Nov 2023 11:06:33 -0800 Subject: [PATCH 10/11] hide org/project flags for this release --- src/commands/app/init.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 6cddd3c8..9cd58268 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -447,10 +447,12 @@ InitCommand.flags = { }), org: Flags.string({ description: 'Specify the Adobe Developer Console Org to init from', + hidden: true, exclusive: ['import'] // also no-login }), project: Flags.string({ description: 'Specify the Adobe Developer Console Project to init from', + hidden: true, exclusive: ['import'] // also no-login }), workspace: Flags.string({ From 91e4783b0eef06ffda994df800d3bebf72c58b2c Mon Sep 17 00:00:00 2001 From: Jesse MacFadyen Date: Mon, 13 Nov 2023 11:57:21 -0800 Subject: [PATCH 11/11] use ora for repo download status, test/mocks, todos --- src/commands/app/init.js | 18 ++++++++++++------ test/commands/app/init.test.js | 19 ++++++++++++++++++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/commands/app/init.js b/src/commands/app/init.js index 9cd58268..22cf325f 100644 --- a/src/commands/app/init.js +++ b/src/commands/app/init.js @@ -23,6 +23,7 @@ const hyperlinker = require('hyperlinker') const { importConsoleConfig } = require('../../lib/import') const { loadAndValidateConfigFile } = require('../../lib/import-helper') const { Octokit } = require('@octokit/rest') +const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:init', { provider: 'debug' }) const DEFAULT_WORKSPACE = 'Stage' @@ -370,6 +371,7 @@ class InitCommand extends TemplatesCommand { const octokit = new Octokit({ auth: '' }) + const spinner = ora('Downloading quickstart repo').start() /** @private */ async function downloadRepoDirRecursive (owner, repo, filePath, basePath) { const { data } = await octokit.repos.getContent({ owner, repo, path: filePath }) @@ -380,28 +382,32 @@ class InitCommand extends TemplatesCommand { await downloadRepoDirRecursive(owner, repo, fileOrDir.path, basePath) } else { // todo: use a spinner - console.log(`Downloading ${fileOrDir.path}`) + spinner.text = `Downloading ${fileOrDir.path}` const response = await fetch(fileOrDir.download_url) const jsonResponse = await response.text() fs.writeFileSync(path.relative(basePath, fileOrDir.path), jsonResponse) } } } - // todo: this fails past the first level deep, /// + // we need to handle n-deep paths, /// const [owner, repo, ...restOfPath] = fullRepo.split('/') const basePath = restOfPath.join('/') try { - await octokit.repos.getContent({ owner, repo, path: `${basePath}/app.config.yaml` }) + const response = await octokit.repos.getContent({ owner, repo, path: `${basePath}/app.config.yaml` }) + aioLogger.debug(`github headers: ${JSON.stringify(response.headers, 0, 2)}`) await downloadRepoDirRecursive(owner, repo, basePath, basePath) + spinner.succeed('Downloaded quickstart repo') } catch (e) { if (e.status === 404) { + spinner.fail('Quickstart repo not found') this.error('--repo does not point to a valid Adobe App Builder app') } if (e.status === 403) { - // todo: remove this, it is feature creep, but helpful for debugging + // This is helpful for debugging, but by default we don't show it // github rate limit is 60 requests per hour for unauthenticated users - // const resetTime = new Date(e.response.headers['x-ratelimit-reset'] * 1000) - // console.log('resetTime : ', resetTime.toLocaleTimeString()) + const resetTime = new Date(e.response.headers['x-ratelimit-reset'] * 1000) + aioLogger.debug(`too many requests, resetTime : ${resetTime.toLocaleTimeString()}`) + spinner.fail() this.error('too many requests, please try again later') } else { this.error(e) diff --git a/test/commands/app/init.test.js b/test/commands/app/init.test.js index 9e5f727e..95e75c07 100644 --- a/test/commands/app/init.test.js +++ b/test/commands/app/init.test.js @@ -28,6 +28,22 @@ jest.mock('inquirer', () => ({ createPromptModule: jest.fn() })) +// mock ora +jest.mock('ora', () => { + const mockOra = { + start: jest.fn(() => mockOra), + stop: jest.fn(() => mockOra), + succeed: jest.fn(() => mockOra), + fail: jest.fn(() => mockOra), + info: jest.fn(() => mockOra), + warn: jest.fn(() => mockOra), + stopAndPersist: jest.fn(() => mockOra), + clear: jest.fn(() => mockOra), + promise: jest.fn(() => Promise.resolve(mockOra)) + } + return jest.fn(() => mockOra) +}) + // mock login jest.mock('@adobe/aio-lib-ims') @@ -303,7 +319,7 @@ describe('--no-login', () => { test('--repo --no-login', async () => { const getContent = () => new Promise((resolve, reject) => { - resolve({ status: 302, data: [] }) + resolve({ headers: [], status: 302, data: [] }) }) Octokit.mockImplementation(() => ({ repos: { getContent } })) @@ -367,6 +383,7 @@ describe('--no-login', () => { const getContent = () => new Promise((resolve, reject) => { // console.log('rejecting with 403') const error = new Error('the error message is not checked, just the status code') + error.response = { headers: { 'x-ratelimit-reset': 99999999999 } } error.status = 403 reject(error) })