@installdoc/ansible-gas-station
Version:
An Ansible playbook that provisions your network with software from GitHub Awesome lists, developed with disaster recovery in mind ⛽🔥🤤
417 lines (384 loc) • 12.3 kB
JavaScript
/* eslint-disable no-negated-condition */
import chalk from 'chalk'
import inquirer from 'inquirer'
import { execSync } from 'node:child_process'
import signale from 'signale'
import { decorateFiles } from './lib/decorate-files.js'
import { logInstructions, LOG_DECORATOR_REGEX } from './lib/log.js'
const DECORATION_LENGTH = 2
/**
* Writes to package.json with jq
*
* @param {string} value - The value being written
* @param {string} location - The jq key of package.json being written to
* @returns {void}
*/
function writeField(value, location) {
execSync(
`TMP="$(mktemp)" && jq --arg field "${value}" '.blueprint.${location} = $field' package.json` +
' > "$TMP" && mv "$TMP" package.json',
{
stdio: 'inherit'
}
)
}
/**
* Prompts for the name of the project
*
* @returns {*} Void
*/
async function promptForName() {
const currentName = execSync(`jq -r '.blueprint.name' package.json`).toString().trimEnd()
if (currentName !== 'null') {
signale.info('The `name` has already been populated')
} else {
logInstructions(
'Project Name',
'The project name should be a short, capitalized title for the project. The name is used' +
' for the GitLab project title, references to the project in documentation,' +
' and as a title for the README.md when the blueprint.title is not specified.'
)
const response = await inquirer.prompt([
{
message: 'Enter a short, descriptive name for the project:',
name: 'name',
type: 'input'
}
])
writeField(response.name, 'name')
signale.success('Added `name` to package.json')
}
}
/**
* Prompts for the title of the project
*
* @returns {*} Void
*/
async function promptForTitle() {
const currentTitle = execSync(`jq -r '.blueprint.title' package.json`).toString().trimEnd()
if (currentTitle !== 'null') {
signale.info('The `title` has already been populated')
} else {
logInstructions(
'Project Title',
'The project title is a longer version of the project name. It is used as the title header of the README.md.'
)
const response = await inquirer.prompt([
{
message: 'Enter a catchy title for the project:',
name: 'title',
type: 'input'
}
])
writeField(response.title, 'title')
signale.success('Added `title` to package.json')
}
}
/**
* Prompts for a brief description of the project
*
* @returns {*} Void
*/
async function promptForDescription() {
const currentDesc = execSync(`jq -r '.blueprint.description' package.json`).toString().trimEnd()
if (currentDesc !== 'null') {
signale.info('The `description` has already been populated')
} else {
logInstructions(
'Description Instructions',
`${
'The brief description should describe the project in as few words as possible while' +
' still giving users enough information to know exactly what the project is all about.' +
' The description should make sense when placed in the following contexts:\n\n'
}${chalk.white('●')} A project that {{ description }}\n${chalk.white(
'●'
)} This repository is home to a project that {{ description }}`
)
const response = await inquirer.prompt([
{
message: 'Enter a brief description of the project (no more than 100 characters):',
name: 'description',
type: 'input'
}
])
writeField(response.description, 'description')
signale.success('Added `description` to package.json')
}
}
/**
* Asks what group the project belongs to
*
* @param {string} gitUrl - The GitLab URL
* @returns {string} The group
*/
// eslint-disable-next-line max-statements, require-jsdoc
async function promptForGroup(gitUrl) {
const currentGroup = execSync(`jq -r '.blueprint.group' package.json`).toString().trimEnd()
if (currentGroup !== 'null') {
signale.info('The `group` has already been populated')
return currentGroup
}
const guesses = {
angular: '/apps/',
ansible: '/ansible-roles/',
docker: '/docker/',
go: '/go/',
npm: '/npm/',
packer: '/packer/',
python: '/python/'
}
const guess = Object.entries(guesses)
.map((value) => (gitUrl.includes(value[1]) ? value[0] : false))
.find((exists) => exists)
if (guess) {
// eslint-disable-next-line security/detect-object-injection
signale.info(`Setting group to \`${guess}\` because the GitLab URL contained \`${guesses[guess]}\``)
writeField(guess, 'group')
return guess
}
const choices = ['Angular', 'Ansible', 'Docker', 'Go', 'Node.js', 'Packer', 'Python', 'Other']
const decoratedChoices = choices.map((choice) => decorateFiles(choice))
const response = await inquirer.prompt([
{
choices: decoratedChoices,
message: 'What group does this project belong to?',
name: 'group',
type: 'list'
}
])
const groupValue = response.group
.replace('Node.js', 'npm')
.replace(LOG_DECORATOR_REGEX, '')
.toLowerCase()
.slice(DECORATION_LENGTH)
.replace(' ', '-')
writeField(groupValue, 'group')
signale.success('Added `group` to package.json')
return groupValue
}
const subgroups = {
angular: {
app: '/apps/',
website: '/website/'
},
ansible: {
playbook: '/non-existant-currently/',
role: '/ansible-roles/'
},
docker: {
'ansible-molecule': '/ansible-molecule/',
app: '/docker/app/',
'ci-pipeline': '/ci-pipeline/',
'docker-compose': '/docker-compose/',
software: '/docker/software/'
},
go: {
cli: '/go/cli/',
library: '/go/library/'
},
npm: {
app: '/npm/app/',
cli: '/npm/cli/',
config: '/npm/config/',
library: '/npm/library/',
plugin: '/npm/plugin/'
},
packer: {
desktop: 'desktop',
server: 'server'
},
python: {
cli: '/python/cli/',
library: '/python/library/'
}
}
const choiceOptions = {
angular: ['Application', 'Website', 'Other'],
ansible: ['Playbook', 'Role', 'Other'],
docker: ['Ansible Molecule', 'Application', 'CI Pipeline', 'Docker Compose', 'Software', 'Other'],
go: ['CLI', 'Library', 'Other'],
npm: ['Application', 'CLI', 'Configuration', 'Library', 'Plugin', 'Other'],
packer: ['Desktop', 'Server', 'Other'],
python: ['CLI', 'Library', 'Other']
}
/**
* Asks what subgroup the project belongs to
*
* @param {string} gitUrl - The GitLab URL
* @param {string} group - The project's group
* @returns {string} The subgroup
*/
// eslint-disable-next-line max-statements, require-jsdoc
async function promptForSubgroup(gitUrl, group) {
const currentSubgroup = execSync(`jq -r '.blueprint.subgroup' package.json`).toString().trimEnd()
if (currentSubgroup !== 'null') {
signale.info('The `subgroup` has already been populated')
return currentSubgroup
}
if (group === 'other') {
return 'other'
}
// eslint-disable-next-line security/detect-object-injection
const guesses = subgroups[group] ? subgroups[group] : {}
const guess = Object.entries(guesses)
.map((value) => (gitUrl.includes(value[1]) ? value[0] : false))
.find((exists) => exists)
if (guess) {
// eslint-disable-next-line security/detect-object-injection
signale.info(`Setting subgroup to \`${guess}\` because the GitLab URL contained \`${guesses[guess]}\``)
writeField(guess, 'subgroup')
return guess
}
// eslint-disable-next-line security/detect-object-injection
const choices = choiceOptions[group]
const decoratedChoices = choices.map((choice) => decorateFiles(choice))
const response = await inquirer.prompt([
{
choices: decoratedChoices,
message: 'What sub-group does this project belong to?',
name: 'subgroup',
type: 'list'
}
])
const subgroupValue = response.subgroup
.replace('Application', 'app')
.replace('Configuration', 'config')
.replace(LOG_DECORATOR_REGEX, '')
.toLowerCase()
.slice(DECORATION_LENGTH)
.replace(' ', '-')
signale.success('Added `subgroup` to package.json')
writeField(subgroupValue, 'subgroup')
return subgroupValue
}
/**
* Prompts for the GitHub repository
*
* @returns {string} The GitHub repository
*/
async function githubPrompt() {
const githubRepo = execSync(`jq -r '.blueprint.repository.github' package.json`).toString().trimEnd()
if (githubRepo !== 'null') {
signale.info('The GitHub repository URL in the blueprint data is already present')
return githubRepo
}
const response = await inquirer.prompt([
{
message:
'What is planned GitHub repository HTTPS address' +
' (e.g. https://github.com/ProfessorManhattan/ansible-androidstudio)?',
name: 'github',
type: 'input'
}
])
writeField(response.github, 'repository.github')
signale.success('Added `repository.github` to package.json')
return response.github
}
/**
* Prompts for the GitLab repository
*
* @returns {string} The GitLab repository
*/
async function gitlabPrompt() {
const gitlabRepo = execSync(`jq -r '.blueprint.repository.gitlab' package.json`).toString().trimEnd()
if (gitlabRepo !== 'null') {
signale.info('The GitLab repository URL in the blueprint data is already present')
return gitlabRepo
}
const response = await inquirer.prompt([
{
message: 'What is planned GitLab repository HTTPS address (e.g. https://gitlab.com/megabyte-labs/gas-station)?',
name: 'gitlab',
type: 'input'
}
])
writeField(response.gitlab, 'repository.gitlab')
signale.success('Added `repository.gitlab` to package.json')
return response.gitlab
}
/**
* This step acquires the git repositories by first checking `git remote get-url origin` and
* then prompting the user for missing information
*
* @returns {*} An object containing the GitLab and GitHub repositories
*/
// eslint-disable-next-line max-statements, require-jsdoc
async function getGitRepositories() {
// eslint-disable-next-line functional/no-try-statement
try {
const gitOrigin = execSync(`git remote get-url origin`).toString().trimEnd()
if (gitOrigin.includes('gitlab.com')) {
signale.info('Detected GitLab address automatically')
const github = await githubPrompt()
return {
github,
gitlab: gitOrigin
.replace('git@gitlab', 'https://gitlab')
.replace('gitlab.com:', 'gitlab.com/')
.replace('.git', '')
}
} else if (gitOrigin.includes('github.com')) {
signale.info('Detected GitHub address automatically')
const gitlab = await gitlabPrompt()
return {
github: gitOrigin
.replace('git@github', 'https://gitlab')
.replace('github.com:', 'github.com/')
.replace('.git', ''),
gitlab
}
}
// eslint-disable-next-line functional/no-throw-statement
throw Error
} catch {
const gitlab = await gitlabPrompt()
const github = await githubPrompt()
return {
github,
gitlab
}
}
}
/**
* Open editor where user can add markdown for the overview.
*
* @returns {*} Void
*/
async function promptForOverview() {
const currentOverview = execSync(`jq -r '.blueprint.overview' package.json`).toString().trimEnd()
if (currentOverview !== 'null') {
signale.info('The `overview` has already been populated')
} else {
const response = await inquirer.prompt([
{
message: 'Enter an overview for the project',
name: 'overview',
type: 'editor'
}
])
writeField(response.overview, 'overview')
signale.success('Added `overview` to package.json')
}
}
/**
* Main script logic
*/
// eslint-disable-next-line require-jsdoc
async function run() {
logInstructions(
'Package Initialization',
'Provide answers to the following prompts to initialize the project. Some parts of the build process' +
' are dependent on some of the answers, so it is important to answer the questions.'
)
await promptForName()
await promptForTitle()
await promptForDescription()
const gits = await getGitRepositories()
const group = await promptForGroup(gits.gitlab)
await promptForSubgroup(gits.gitlab, group)
await promptForOverview()
const slug = gits.gitlab.split('/').at(-1)
writeField(slug, 'slug')
}
run()