Skip to content

Commit

Permalink
Add support for quickstart apps (#679)
Browse files Browse the repository at this point in the history
* Add support for quickstart apps
* move quick-start to repo
* nit: download and install repo first
* use path.relative, specific error handling
* clean up, linting, coverage 100%
* use ora for repo download status, test/mocks, todos

---------

Co-authored-by: Jesse MacFadyen <[email protected]>
  • Loading branch information
moritzraho and purplecabbage authored Nov 13, 2023
1 parent 8863c0c commit 19c916e
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 36 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@adobe/generator-aio-app": "^6.0.0",
"@adobe/generator-app-common-lib": "^1.0.0",
"@adobe/inquirer-table-checkbox": "^1.2.0",
"@octokit/rest": "^19.0.11",
"@oclif/core": "^2.11.6",
"@parcel/core": "^2.7.0",
"@parcel/reporter-cli": "^2.7.0",
Expand Down
153 changes: 117 additions & 36 deletions src/commands/app/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ const generators = require('@adobe/generator-aio-app')
const TemplateRegistryAPI = require('@adobe/aio-lib-templates')
const inquirer = require('inquirer')
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'

Expand Down Expand Up @@ -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.repo) {
await this.withQuickstart(flags.repo)
} 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) {
Expand All @@ -128,40 +133,50 @@ 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()

// 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)

// 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.repo) {
// 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
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.repo) {
await this.runCodeGenerators(this.getInitialGenerators(flags), flags.yes, consoleConfig.project.name)
}

// 8. import config
await this.importConsoleConfig(consoleConfig, flags)

// 9. install templates
await this.installTemplates({
useDefaultValues: flags.yes,
installNpm: flags.install,
templates
})
if (!flags.repo) {
await this.installTemplates({
useDefaultValues: flags.yes,
installNpm: flags.install,
templates
})
}

this.log(chalk.blue(chalk.bold(`Project initialized for Workspace ${workspace.name}, you can run 'aio app use -w <workspace>' to switch workspace.`)))
}
Expand Down Expand Up @@ -275,21 +290,24 @@ 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) {
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)
Expand Down Expand Up @@ -348,7 +366,56 @@ class InitCommand extends TemplatesCommand {
}
)
}

async withQuickstart (fullRepo) {
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 })
for (const fileOrDir of data) {
if (fileOrDir.type === 'dir') {
const destDir = path.relative(basePath, fileOrDir.path)
fs.ensureDirSync(destDir)
await downloadRepoDirRecursive(owner, repo, fileOrDir.path, basePath)
} else {
// todo: use a spinner
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)
}
}
}
// we need to handle n-deep paths, <owner>/<repo>/<path>/<path>
const [owner, repo, ...restOfPath] = fullRepo.split('/')
const basePath = restOfPath.join('/')
try {
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) {
// 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)
aioLogger.debug(`too many requests, resetTime : ${resetTime.toLocaleTimeString()}`)
spinner.fail()
this.error('too many requests, please try again later')
} else {
this.error(e)
}
}
}
}

InitCommand.description = `Create a new Adobe I/O App
`

Expand All @@ -372,18 +439,28 @@ InitCommand.flags = {
description: 'Extension point(s) to implement',
char: 'e',
multiple: true,
exclusive: ['template']
exclusive: ['template', 'repo']
}),
'standalone-app': Flags.boolean({
description: 'Create a stand-alone application',
default: false,
exclusive: ['template']
exclusive: ['template', 'repo']
}),
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',
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({
description: 'Specify the Adobe Developer Console Workspace to init from, defaults to Stage',
default: DEFAULT_WORKSPACE,
Expand All @@ -394,6 +471,10 @@ InitCommand.flags = {
description: 'Skip and confirm prompt for creating a new workspace',
default: false
}),
repo: Flags.string({
description: 'Init from gh quick-start repo. Expected to be of the form <owner>/<repo>/<path>',
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
Expand Down
Loading

0 comments on commit 19c916e

Please sign in to comment.