diff --git a/README.md b/README.md index 8e303e8..e4572aa 100644 --- a/README.md +++ b/README.md @@ -12,45 +12,145 @@ Snapfu is the scaffolding command line tool for the Searchspring Snap SDK. This npm install -g snapfu ``` -## Login +## Usage -Login to access your Github organizations - the following command will open a browser window -to give snapfu access to your Github organizations and to be able to create repositories in subsequent steps. +```bash +snapfu [--options] +``` + +## Commands + +### `init` - Create a new snap project +Creates a new snap project (optional directory) + +```bash +snapfu init +``` + + +### `badges` - Badge template management +Manage badge templates for your project + +```bash +snapfu badges [--options] +``` + +**Subcommands:** +- `init` - Initialize badge template in current project +- `list [local | remote]` - Display list of badge templates (local or remote) +- `archive ` - Remove remote badge template + - `--secret-key ` - Secret key for authentication +- `sync [ | locations.json]` - Synchronize badge template and parameters with remote + - `--secret-key ` - Secret key for authentication + +### `recs` - Recommendation template management +Manage recommendation templates for your project + +```bash +snapfu recs [--options] +``` + +**Subcommands:** +- `init` - Initialize recommendation template in current project +- `list [local | remote]` - Display list of recommendation templates (local or remote) +- `archive ` - Remove remote recommendation template (optional branch) + - `--secret-key ` - Secret key for authentication +- `sync ` - Synchronize recommendation template and parameters with remote (optional branch) + - `--secret-key ` - Secret key for authentication + +### `secrets` - Project secret management +Manage secrets in your snap project + +```bash +snapfu secrets [--options] +``` + +**Subcommands:** +- `add` - Adds secrets to snap project +- `update` - Update secrets in snap project +- `verify` - Verify secrets in snap project + +### `patch` - Apply patches to update project +Apply patches to update your project + +```bash +snapfu patch [--options] +``` + +**Subcommands:** +- `apply` - Apply patch version (version or latest) +- `list` - List available versions for project +- `fetch` - Fetch latest versions of patches + +### `login` - OAuth with GitHub +OAuths with GitHub to retrieve additional scaffolds and create repositories when using the init command ```bash snapfu login ``` -## Init +### `logout` - Remove login credentials +Removes login credentials -Create your website with the init command. Init will gather some information about the kind -of Snap project you wish to create. You will need your `siteId` and `secretKey` from the SMC before you run this command. This command will: +```bash +snapfu logout +``` + +### `org-access` - Review organization access +Review and change organization access for the tool + +```bash +snapfu org-access +``` -- download scaffolding files -- create and initialize a repository in the Github organization you selected -- populate a Github secret with the provided `secretKey` +### `whoami` - Show current user +Shows the current user ```bash -snapfu init my-awesome-website +snapfu whoami ``` - +### `about` - Show versioning +Shows versioning information -## Run it +```bash +snapfu about +``` -Now you can run the project with your standard `npm` tooling. +### `help` - Display help text +Display help text (optional command) ```bash -cd my-awesome-website -npm install -npm run dev +snapfu help [] ``` -See the `package.json` for other npm commands. +## Getting Started + +1. **Install snapfu globally:** + ```bash + npm install -g snapfu + ``` + +2. **Login (optional):** + ```bash + snapfu login + ``` + +3. **Create a new project:** + ```bash + snapfu init my-awesome-website + ``` + +4. **Run the project:** + ```bash + cd my-awesome-website + npm install + npm run dev + ``` ## Deployment -This tool integrates with the Searchspring build and deploy process. In order to take advantage of this you must select searchspring-implementations as your organizaiton during init. +This tool integrates with the Searchspring build and deploy process. In order to take advantage of this you must have access to the `searchspring-implementations` Github organization and select it during init command. (Requires login & invitation to the organization upon request). The tool uses Github actions to copy files to our AWS S3 backed CDN (Cloudfront). @@ -69,17 +169,14 @@ https://snapui.searchspring.io//my-branch/bundle.js ## Deploying to other places -You can modify the file `deploy.yml` in your generated project under `my-awesome-website/.github/workflows/deploy.yml` -to complete different actions if you don't want to use the Searchspring build process or don't have access to it. +You can modify the file `deploy.yml` in your generated project under `my-awesome-website/.github/workflows/deploy.yml` to complete different actions if you don't want to use the Searchspring build process or don't have access to it. ### SCP - Deploy the built artifacts using `scp`. [https://github.com/marketplace/actions/scp-command-to-transfer-files](https://github.com/marketplace/actions/scp-command-to-transfer-files) ### Google Cloud - Deploy to GCP using `gcloud`. [https://github.com/marketplace/actions/setup-gcloud-environment](https://github.com/marketplace/actions/setup-gcloud-environment) ### SFTP - Deploy a built artifacts through SFTP. [https://github.com/marketplace/actions/sftp-deploy](https://github.com/marketplace/actions/sftp-deploy) + diff --git a/src/badges.js b/src/badges.js index 5abe954..9e75e5b 100644 --- a/src/badges.js +++ b/src/badges.js @@ -236,6 +236,7 @@ export async function listBadgeTemplates(options) { const list = async (secretKey, siteId = '', name = '') => { if (!secretKey) { console.log(chalk.red('Unable to list remote badge template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } const remoteTemplates = await new ConfigApi(secretKey, options).getBadgeTemplates({ siteId }); @@ -318,6 +319,7 @@ export async function removeBadgeTemplate(options) { if (!secretKey) { console.log(chalk.red('Unable to archive remote badge template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } const { message } = await new ConfigApi(secretKey, options).archiveBadgeTemplate({ payload, siteId }); @@ -767,6 +769,7 @@ export async function syncBadgeTemplate(options) { const sync = async (template, secretKey, siteId) => { if (!secretKey) { console.log(chalk.red('Unable to sync remote badge template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } // validate template against remote locations (locations get validated and synced first) @@ -824,6 +827,7 @@ export async function syncBadgeTemplate(options) { const syncLocations = async (secretKey, siteId) => { if (!secretKey) { console.log(chalk.red('Unable to sync remote badge locations due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } console.log(` synchronizing locations`); diff --git a/src/cli.js b/src/cli.js index 9484a19..f01e669 100644 --- a/src/cli.js +++ b/src/cli.js @@ -61,102 +61,95 @@ async function parseArgumentsIntoOptions(rawArgs) { let multipleSites = []; // drop out if not logged in for certain commands - const userCommands = ['init', 'badges', 'recs', 'recommendation', 'recommendations', 'secret', 'secrets', 'logout', 'whoami', 'org-access']; - const secretCommands = ['badges', 'recs', 'recommendation', 'recommendations', 'secret', 'secrets']; + const requiredLoginCommands = ['logout', 'whoami', 'org-access']; const templatesRestrictedCommands = ['recs', 'recommendation', 'recommendations']; const loggedIn = user && user.token; const secretOptions = args['--secrets-ci'] || secretKey; - if (userCommands.includes(command) && !(loggedIn || secretOptions || args['--ci'])) { - console.log(chalk.yellow(`Login is required. Please login.`)); - console.log(chalk.grey(`\n\tsnapfu login\n`)); + if (requiredLoginCommands.includes(command) && !(loggedIn || secretOptions || args['--ci'])) { + console.log(chalk.yellow(`Login is required when using the '${command}' command.`)); exit(1); } else if (context.project.distribution == 'SnapTemplates' && templatesRestrictedCommands.includes(command)) { console.log(chalk.yellow(`The '${command}' command is not supported when using SnapTemplates.`)); exit(0); - } else if (secretCommands.includes(command) && (loggedIn || secretOptions || args['--ci'])) { - const getSecretKeyFromCLI = (siteId) => { - try { - const secrets = JSON.parse(args['--secrets-ci']); - const secretKey = secrets[`WEBSITE_SECRET_KEY_${siteId.toUpperCase()}`]; - return secretKey; - } catch (e) { - return; - } - }; + } - if (context.searchspring && typeof context.searchspring.siteId === 'object') { - // searchsoring.siteId contains multiple sites + const getSecretKeyFromCLI = (siteId) => { + try { + const secrets = JSON.parse(args['--secrets-ci']); + const secretKey = secrets[`WEBSITE_SECRET_KEY_${siteId.toUpperCase()}`]; + return secretKey; + } catch (e) { + return; + } + }; - const siteIds = Object.keys(context.searchspring.siteId); - if (!siteIds || !siteIds.length) { - console.log(chalk.red('searchspring.siteId object in package.json is empty')); - exit(1); - } + if (context.searchspring && typeof context.searchspring.siteId === 'object') { + // searchsoring.siteId contains multiple sites - multipleSites = siteIds - .map((siteId) => { - try { - const { name } = context.searchspring.siteId[siteId]; - const secretKey = getSecretKeyFromCLI(siteId) || user.keys[siteId]; - - if (!secretKey) { - console.log(chalk.red(`Cannot find the secretKey for siteId '${siteId}'.`)); - console.log(chalk.bold.white(`Please run the following command:`)); - console.log(chalk.gray(`\tsnapfu secrets add\n`)); - } - if (!secretKey && args['--secrets-ci']) { - console.log( - chalk.red(`Could not find Github secret 'WEBSITE_SECRET_KEY_${siteId.toUpperCase()}' in 'secrets' input - It can be added by running 'snapfu secrets add' in the project's directory locally, - or added manual in the project's repository secrets. - The value can be obtained in the Searchspring Management Console. - Then ensure that you are providing 'secrets' when running the action. ie: - - jobs: - Publish: - runs-on: ubuntu-latest - name: Snap Action - steps: - - name: Checkout action - uses: actions/checkout@v2 - with: - repository: searchspring/snap-action - - name: Run @searchspring/snap-action - uses: ./ - with: - secrets: \${{ toJSON(secrets) }} - ... - `) - ); - } - - return { - siteId, - name, - secretKey, - }; - } catch (e) { - console.log(chalk.red('The searchspring.siteId object in package.json is invalid. Expected format:')); + const siteIds = Object.keys(context.searchspring.siteId); + if (!siteIds || !siteIds.length) { + console.log(chalk.red('searchspring.siteId object in package.json is empty')); + exit(1); + } + + multipleSites = siteIds + .map((siteId) => { + try { + const { name } = context.searchspring.siteId[siteId]; + const secretKey = getSecretKeyFromCLI(siteId) || user.keys[siteId]; + + if (!secretKey && args['--secrets-ci']) { console.log( - chalk.red(` - "searchspring": { - "siteId": { - "xxxxx1": { - "name": "site1.com.au" - }, - "xxxxx2": { - "name": "site2.hk" - } - }, - }`) + chalk.red(`Could not find Github secret 'WEBSITE_SECRET_KEY_${siteId.toUpperCase()}' in 'secrets' input +It can be added by running 'snapfu secrets add' in the project's directory locally, +or added manual in the project's repository secrets. +The value can be obtained in the Searchspring Management Console. +Then ensure that you are providing 'secrets' when running the action. ie: + +jobs: + Publish: + runs-on: ubuntu-latest + name: Snap Action + steps: + - name: Checkout action + uses: actions/checkout@v2 + with: + repository: searchspring/snap-action + - name: Run @searchspring/snap-action + uses: ./ + with: + secrets: \${{ toJSON(secrets) }} + ... +`) ); - exit(1); } - }) - .filter((site) => site.secretKey); + + return { + siteId, + name, + secretKey, + }; + } catch (e) { + console.log(chalk.red('The searchspring.siteId object in package.json is invalid. Expected format:')); + console.log( + chalk.red(` +"searchspring": { + "siteId": { + "xxxxx1": { + "name": "site1.com.au" + }, + "xxxxx2": { + "name": "site2.hk" } + }, +}`) + ); + exit(1); + } + }) + .filter((site) => site.secretKey); } let packageJSON = {}; diff --git a/src/help.js b/src/help.js index 2015021..580c18a 100644 --- a/src/help.js +++ b/src/help.js @@ -12,7 +12,7 @@ These are the snapfu commands used in various situations ${chalk.whiteBright('recs')} Recommendation template management ${chalk.whiteBright('secrets')} Project secret management ${chalk.whiteBright('patch')} Apply patches to update project - ${chalk.whiteBright('login')} Oauths with github + ${chalk.whiteBright('login')} Oauths with github to retrieve additional scaffolds and create repositories when using the init command ${chalk.whiteBright('logout')} Removes login credentials ${chalk.whiteBright('org-access')} Review and change organization access for the tool ${chalk.whiteBright('whoami')} Shows the current user diff --git a/src/init.js b/src/init.js index b7d98ba..ce2b0ef 100644 --- a/src/init.js +++ b/src/init.js @@ -1,6 +1,6 @@ import os from 'os'; import { exit, cwd } from 'process'; -import { readdirSync, readFileSync, existsSync, mkdirSync, promises as fs } from 'fs'; +import { readdirSync, existsSync, mkdirSync, promises as fs } from 'fs'; import path from 'path'; import chalk from 'chalk'; import { Octokit } from '@octokit/rest'; @@ -14,6 +14,7 @@ import { ConfigApi } from './services/ConfigApi.js'; import YAML from 'yaml'; export const DEFAULT_BRANCH = 'production'; +const TEMPLATES_SCAFFOLD_NAME_IDENTIFIER = '-templates'; export const createDir = (dir) => { return new Promise((resolutionFunc, rejectionFunc) => { @@ -35,6 +36,7 @@ export const createDir = (dir) => { export const init = async (options) => { try { const { user } = options; + const isLoggedIn = user && user.token; let dir; if (options.args.length === 1) { @@ -45,18 +47,21 @@ export const init = async (options) => { console.log(chalk.yellow(`A parameter was not provided to the init command. The current working directory will be initialized.`)); } - let octokit = new Octokit({ + const octokit = new Octokit({ auth: user.token, request: { fetch: fetch, }, }); - let orgs = await octokit.orgs.listForAuthenticatedUser().then(({ data }) => { - return data.map((org) => { - return org.login; + let orgs = []; + if (isLoggedIn) { + orgs = await octokit.orgs.listForAuthenticatedUser().then(({ data }) => { + return data.map((org) => { + return org.login; + }); }); - }); + } const fetchScaffoldRepos = async () => { // using search modifiers - https://docs.github.com/en/search-github/searching-on-github/searching-for-repositories @@ -87,133 +92,178 @@ export const init = async (options) => { const snapfuScaffoldRepos = await fetchScaffoldRepos(); if (!snapfuScaffoldRepos?.length) { - console.log(chalk.red('failed to fetch scaffolds...')); - } else { - let questions = [ - { - type: 'input', - name: 'name', - validate: (input) => { - return input && input.length > 0; - }, - message: 'Please choose the name of this repository:', - default: path.basename(dir), - }, - { - type: 'list', - name: 'framework', - message: "Please choose the framework you'd like to use:", - choices: ['preact'], - default: 'preact', - }, - ]; + console.log(chalk.red('Failed to fetch scaffolds. Please try again later or contact Athos support for assistance.')); + exit(1); + } - const answers1 = await inquirer.prompt(questions); + let questions1 = [ + { + type: 'list', + name: 'framework', + message: "Please choose the framework you'd like to use:", + choices: ['preact'], + default: 'preact', + }, + { + type: 'list', + name: 'distribution', + message: 'Would you like to use Snap or Snap Templates?', + when: () => { + // TODO: remove when templates scaffolds exist + return snapfuScaffoldRepos.some((repo) => repo.name.includes(TEMPLATES_SCAFFOLD_NAME_IDENTIFIER)); + }, + choices: ['Snap', 'Snap Templates'], + default: 'Snap', + }, + ]; - // filter out repos that apply to the framework - const scaffoldRepos = snapfuScaffoldRepos - .filter((repo) => repo.name.startsWith(`snapfu-scaffold-${answers1.framework}`)) - .map((repo) => repo.full_name) - .sort(); + const answers1 = await inquirer.prompt(questions1); - const scaffolds = {}; - // map repos - scaffoldRepos.forEach((repository) => { - const [owner, repo] = repository.split('/'); + // filter scaffolds based on framework and distribution + let availableScaffolds = snapfuScaffoldRepos.filter((repo) => repo.name.startsWith(`snapfu-scaffold-${answers1.framework}`)); + if (answers1.distribution === 'Snap Templates') { + availableScaffolds = snapfuScaffoldRepos.filter((repo) => repo.name.includes(TEMPLATES_SCAFFOLD_NAME_IDENTIFIER)); + } else { + availableScaffolds = snapfuScaffoldRepos.filter((repo) => !repo.name.includes(TEMPLATES_SCAFFOLD_NAME_IDENTIFIER)); + } - scaffolds[repository] = { + const scaffolds = availableScaffolds + .map((repo) => repo.full_name) + .sort() + .reduce((acc, fullName) => { + const [owner, repo] = fullName.split('/'); + acc[repo] = { repo, owner, - ssh: `git@github.com:${repository}.git`, - http: `https://github.com/${repository}`, + ssh: `git@github.com:${fullName}.git`, + http: `https://github.com/${fullName}`, }; + return acc; + }, {}); + + // add additional custom scaffolds (snapfu.json advanced user) + if (isLoggedIn && user?.settings?.scaffolds?.repositories?.length) { + // add separator for clear delimiting + const capitalizedFramework = answers1.framework.charAt(0).toUpperCase() + answers1.framework.slice(1); + + availableScaffolds.unshift(new inquirer.Separator(`Snapfu ${capitalizedFramework} Scaffolds`)); + availableScaffolds.push(new inquirer.Separator('Custom Scaffolds')); + + // loop through custom repos and add to scaffoldRepos list and scaffolds mapping + user.settings.scaffolds.repositories.forEach((url) => { + // supporting HTTP only list for now + const split = url.split('/'); + + if (split.length > 2) { + const repo = split[split.length - 1]; + const owner = split[split.length - 2]; + const repository = `${owner}/${repo}`; + + availableScaffolds.push(repository); + scaffolds[repository] = { + repo, + owner, + ssh: `git@github.com:${repository}.git`, + http: url, + }; + } }); + } - if (user?.settings?.scaffolds?.repositories?.length) { - // add separator for clear delimiting - const capitalizedFramework = answers1.framework.charAt(0).toUpperCase() + answers1.framework.slice(1); - - scaffoldRepos.unshift(new inquirer.Separator(`Snapfu ${capitalizedFramework} Scaffolds`)); - scaffoldRepos.push(new inquirer.Separator('Custom Scaffolds')); - - // loop through custom repos and add to scaffoldRepos list and scaffolds mapping - user.settings.scaffolds.repositories.forEach((url) => { - // supporting HTTP only list for now - const split = url.split('/'); - - if (split.length > 2) { - const repo = split[split.length - 1]; - const owner = split[split.length - 2]; - const repository = `${owner}/${repo}`; - - scaffoldRepos.push(repository); + const questions2 = [ + { + type: 'list', + name: 'scaffold', + message: "Please choose the scaffold you'd like to use:", + choices: availableScaffolds, + default: `snapfu-scaffold-${answers1.framework}`, + }, + ]; - scaffolds[repository] = { - repo, - owner, - ssh: `git@github.com:${repository}.git`, - http: url, - }; - } - }); - } + const answers2 = await inquirer.prompt(questions2); - const questions2 = [ - { - type: 'list', - name: 'scaffold', - message: "Please choose the scaffold you'd like to use:", - choices: scaffoldRepos, - default: `snapfu-scaffold-${answers1.framework}`, - }, - ]; + const scaffold = scaffolds[answers2.scaffold]; + if (!scaffold) { + console.log(chalk.red(`Failed to find the selected scaffold ${answers2.scaffold}...\n Please report this issue to Athos support.`)); + exit(1); + } - const answers2 = await inquirer.prompt(questions2); + // fetch scaffold configuration if available + try { + const contentResponse = await octokit.rest.repos.getContent({ + owner: scaffold.owner, + repo: scaffold.repo, + path: 'snapfu.config.yml', + }); - // scaffold reference - const scaffold = scaffolds[answers2.scaffold]; - if (!scaffold) { - console.log(chalk.red(`Failed to find the selected scaffold ${answers2.scaffold}...\n`)); + try { + const buffer = new Buffer.from(contentResponse.data.content, 'base64'); + const fileContents = buffer.toString('ascii'); + scaffold.advanced = YAML.parse(fileContents); + } catch (err) { + console.log(chalk.red(`Failed to parse snapfu.config.yml contents...\n Please report this issue to Athos support.`)); exit(1); } + } catch { + // do nothing - no advanced configuration available + } - try { - const contentResponse = await octokit.rest.repos.getContent({ - owner: scaffold.owner, - repo: scaffold.repo, - path: 'snapfu.config.yml', - }); - - try { - const buffer = new Buffer.from(contentResponse.data.content, 'base64'); - const fileContents = buffer.toString('ascii'); - scaffold.advanced = YAML.parse(fileContents); - } catch (err) { - console.log(chalk.red(`Failed to parse snapfu.config.yml contents...\n`)); - exit(1); + // ask follow up scaffold questions (if they exist) + if (scaffold.advanced?.variables?.length) { + let advancedQuestions = []; + scaffold.advanced.variables.forEach((variable) => { + if (variable.name && variable.type && variable.message) { + // remove the value + delete variable.value; + advancedQuestions.push(variable); } - } catch (err) { - if (err.status !== 404) { - console.log(chalk.red(`Failed to fetch snapfu.config.yml...\n`)); - exit(1); - } - } + }); - // ask additional questions (for advanced scaffolds) - if (scaffold.advanced?.variables?.length) { - let advancedQuestions = []; - scaffold.advanced.variables.forEach((variable) => { - if (variable.name && variable.type && variable.message) { - // remove the value - delete variable.value; - advancedQuestions.push(variable); - } - }); + scaffold.answers = await inquirer.prompt(advancedQuestions); + } - scaffold.answers = await inquirer.prompt(advancedQuestions); - } + // ask for siteId (as this is used as a snapfu variable when copying over scaffold) + const questions3 = [ + { + type: 'input', + name: 'siteId', + message: 'Please enter the siteId as found in the SMC console (a1b2c3):', + validate: (input) => { + return input && input.length > 0 && /^[0-9a-z]{6}$/.test(input); + }, + }, + ]; + const answers3 = await inquirer.prompt(questions3); + + let useGitHubRepo = false; + let repositoryAnswers = {}; + + const githubQuestions = [ + { + type: 'confirm', + name: 'useGitHubRepo', + message: 'Would you like to create a GitHub repository?', + when: () => { + return isLoggedIn; + }, + default: true, + }, + ]; + + const githubAnswers = await inquirer.prompt(githubQuestions); + useGitHubRepo = githubAnswers.useGitHubRepo; - const questions3 = [ + if (useGitHubRepo) { + const repoQuestions = [ + { + type: 'input', + name: 'name', + validate: (input) => { + return input && input.length > 0; + }, + message: 'Please choose the name of this repository:', + default: path.basename(dir), + }, { type: 'list', name: 'organization', @@ -224,29 +274,32 @@ export const init = async (options) => { return orgs && orgs.length > 0; }, }, - { - type: 'input', - name: 'siteId', - message: 'Please enter the siteId as found in the SMC console (a1b2c3):', - validate: (input) => { - return input && input.length > 0 && /^[0-9a-z]{6}$/.test(input); - }, - }, - { - type: 'input', - name: 'secretKey', - message: 'Please enter the secretKey as found in the SMC console (32 characters):', - validate: (input) => { - return input && input.length > 0 && /^[0-9a-zA-Z]{32}$/.test(input); - }, - }, ]; - const answers3 = await inquirer.prompt(questions3); - // combined answers - const answers = { ...answers1, ...answers2, ...answers3 }; + repositoryAnswers = await inquirer.prompt(repoQuestions); + + // Ask for secretKey if org is implementations + if (repositoryAnswers.organization === 'searchspring-implementations') { + const secretQuestions = [ + { + type: 'input', + name: 'secretKey', + message: 'Please enter the secretKey as found in the SMC console (32 characters):', + validate: (input) => { + return input && input.length > 0 && /^[0-9a-zA-Z]{32}$/.test(input); + }, + }, + ]; + const secretAnswers = await inquirer.prompt(secretQuestions); + repositoryAnswers.secretKey = secretAnswers.secretKey; + } + } + + // combined answers + const answers = { ...answers1, ...answers2, ...answers3, ...repositoryAnswers }; - // validate siteId and secretKey + // validate siteId and secretKey + if (answers.secretKey) { try { await new ConfigApi(answers.secretKey, options).validateSite({ siteId: answers.siteId }); } catch (err) { @@ -254,10 +307,12 @@ export const init = async (options) => { console.log(chalk.red(err)); exit(1); } + } - // create local directory - let folderName = await createDir(dir); + // create local directory + let folderName = await createDir(dir); + if (useGitHubRepo) { // set organization to user.login when answer is undefined (question never asked) answers.organization = answers.organization || user.login; @@ -292,7 +347,7 @@ export const init = async (options) => { if (default_branch !== DEFAULT_BRANCH) { console.log(`Renaming default branch ${chalk.cyan(default_branch)} to ${chalk.cyan(DEFAULT_BRANCH)}\n`); - const response2 = await octokit.repos.renameBranch({ + await octokit.repos.renameBranch({ owner: answers.organization, repo: answers.name, branch: default_branch, @@ -345,73 +400,92 @@ export const init = async (options) => { console.log(); } } + } + if (useGitHubRepo && isLoggedIn && !options.dev) { // newly create repo URLs + const creationMethod = answers.organization == user.login ? 'createForAuthenticatedUser' : 'createInOrg'; const repoUrlSSH = `git@github.com:${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}.git`; const repoUrlHTTP = `https://${user.login}@github.com/${creationMethod == 'createInOrg' ? answers.organization : user.login}/${answers.name}`; - if (!options.dev) { - try { - console.log(`Cloning repository via SSH...`); - await cloneAndCopyRepo(repoUrlSSH, dir, false); - console.log(`${chalk.cyan(repoUrlSSH)}\n`); - } catch (err) { - console.log(`SSH authentication failed. Cloning repository via HTTPS...`); - await cloneAndCopyRepo(repoUrlHTTP, dir, false); - console.log(`${chalk.cyan(repoUrlHTTP)}\n`); - } - } - - const scaffoldVariables = { - 'snapfu.name': answers.name, - 'snapfu.siteId': answers.siteId, - 'snapfu.author': user.name, - 'snapfu.framework': answers.framework, - }; - - // add advanced scaffold variables - if (scaffold.answers) { - Object.keys(scaffold.answers).forEach((key) => { - const value = scaffold.answers[key]; - scaffoldVariables[`snapfu.variables.${key}`] = value; - }); - } - try { - console.log(`Cloning scaffolding into ${dir} via SSH...`); - await cloneAndCopyRepo(scaffold.ssh, dir, true, scaffoldVariables); - console.log(`${chalk.cyan(scaffold.ssh)}\n`); + console.log(`Cloning repository via SSH...`); + await cloneAndCopyRepo(repoUrlSSH, dir, false); + console.log(`${chalk.cyan(repoUrlSSH)}\n`); } catch (err) { - console.log(`SSH authentication failed. Cloning scaffolding into ${dir} via HTTPS...`); - await cloneAndCopyRepo(scaffold.http, dir, true, scaffoldVariables); - console.log(`${chalk.cyan(scaffold.http)}\n`); + console.log(`SSH authentication failed. Cloning repository via HTTPS...`); + await cloneAndCopyRepo(repoUrlHTTP, dir, false); + console.log(`${chalk.cyan(repoUrlHTTP)}\n`); } + } - // waiting here due to copyPromise function resolving before scaffold is actually copied - // TODO: look into why ncp does not like our filtering (does not resolve promise in callback) - // wait... - await wait(1000); + const scaffoldVariables = { + 'snapfu.name': answers.name || path.basename(dir), + 'snapfu.siteId': answers.siteId, + 'snapfu.author': user?.name || user.login || 'Unknown', + 'snapfu.framework': answers.framework, + }; - // save secretKey mapping to creds.json - await auth.saveSecretKey(answers.secretKey, answers.siteId, options.config.searchspringDir); - await setRepoSecret(options, { - siteId: answers.siteId, - secretKey: answers.secretKey, - organization: answers.organization, - name: answers.name, - dir, + // add advanced scaffold variables + if (scaffold.answers) { + Object.keys(scaffold.answers).forEach((key) => { + const value = scaffold.answers[key]; + scaffoldVariables[`snapfu.variables.${key}`] = value; }); + } + + try { + console.log(`Cloning scaffolding into ${dir} via SSH...`); + await cloneAndCopyRepo(scaffold.ssh, dir, true, scaffoldVariables); + console.log(`${chalk.cyan(scaffold.ssh)}\n`); + } catch (err) { + console.log(`SSH authentication failed. Cloning scaffolding into ${dir} via HTTPS...`); + await cloneAndCopyRepo(scaffold.http, dir, true, scaffoldVariables); + console.log(`${chalk.cyan(scaffold.http)}\n`); + } + + // waiting here due to copyPromise function resolving before scaffold is actually copied + // TODO: look into why ncp does not like our filtering (does not resolve promise in callback) + // wait... + await wait(1000); + + // save secretKey mapping to creds.json + if (answers.secretKey) { + await auth.saveSecretKey(answers.secretKey, answers.siteId, options.config.searchspringDir); + } + + // Set repository secret and branch protection + if (useGitHubRepo && isLoggedIn) { + if (answers.secretKey) { + await setRepoSecret(options, { + siteId: answers.siteId, + secretKey: answers.secretKey, + organization: answers.organization, + name: answers.name, + dir, + }); + } await setBranchProtection(options, { organization: answers.organization, name: answers.name }); + } - if (dir != cwd()) { - console.log(`The ${chalk.blue(folderName)} directory has been created and initialized from ${chalk.blue(`${answers.scaffold}`)}.`); + if (dir != cwd()) { + console.log(`The ${chalk.blue(folderName)} directory has been created and initialized from ${chalk.blue(`${answers.scaffold}`)}.`); + if (useGitHubRepo) { console.log(`Get started by installing package dependencies and creating a branch:`); console.log(chalk.grey(`\n\tcd ${folderName} && npm install && git checkout -b development\n`)); } else { - console.log(`Current working directory has been initialized from ${chalk.blue(`${answers.scaffold}`)}.`); + console.log(`Get started by installing package dependencies:`); + console.log(chalk.grey(`\n\tcd ${folderName} && npm install\n`)); + } + } else { + console.log(`Current working directory has been initialized from ${chalk.blue(`${answers.scaffold}`)}.`); + if (useGitHubRepo) { console.log(`Get started by installing package dependencies and creating a branch:`); console.log(chalk.grey(`\n\tnpm install && git checkout -b development\n`)); + } else { + console.log(`Get started by installing package dependencies:`); + console.log(chalk.grey(`\n\tnpm install\n`)); } } } catch (err) { @@ -432,7 +506,7 @@ export const setBranchProtection = async function (options, details) { const { organization, name } = details; - if (!options.dev && organization && name) { + if (!options.dev && organization === 'searchspring-implementations' && name) { console.log(`Setting branch protection for ${DEFAULT_BRANCH} in ${organization}/${name}...`); try { @@ -496,8 +570,6 @@ export const setBranchProtection = async function (options, details) { console.log(chalk.red(`failed to create branch protection rule`)); console.log(chalk.red(err)); } - } else { - console.log(chalk.yellow('skipping creation of branch protection')); } console.log(); // new line spacing }; diff --git a/src/recs.js b/src/recs.js index a93e8f6..c00629e 100644 --- a/src/recs.js +++ b/src/recs.js @@ -267,7 +267,7 @@ export async function listTemplates(options) { console.log(` ${chalk.white(`${type.charAt(0).toUpperCase() + type.slice(1)} Templates`)}`); // loop through each template and log details typeTemplates.map((template) => { - console.log(` ${chalk.green(template.details.name)} ${chalk.blue(`[${repository.branch}]`)}`); + console.log(` ${chalk.green(template.details.name)} ${repository.branch ? chalk.blue(`[${repository.branch}]`) : ''}`); }); } }); @@ -284,6 +284,7 @@ export async function listTemplates(options) { const list = async (secretKey, siteId = '', name = '') => { if (!secretKey) { console.log(chalk.red('Unable to list remote template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } const remoteTemplates = await new ConfigApi(secretKey, options).getTemplates({ siteId }); @@ -362,6 +363,7 @@ export async function removeTemplate(options) { if (!secretKey) { console.log(chalk.red('Unable to archive remote template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } await new ConfigApi(secretKey, options).archiveTemplate({ payload, siteId }); @@ -463,6 +465,7 @@ ${invalidParam} const sync = async (template, secretKey, siteId) => { if (!secretKey) { console.log(chalk.red('Unable to sync remote template due to missing secretKey')); + console.log(chalk.grey(`\n\tsnapfu secrets add\n`)); return; } const payload = buildTemplatePayload(template.details, { branch: branchName, framework: searchspring.framework });