diff --git a/.npmignore b/.npmignore index 82d81fb..6333ce3 100644 --- a/.npmignore +++ b/.npmignore @@ -8,4 +8,5 @@ src tsconfig.json tslint.json docs -.github \ No newline at end of file +.github +scripts diff --git a/bin/actions-toolkit.js b/bin/actions-toolkit.js deleted file mode 100755 index 45f0ec7..0000000 --- a/bin/actions-toolkit.js +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs') -const path = require('path') - -const name = process.argv[2] - -if (!name) { - console.log(`\nUsage: npx actions-toolkit `) - process.exit(1) -} -const base = path.join(process.cwd(), name) - -try { - console.log(`Creating folder ${base}...`) - fs.mkdirSync(base) -} catch (err) { - if (err.code === 'EEXIST') { - console.error(`Folder ${base} already exists!`) - } else { - console.error(err.code) - } - process.exit(1) -} - -const { version } = require('../package.json') -const packageJson = { - name, - private: true, - main: 'index.js', - dependencies: { - 'actions-toolkit': `^${version}` - } -} - -const templateDir = path.join(__dirname, 'template') - -console.log('Creating package.json...') -fs.writeFileSync(path.join(base, 'package.json'), JSON.stringify(packageJson, null, 2)) - -fs.readdirSync(templateDir).forEach(file => { - const contents = fs.readFileSync(path.join(templateDir, file)) - console.log(`Creating ${file}...`) - fs.writeFileSync(path.join(base, file), contents) -}) - -console.log(`\nDone! Enjoy building your GitHub Action! Get started with:\n\ncd ${name} && npm install`) diff --git a/bin/cli.js b/bin/cli.js new file mode 100755 index 0000000..12e5eac --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node + +const createAction = require('./create-action') + +createAction(process.argv.slice(2)) + .then(() => { + process.exit(0) + }) + .catch(error => { + console.error(error) + process.exit(1) + }) diff --git a/bin/colors.json b/bin/colors.json new file mode 100644 index 0000000..a35eec7 --- /dev/null +++ b/bin/colors.json @@ -0,0 +1,10 @@ +[ + "white", + "yellow", + "blue", + "green", + "orange", + "red", + "purple", + "gray-dark" +] diff --git a/bin/create-action.js b/bin/create-action.js new file mode 100755 index 0000000..dda2882 --- /dev/null +++ b/bin/create-action.js @@ -0,0 +1,169 @@ +const fs = require('fs') +const path = require('path') +const { promisify } = require('util') +const minimist = require('minimist') +const { Signale } = require('signale') +const { prompt } = require('enquirer') +const colors = require('./colors.json') +const icons = require('./feather-icons.json') + +const readFile = promisify(fs.readFile) +const writeFile = promisify(fs.writeFile) +const mkdir = promisify(fs.mkdir) + +/** + * Reads a template file from disk. + * + * @param {string} filename The template filename to read. + * @returns {Promise} The template file string contents. + */ +async function readTemplate (filename) { + const templateDir = path.join(__dirname, 'template') + return readFile(path.join(templateDir, filename), 'utf8') +} + +/** + * A predicate function to ensure a string is not empty. + * + * @param {string} value The string value. + * @returns {boolean} Whether the string is empty or not. + */ +const isNotEmpty = value => value.length > 0 + +/** + * The options object returned from the CLI questionnaire prompt. + * @typedef {object} PromptAnswers + * @property {string} name The action name. + * @property {string} description The action description. + * @property {string} icon The feather icon name. See `bin/feather-icons.json` for options. + * @property {string} color The GitHub Action color. See `bin/colors.json` for options. + */ + +/** + * Prompts the user with a questionnaire to get key metadata for the GitHub Action. + * + * @returns {Promise} An object containing prompt answers. + */ +async function getActionMetadata () { + return prompt([ + { + type: 'input', + name: 'name', + message: 'What is the name of your action?', + initial: 'Your action name', + validate: isNotEmpty + }, + { + type: 'input', + name: 'description', + message: 'What is a short description of your action?', + initial: 'A description of your action', + validate: isNotEmpty + }, + { + type: 'autocomplete', + name: 'icon', + message: 'Choose an icon for your action. Visit https://feathericons.com for a visual reference.', + choices: icons, + limit: 10 + }, + { + type: 'autocomplete', + name: 'color', + message: 'Choose a background color background color used in the visual workflow editor for your action.', + choices: colors + } + ]) +} + +/** + * Creates a Dockerfile contents string, replacing variables in the Dockerfile template + * with values passed in by the user from the CLI prompt. + * + * @param {PromptAnswers} answers The CLI prompt answers. + * @returns {Promise} The Dockerfile contents. + */ +async function createDockerfile (answers) { + const dockerfileTemplate = await readTemplate('Dockerfile') + return dockerfileTemplate + .replace(':NAME', answers.name) + .replace(':DESCRIPTION', answers.description) + .replace(':ICON', answers.icon) + .replace(':COLOR', answers.color) +} + +/** + * Creates a `package.json` object with the latest version + * of `actions-toolkit` ready to be installed. + * + * @param {string} name The action package name. + * @returns {object} The `package.json` contents. + */ +function createPackageJson (name) { + const { version } = require('../package.json') + return { + name, + private: true, + main: 'index.js', + dependencies: { + 'actions-toolkit': `^${version}` + } + } +} + +/** + * Runs the create action CLI prompt and bootstraps a new directory for the user. + * + * @public + * @param {string[]} argv The command line arguments to parse. + * @param {import("signale").Signale} [logger] The Signale logger. + * @returns {Promise} Nothing. + */ +module.exports = async function createAction (argv, signale = new Signale({ + config: { + displayLabel: false + } +})) { + const args = minimist(argv) + const directoryName = args._[0] + if (!directoryName || args.help) { + signale.log('\nUsage: npx actions-toolkit ') + return process.exit(1) + } + + signale.star('Welcome to actions-toolkit! Let\'s get started creating an action.\n') + + const base = path.join(process.cwd(), directoryName) + try { + signale.info(`Creating folder ${base}...`) + await mkdir(base) + } catch (err) { + if (err.code === 'EEXIST') { + signale.fatal(`Folder ${base} already exists!`) + } + throw err + } + + // Collect answers + const metadata = await getActionMetadata() + + signale.log('\n------------------------------------\n') + + // Create the templated files + const dockerfile = await createDockerfile(metadata) + const packageJson = createPackageJson(directoryName) + const entrypoint = await readTemplate('entrypoint.js') + + await Promise.all([ + ['package.json', JSON.stringify(packageJson, null, 2)], + ['Dockerfile', dockerfile], + ['entrypoint.js', entrypoint] + ].map(async ([filename, contents]) => { + signale.info(`Creating ${filename}...`) + await writeFile(path.join(base, filename), contents) + })) + + signale.log('\n------------------------------------\n') + signale.success(`Done! Enjoy building your GitHub Action!`) + signale.info(`Get started with:\n\ncd ${directoryName} && npm install`) +} diff --git a/bin/feather-icons.json b/bin/feather-icons.json new file mode 100644 index 0000000..f0c028e --- /dev/null +++ b/bin/feather-icons.json @@ -0,0 +1,278 @@ +[ + "activity", + "airplay", + "alert-circle", + "alert-octagon", + "alert-triangle", + "align-center", + "align-justify", + "align-left", + "align-right", + "anchor", + "aperture", + "archive", + "arrow-down-circle", + "arrow-down-left", + "arrow-down-right", + "arrow-down", + "arrow-left-circle", + "arrow-left", + "arrow-right-circle", + "arrow-right", + "arrow-up-circle", + "arrow-up-left", + "arrow-up-right", + "arrow-up", + "at-sign", + "award", + "bar-chart-2", + "bar-chart", + "battery-charging", + "battery", + "bell-off", + "bell", + "bluetooth", + "bold", + "book-open", + "book", + "bookmark", + "box", + "briefcase", + "calendar", + "camera-off", + "camera", + "cast", + "check-circle", + "check-square", + "check", + "chevron-down", + "chevron-left", + "chevron-right", + "chevron-up", + "chevrons-down", + "chevrons-left", + "chevrons-right", + "chevrons-up", + "chrome", + "circle", + "clipboard", + "clock", + "cloud-drizzle", + "cloud-lightning", + "cloud-off", + "cloud-rain", + "cloud-snow", + "cloud", + "code", + "codepen", + "coffee", + "command", + "compass", + "copy", + "corner-down-left", + "corner-down-right", + "corner-left-down", + "corner-left-up", + "corner-right-down", + "corner-right-up", + "corner-up-left", + "corner-up-right", + "cpu", + "credit-card", + "crop", + "crosshair", + "database", + "delete", + "disc", + "dollar-sign", + "download-cloud", + "download", + "droplet", + "edit-2", + "edit-3", + "edit", + "external-link", + "eye-off", + "eye", + "facebook", + "fast-forward", + "feather", + "figma", + "file-minus", + "file-plus", + "file-text", + "file", + "film", + "filter", + "flag", + "folder-minus", + "folder-plus", + "folder", + "frown", + "gift", + "git-branch", + "git-commit", + "git-merge", + "git-pull-request", + "github", + "gitlab", + "globe", + "grid", + "hard-drive", + "hash", + "headphones", + "heart", + "help-circle", + "home", + "image", + "inbox", + "info", + "instagram", + "italic", + "key", + "layers", + "layout", + "life-buoy", + "link-2", + "link", + "linkedin", + "list", + "loader", + "lock", + "log-in", + "log-out", + "mail", + "map-pin", + "map", + "maximize-2", + "maximize", + "meh", + "menu", + "message-circle", + "message-square", + "mic-off", + "mic", + "minimize-2", + "minimize", + "minus-circle", + "minus-square", + "minus", + "monitor", + "moon", + "more-horizontal", + "more-vertical", + "mouse-pointer", + "move", + "music", + "navigation-2", + "navigation", + "octagon", + "package", + "paperclip", + "pause-circle", + "pause", + "pen-tool", + "percent", + "phone-call", + "phone-forwarded", + "phone-incoming", + "phone-missed", + "phone-off", + "phone-outgoing", + "phone", + "pie-chart", + "play-circle", + "play", + "plus-circle", + "plus-square", + "plus", + "pocket", + "power", + "printer", + "radio", + "refresh-ccw", + "refresh-cw", + "repeat", + "rewind", + "rotate-ccw", + "rotate-cw", + "rss", + "save", + "scissors", + "search", + "send", + "server", + "settings", + "share-2", + "share", + "shield-off", + "shield", + "shopping-bag", + "shopping-cart", + "shuffle", + "sidebar", + "skip-back", + "skip-forward", + "slack", + "slash", + "sliders", + "smartphone", + "smile", + "speaker", + "square", + "star", + "stop-circle", + "sun", + "sunrise", + "sunset", + "tablet", + "tag", + "target", + "terminal", + "thermometer", + "thumbs-down", + "thumbs-up", + "toggle-left", + "toggle-right", + "trash-2", + "trash", + "trello", + "trending-down", + "trending-up", + "triangle", + "truck", + "tv", + "twitter", + "type", + "umbrella", + "underline", + "unlock", + "upload-cloud", + "upload", + "user-check", + "user-minus", + "user-plus", + "user-x", + "user", + "users", + "video-off", + "video", + "voicemail", + "volume-1", + "volume-2", + "volume-x", + "volume", + "watch", + "wifi-off", + "wifi", + "wind", + "x-circle", + "x-octagon", + "x-square", + "x", + "youtube", + "zap-off", + "zap", + "zoom-in", + "zoom-out" +] diff --git a/bin/template/Dockerfile b/bin/template/Dockerfile index bef732c..4f65fc5 100644 --- a/bin/template/Dockerfile +++ b/bin/template/Dockerfile @@ -11,12 +11,12 @@ FROM node:slim # Labels for GitHub to read your action -LABEL "com.github.actions.name"="Your action name" -LABEL "com.github.actions.description"="A description of your action" -# Here all of the available icons: https://feathericons.com/ -LABEL "com.github.actions.icon"="play" +LABEL "com.github.actions.name"=":NAME" +LABEL "com.github.actions.description"=":DESCRIPTION" +# Here are all of the available icons: https://feathericons.com/ +LABEL "com.github.actions.icon"=":ICON" # And all of the available colors: https://developer.github.com/actions/creating-github-actions/creating-a-docker-container/#label -LABEL "com.github.actions.color"="gray-dark" +LABEL "com.github.actions.color"=":COLOR" # Copy the package.json and package-lock.json COPY package*.json ./ diff --git a/package-lock.json b/package-lock.json index fdd1a36..e91c5a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -653,6 +653,11 @@ "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", "dev": true }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==" + }, "ansi-escapes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", @@ -1223,12 +1228,6 @@ "supports-color": "^5.3.0" } }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true - }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -1632,6 +1631,14 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.0.tgz", + "integrity": "sha512-RNGUbRVlfnjmpxV+Ed+7CGu0rg3MK7MmlW+DW0v7V2zdAUBC1s4BxCRiIAozbYB2UJ+q4D+8tW9UFb11kF72/g==", + "requires": { + "ansi-colors": "^3.2.1" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1748,6 +1755,12 @@ "uri-js": "^4.2.2" } }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -1770,6 +1783,17 @@ "ms": "^2.1.1" } }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", @@ -1782,6 +1806,27 @@ "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", "dev": true }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + } + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1793,6 +1838,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true + }, + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "dev": true, + "requires": { + "symbol-observable": "1.0.1" + } } } }, @@ -2208,17 +2262,6 @@ } } }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "dev": true, - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - } - }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -2710,27 +2753,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "inquirer": { - "version": "5.2.0", - "resolved": "http://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - } - }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -4923,12 +4945,12 @@ "dev": true }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "rsvp": { @@ -4952,15 +4974,6 @@ "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", "dev": true }, - "rxjs": { - "version": "5.5.12", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", - "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", - "dev": true, - "requires": { - "symbol-observable": "1.0.1" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", diff --git a/package.json b/package.json index dd546bc..623d920 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "./lib/index.js", "types": "./lib/index.d.ts", "bin": { - "actions-toolkit": "./bin/actions-toolkit.js" + "actions-toolkit": "./bin/cli.js" }, "scripts": { "build": "rimraf lib && tsc -p tsconfig.json", @@ -41,6 +41,7 @@ "@types/signale": "^1.2.0", "jest": "^24.5.0", "nock": "^10.0.6", + "rimraf": "^2.6.3", "standard": "^12.0.1", "ts-jest": "^24.0.0", "tslint": "^5.12.1", @@ -56,6 +57,7 @@ "dependencies": { "@octokit/graphql": "^2.0.1", "@octokit/rest": "^16.15.0", + "enquirer": "^2.3.0", "execa": "^1.0.0", "flat-cache": "^2.0.1", "js-yaml": "^3.12.1", diff --git a/scripts/update-feather-icons.js b/scripts/update-feather-icons.js new file mode 100644 index 0000000..7b990da --- /dev/null +++ b/scripts/update-feather-icons.js @@ -0,0 +1,22 @@ +const fs = require('fs') +const https = require('https') +const path = require('path') + +https.get('https://unpkg.com/feather-icons/dist/icons.json', resp => { + let data = '' + + resp.on('data', chunk => { + data += chunk + }) + + resp.on('end', () => { + const icons = JSON.parse(data) + const iconNames = Object.keys(icons) + + const iconsFilePath = path.join(process.cwd(), 'bin', 'feather-icons.json') + + fs.writeFile(iconsFilePath, JSON.stringify(iconNames, null, 2), err => { + if (err) throw err + }) + }) +}) diff --git a/tests/__mocks__/enquirer.js b/tests/__mocks__/enquirer.js new file mode 100644 index 0000000..56902ac --- /dev/null +++ b/tests/__mocks__/enquirer.js @@ -0,0 +1,8 @@ +const enquirer = jest.genMockFromModule('enquirer') + +let answers + +enquirer.prompt = jest.fn(async () => answers) +enquirer.__setAnswers = obj => { answers = obj } + +module.exports = enquirer diff --git a/tests/__snapshots__/create-action.test.js.snap b/tests/__snapshots__/create-action.test.js.snap new file mode 100644 index 0000000..bd4ecf3 --- /dev/null +++ b/tests/__snapshots__/create-action.test.js.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`creates project with labels passed to Dockerfile from questionnaire: Dockerfile 1`] = ` +# Use the latest version of Node.js +# +# You may prefer the full image: +# FROM node +# +# or even an alpine image (a smaller, faster, less-feature-complete image): +# FROM node:alpine +# +# You can specify a version: +# FROM node:10-slim +FROM node:slim + +# Labels for GitHub to read your action +LABEL "com.github.actions.name"="My Project Name" +LABEL "com.github.actions.description"="A cool project" +# Here are all of the available icons: https://feathericons.com/ +LABEL "com.github.actions.icon"="anchor" +# And all of the available colors: https://developer.github.com/actions/creating-github-actions/creating-a-docker-container/#label +LABEL "com.github.actions.color"="blue" + +# Copy the package.json and package-lock.json +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy the rest of your action's code +COPY . . + +# Run \`node /entrypoint.js\` +ENTRYPOINT ["node", "/entrypoint.js"] + +`; + +exports[`creates project with labels passed to Dockerfile from questionnaire: entrypoint.js 1`] = ` +const { Toolkit } = require('actions-toolkit') +const tools = new Toolkit() + +console.log(tools.arguments) + +`; + +exports[`creates project with labels passed to Dockerfile from questionnaire: package.json 1`] = ` +{ + "name": "__tmp/my-project-name", + "private": true, + "main": "index.js", + "dependencies": { + "actions-toolkit": "^1.0.0-static-version-for-test" + } +} +`; diff --git a/tests/create-action.test.js b/tests/create-action.test.js new file mode 100644 index 0000000..62eea3b --- /dev/null +++ b/tests/create-action.test.js @@ -0,0 +1,115 @@ +const fs = require('fs') +const path = require('path') +const rimraf = require('rimraf') + +jest.mock('enquirer') + +// Remove double quotes around snapshots. +// This improves snapshot readability for generated file contents. +expect.addSnapshotSerializer({ + test: value => typeof value === 'string', + print: value => value +}) + +/** Creates a mock Signale logger instance for use in asserting log function calls. */ +const createLogger = () => ({ + log: jest.fn(), + info: jest.fn(), + fatal: jest.fn(), + star: jest.fn(), + success: jest.fn() +}) + +let logger + +beforeEach(() => { + jest.resetModules() + + process.exit = jest.fn() + logger = createLogger() + + // Create temporary directory for writing CLI test output. + fs.mkdirSync(path.resolve(__dirname, '../__tmp')) +}) + +afterEach(() => { + jest.restoreAllMocks() + + // Remove temporary directory where CLI test output may have been written. + rimraf.sync(path.resolve(__dirname, '../__tmp')) +}) + +const runCLI = (...args) => { + const createAction = require('../bin/create-action') + return createAction(args, logger) +} + +test('prints help when no arguments are passed', async () => { + await runCLI() + + expect(process.exit).toHaveBeenCalledWith(1) + expect(logger.log).toHaveBeenCalledWith( + expect.stringMatching(/Usage: npx actions-toolkit /) + ) +}) + +test('prints help when --help is passed', async () => { + await runCLI('--help') + + expect(process.exit).toHaveBeenCalledWith(1) + expect(logger.log).toHaveBeenCalledWith( + expect.stringMatching(/Usage: npx actions-toolkit /) + ) +}) + +test('fails to start creating project in a directory that already exists', async () => { + await expect(runCLI('tests/fixtures/workspaces/action-already-exists')).rejects.toThrowError() + expect(logger.fatal).toHaveBeenCalledWith( + expect.stringMatching(/Folder .*tests\/fixtures\/workspaces\/action-already-exists already exists!/) + ) +}) + +test('exits with a failure message when a user cancels the questionnaire', async () => { + // Mock enquirer to throw an error as if a user presses ctrl+c to cancel the questionnaire. + const mockEnquirer = require('enquirer') + mockEnquirer.prompt.mockImplementationOnce(() => { + throw new Error() + }) + + await expect(runCLI('__tmp/my-project-name')).rejects.toThrowError() +}) + +test('creates project with labels passed to Dockerfile from questionnaire', async () => { + jest.mock('../package.json', () => ({ version: '1.0.0-static-version-for-test' })) + + require('enquirer').__setAnswers({ + name: 'My Project Name', + description: 'A cool project', + icon: 'anchor', + color: 'blue' + }) + + const readGeneratedFile = name => + fs.readFileSync(path.resolve(__dirname, '../__tmp/my-project-name', name), 'utf-8') + + await runCLI('__tmp/my-project-name') + + expect(logger.info).toHaveBeenCalledWith( + expect.stringMatching(/Creating folder .*my-project-name.../) + ) + expect(logger.star).toHaveBeenCalledWith( + expect.stringMatching(/Welcome to actions-toolkit/) + ) + expect(logger.info).toHaveBeenCalledWith( + expect.stringMatching(/Creating package.json/) + ) + expect(logger.info).toHaveBeenCalledWith( + expect.stringMatching(/Creating Dockerfile/) + ) + expect(logger.info).toHaveBeenCalledWith( + expect.stringMatching(/Creating entrypoint.js/) + ) + expect(readGeneratedFile('package.json')).toMatchSnapshot('package.json') + expect(readGeneratedFile('Dockerfile')).toMatchSnapshot('Dockerfile') + expect(readGeneratedFile('entrypoint.js')).toMatchSnapshot('entrypoint.js') +}) diff --git a/tests/fixtures/workspaces/action-already-exists/Dockerfile b/tests/fixtures/workspaces/action-already-exists/Dockerfile new file mode 100644 index 0000000..1bc6456 --- /dev/null +++ b/tests/fixtures/workspaces/action-already-exists/Dockerfile @@ -0,0 +1 @@ +# GitHub Action Dockerfile stub for tests