From 2c4ed54679ccaa4148da3f6fb353510fd1c17b6a Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Wed, 27 Feb 2019 21:40:42 -0800 Subject: [PATCH 01/19] WIP: Create CLI questionnaire --- __mocks__/fs.js | 21 ++++ bin/actions-toolkit.js | 75 +++++++++++- bin/colors.json | 10 ++ bin/feather-icons.json | 259 ++++++++++++++++++++++++++++++++++++++++ bin/template/Dockerfile | 8 +- package-lock.json | 112 +++++++++-------- package.json | 2 + tests/cli.test.js | 10 ++ 8 files changed, 444 insertions(+), 53 deletions(-) create mode 100644 __mocks__/fs.js create mode 100644 bin/colors.json create mode 100644 bin/feather-icons.json create mode 100644 tests/cli.test.js diff --git a/__mocks__/fs.js b/__mocks__/fs.js new file mode 100644 index 0000000..eb0dfeb --- /dev/null +++ b/__mocks__/fs.js @@ -0,0 +1,21 @@ +const fs = jest.genMockFromModule('fs') +const realFs = jest.requireActual('fs') + +let fileHolder = new Map() + +// Allow reading from disk. +fs.readdir = realFs.readdir +fs.readFile = realFs.readFile + +// Write file contents to memory. +fs.writeFile = (path, contents, cb) => { + fileHolder.set(path, contents) + // In mock world, we can never fail. :') + cb(null) +} + +// Add some helper methods for getting and setting memory FS. +fs.__reset = () => { fileHolder = new Map() } +fs.__getContents = path => fileHolder.get(path) + +module.exports = fs diff --git a/bin/actions-toolkit.js b/bin/actions-toolkit.js index 45f0ec7..02e56b0 100755 --- a/bin/actions-toolkit.js +++ b/bin/actions-toolkit.js @@ -2,6 +2,79 @@ const fs = require('fs') const path = require('path') +const { promisify } = require('util') +const minimist = require('minimist') +const { prompt } = require('enquirer') +const { render } = require('mustache') +const colors = require('./colors.json') +const icons = require('./feather-icons.json') + +const readFile = promisify(fs.readFile) +const mkdir = promisify(fs.mkdir) +const readdir = promisify(fs.readdir) +const writeFile = promisify(fs.writeFile) + +const templateDir = path.join(__dirname, 'template') + +/** A predicate function to ensure a string is not empty. */ +const isNotEmpty = value => value.length > 0 + +const getActionMetadata = () => 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 + } +]).catch(() => { + // When prompt() rejects, that means the user has pressed ctrl+c to cancel input. + // The idea here is to create a "cancel error" that can be rethrown + // and caught by the main CLI runner, which can print an error message. + const error = new Error() + error.code = 'ECANCEL' + throw error +}) + +const createDockerfile = async ({ name, description, icon, color }) => { + const dockerfileTemplate = await readFile(path.join(templateDir, 'Dockerfile'), 'utf8') + return render(dockerfileTemplate, { name, description, icon, color }) +} + +const main = async () => { + const args = minimist(process.argv.slice(2)) + const directoryName = args._[0] + if (!directoryName || args.help) { + console.log(`\nUsage: npx actions-toolkit `) + process.exit(1) + } + // todo: check if directoryName exists and cancel if it does + console.log() + console.log("Welcome to actions-toolkit! Let's get started creating an action.") + const metadata = await getActionMetadata() + const dockerfile = await createDockerfile(metadata) + // todo: write Dockerfile, package.json, and entrypoint.js to disk. +} const name = process.argv[2] @@ -33,8 +106,6 @@ const packageJson = { } } -const templateDir = path.join(__dirname, 'template') - console.log('Creating package.json...') fs.writeFileSync(path.join(base, 'package.json'), JSON.stringify(packageJson, null, 2)) 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/feather-icons.json b/bin/feather-icons.json new file mode 100644 index 0000000..16ba572 --- /dev/null +++ b/bin/feather-icons.json @@ -0,0 +1,259 @@ +[ + "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", + "circle", + "clipboard", + "clock", + "cloud-drizzle", + "cloud-lightning", + "cloud-off", + "cloud-rain", + "cloud-snow", + "cloud", + "code", + "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", + "file-minus", + "file-plus", + "file-text", + "file", + "film", + "filter", + "flag", + "folder-minus", + "folder-plus", + "folder", + "gift", + "git-branch", + "git-commit", + "git-merge", + "git-pull-request", + "globe", + "grid", + "hard-drive", + "hash", + "headphones", + "heart", + "help-circle", + "home", + "image", + "inbox", + "info", + "italic", + "layers", + "layout", + "life-buoy", + "link-2", + "link", + "list", + "loader", + "lock", + "log-in", + "log-out", + "mail", + "map-pin", + "map", + "maximize-2", + "maximize", + "menu", + "message-circle", + "message-square", + "mic-off", + "mic", + "minimize-2", + "minimize", + "minus-circle", + "minus-square", + "minus", + "monitor", + "moon", + "more-horizontal", + "more-vertical", + "move", + "music", + "navigation-2", + "navigation", + "octagon", + "package", + "paperclip", + "pause-circle", + "pause", + "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", + "slash", + "sliders", + "smartphone", + "speaker", + "square", + "star", + "stop-circle", + "sun", + "sunrise", + "sunset", + "tablet", + "tag", + "target", + "terminal", + "thermometer", + "thumbs-down", + "thumbs-up", + "toggle-left", + "toggle-right", + "trash-2", + "trash", + "trending-down", + "trending-up", + "triangle", + "truck", + "tv", + "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-square", + "x", + "zap-off", + "zap", + "zoom-in", + "zoom-out" +] diff --git a/bin/template/Dockerfile b/bin/template/Dockerfile index bef732c..de9f261 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" +LABEL "com.github.actions.name"="{{name}}" +LABEL "com.github.actions.description"="{{{description}}}" # Here all of the available icons: https://feathericons.com/ -LABEL "com.github.actions.icon"="play" +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 9fee51e..1708548 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", @@ -3954,6 +3976,11 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "mustache": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.1.tgz", + "integrity": "sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA==" + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -4937,15 +4964,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 2416203..37d6229 100644 --- a/package.json +++ b/package.json @@ -56,10 +56,12 @@ "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", "minimist": "^1.2.0", + "mustache": "^3.0.1", "signale": "^1.3.0" }, "jest": { diff --git a/tests/cli.test.js b/tests/cli.test.js new file mode 100644 index 0000000..e949532 --- /dev/null +++ b/tests/cli.test.js @@ -0,0 +1,10 @@ +jest.mock('fs') + +beforeEach(() => { + require('fs').__reset() +}) + +test.todo('prints help when no arguments are passed') +test.todo('prints help when --help is passed') +test.todo('fails to start creating project in a directory that already exists') +test.todo('creates project with labels passed to Dockerfile from questionnaire') From 456cdebc0885214cbe6b04f6cddf90c4ba5d5ff3 Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Thu, 28 Feb 2019 11:59:41 -0800 Subject: [PATCH 02/19] Replace mustache with custom solution --- bin/actions-toolkit.js | 9 ++++++--- bin/template/Dockerfile | 8 ++++---- package-lock.json | 5 ----- package.json | 1 - 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/bin/actions-toolkit.js b/bin/actions-toolkit.js index 02e56b0..9bb2b2c 100755 --- a/bin/actions-toolkit.js +++ b/bin/actions-toolkit.js @@ -5,7 +5,6 @@ const path = require('path') const { promisify } = require('util') const minimist = require('minimist') const { prompt } = require('enquirer') -const { render } = require('mustache') const colors = require('./colors.json') const icons = require('./feather-icons.json') @@ -56,9 +55,13 @@ const getActionMetadata = () => prompt([ throw error }) -const createDockerfile = async ({ name, description, icon, color }) => { +const createDockerfile = async (options) => { const dockerfileTemplate = await readFile(path.join(templateDir, 'Dockerfile'), 'utf8') - return render(dockerfileTemplate, { name, description, icon, color }) + return dockerfileTemplate + .replace(':NAME', options.name) + .replace(':DESCRIPTION', options.description) + .replace(':ICON', options.icon) + .replace(':COLOR', options.color) } const main = async () => { diff --git a/bin/template/Dockerfile b/bin/template/Dockerfile index de9f261..ae624f2 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"="{{name}}" -LABEL "com.github.actions.description"="{{{description}}}" +LABEL "com.github.actions.name"=":NAME" +LABEL "com.github.actions.description"=":DESCRIPTION" # Here all of the available icons: https://feathericons.com/ -LABEL "com.github.actions.icon"="{{icon}}" +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"="{{color}}" +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 1708548..180efdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3976,11 +3976,6 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, - "mustache": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.1.tgz", - "integrity": "sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA==" - }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", diff --git a/package.json b/package.json index 37d6229..8ef47df 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "flat-cache": "^2.0.1", "js-yaml": "^3.12.1", "minimist": "^1.2.0", - "mustache": "^3.0.1", "signale": "^1.3.0" }, "jest": { From 78012c58618ca289c78e94540d9c3175a838e50a Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Thu, 28 Feb 2019 15:50:44 -0800 Subject: [PATCH 03/19] Add scripts/update-feather-icons.js --- .npmignore | 3 ++- bin/feather-icons.json | 19 +++++++++++++++++++ scripts/update-feather-icons.js | 22 ++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 scripts/update-feather-icons.js 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/feather-icons.json b/bin/feather-icons.json index 16ba572..f0c028e 100644 --- a/bin/feather-icons.json +++ b/bin/feather-icons.json @@ -53,6 +53,7 @@ "chevrons-left", "chevrons-right", "chevrons-up", + "chrome", "circle", "clipboard", "clock", @@ -63,6 +64,8 @@ "cloud-snow", "cloud", "code", + "codepen", + "coffee", "command", "compass", "copy", @@ -94,6 +97,7 @@ "facebook", "fast-forward", "feather", + "figma", "file-minus", "file-plus", "file-text", @@ -104,11 +108,14 @@ "folder-minus", "folder-plus", "folder", + "frown", "gift", "git-branch", "git-commit", "git-merge", "git-pull-request", + "github", + "gitlab", "globe", "grid", "hard-drive", @@ -120,12 +127,15 @@ "image", "inbox", "info", + "instagram", "italic", + "key", "layers", "layout", "life-buoy", "link-2", "link", + "linkedin", "list", "loader", "lock", @@ -136,6 +146,7 @@ "map", "maximize-2", "maximize", + "meh", "menu", "message-circle", "message-square", @@ -150,6 +161,7 @@ "moon", "more-horizontal", "more-vertical", + "mouse-pointer", "move", "music", "navigation-2", @@ -159,6 +171,7 @@ "paperclip", "pause-circle", "pause", + "pen-tool", "percent", "phone-call", "phone-forwarded", @@ -200,9 +213,11 @@ "sidebar", "skip-back", "skip-forward", + "slack", "slash", "sliders", "smartphone", + "smile", "speaker", "square", "star", @@ -221,11 +236,13 @@ "toggle-right", "trash-2", "trash", + "trello", "trending-down", "trending-up", "triangle", "truck", "tv", + "twitter", "type", "umbrella", "underline", @@ -250,8 +267,10 @@ "wifi", "wind", "x-circle", + "x-octagon", "x-square", "x", + "youtube", "zap-off", "zap", "zoom-in", diff --git a/scripts/update-feather-icons.js b/scripts/update-feather-icons.js new file mode 100644 index 0000000..03a09c7 --- /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@4.19.0/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 + }) + }) +}) From 6b92afe9cd9fd8d028df28a94807b4b49b98e909 Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Thu, 28 Feb 2019 15:53:31 -0800 Subject: [PATCH 04/19] Use Map.prototype.clear in fs mock --- __mocks__/fs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__mocks__/fs.js b/__mocks__/fs.js index eb0dfeb..7ff7907 100644 --- a/__mocks__/fs.js +++ b/__mocks__/fs.js @@ -1,7 +1,7 @@ const fs = jest.genMockFromModule('fs') const realFs = jest.requireActual('fs') -let fileHolder = new Map() +const fileHolder = new Map() // Allow reading from disk. fs.readdir = realFs.readdir @@ -15,7 +15,7 @@ fs.writeFile = (path, contents, cb) => { } // Add some helper methods for getting and setting memory FS. -fs.__reset = () => { fileHolder = new Map() } +fs.__reset = () => fileHolder.clear() fs.__getContents = path => fileHolder.get(path) module.exports = fs From d319112bee3102978d4e4f147849b9e818151eac Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Fri, 1 Mar 2019 13:34:26 -0800 Subject: [PATCH 05/19] Get tests passing :D --- bin/actions-toolkit.js | 177 ++++++++++++++------------- tests/__mocks__/enquirer.js | 8 ++ {__mocks__ => tests/__mocks__}/fs.js | 7 +- tests/__snapshots__/cli.test.js.snap | 55 +++++++++ tests/cli.test.js | 85 ++++++++++++- 5 files changed, 242 insertions(+), 90 deletions(-) create mode 100644 tests/__mocks__/enquirer.js rename {__mocks__ => tests/__mocks__}/fs.js (82%) create mode 100644 tests/__snapshots__/cli.test.js.snap diff --git a/bin/actions-toolkit.js b/bin/actions-toolkit.js index 9bb2b2c..c1e5395 100755 --- a/bin/actions-toolkit.js +++ b/bin/actions-toolkit.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +// @ts-check const fs = require('fs') const path = require('path') @@ -9,52 +10,67 @@ const colors = require('./colors.json') const icons = require('./feather-icons.json') const readFile = promisify(fs.readFile) -const mkdir = promisify(fs.mkdir) -const readdir = promisify(fs.readdir) const writeFile = promisify(fs.writeFile) +const mkdir = promisify(fs.mkdir) const templateDir = path.join(__dirname, 'template') /** A predicate function to ensure a string is not empty. */ const isNotEmpty = value => value.length > 0 -const getActionMetadata = () => 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 +/** + * The options object returned from the CLI questionnaire prompt. + * @typedef {object} PromptOptions + * @property {string} name + * @property {string} description + * @property {string} icon + * @property {string} color + */ + +/** + * @returns {Promise} + */ +const getActionMetadata = async () => { + try { + return await 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 + }, + { + type: 'autocomplete', + name: 'color', + message: 'Choose a background color background color used in the visual workflow editor for your action.', + choices: colors + } + ]) + } catch (err) { + // When prompt() rejects, that means the user has pressed ctrl+c to cancel input. + throw new Error('Cancelled. Maybe next time!') } -]).catch(() => { - // When prompt() rejects, that means the user has pressed ctrl+c to cancel input. - // The idea here is to create a "cancel error" that can be rethrown - // and caught by the main CLI runner, which can print an error message. - const error = new Error() - error.code = 'ECANCEL' - throw error -}) +} +/** + * + * @param {PromptOptions} options + * @returns {Promise} The Dockerfile contents. + */ const createDockerfile = async (options) => { const dockerfileTemplate = await readFile(path.join(templateDir, 'Dockerfile'), 'utf8') return dockerfileTemplate @@ -64,58 +80,55 @@ const createDockerfile = async (options) => { .replace(':COLOR', options.color) } -const main = async () => { - const args = minimist(process.argv.slice(2)) +const createPackageJson = (name) => { + const { version } = require('../package.json') + return { + name, + private: true, + main: 'index.js', + dependencies: { + 'actions-toolkit': `^${version}` + } + } +} + +const runCLI = async (argv) => { + const args = minimist(argv) const directoryName = args._[0] if (!directoryName || args.help) { console.log(`\nUsage: npx actions-toolkit `) process.exit(1) + return + } + const base = path.join(process.cwd(), directoryName) + try { + console.log(`Creating folder ${base}...`) + await mkdir(base) + } catch (err) { + if (err.code === 'EEXIST') { + throw new Error(`Folder ${base} already exists!`) + } else { + console.error(err.code) + throw err + } } - // todo: check if directoryName exists and cancel if it does - console.log() - console.log("Welcome to actions-toolkit! Let's get started creating an action.") + + console.log("\nWelcome to actions-toolkit! Let's get started creating an action.") const metadata = await getActionMetadata() const dockerfile = await createDockerfile(metadata) - // todo: write Dockerfile, package.json, and entrypoint.js to disk. -} + const packageJson = createPackageJson(directoryName) + const entrypoint = await readFile(path.join(templateDir, 'entrypoint.js'), 'utf8') + const files = [ + ['package.json', JSON.stringify(packageJson, null, 2)], + ['Dockerfile', dockerfile], + ['entrypoint.js', entrypoint] + ] + files.forEach(async ([filename, contents]) => { + console.log(`Creating ${filename}...`) + await writeFile(path.join(base, filename), contents) + }) -const name = process.argv[2] - -if (!name) { - console.log(`\nUsage: npx actions-toolkit `) - process.exit(1) + console.log(`\nDone! Enjoy building your GitHub Action! Get started with:\n\ncd ${directoryName} && npm install`) } -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}` - } -} - -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`) +module.exports = runCLI diff --git a/tests/__mocks__/enquirer.js b/tests/__mocks__/enquirer.js new file mode 100644 index 0000000..8a3c6e4 --- /dev/null +++ b/tests/__mocks__/enquirer.js @@ -0,0 +1,8 @@ +const enquirer = jest.genMockFromModule('enquirer') + +let answers + +enquirer.prompt = async () => answers +enquirer.__setAnswers = obj => { answers = obj } + +module.exports = enquirer diff --git a/__mocks__/fs.js b/tests/__mocks__/fs.js similarity index 82% rename from __mocks__/fs.js rename to tests/__mocks__/fs.js index 7ff7907..6b0ff7e 100644 --- a/__mocks__/fs.js +++ b/tests/__mocks__/fs.js @@ -4,15 +4,16 @@ const realFs = jest.requireActual('fs') const fileHolder = new Map() // Allow reading from disk. -fs.readdir = realFs.readdir fs.readFile = realFs.readFile // Write file contents to memory. -fs.writeFile = (path, contents, cb) => { +fs.writeFile = jest.fn((path, contents, cb) => { fileHolder.set(path, contents) // In mock world, we can never fail. :') cb(null) -} +}) + +fs.mkdir = jest.fn((path, cb) => { cb(null) }) // Add some helper methods for getting and setting memory FS. fs.__reset = () => fileHolder.clear() diff --git a/tests/__snapshots__/cli.test.js.snap b/tests/__snapshots__/cli.test.js.snap new file mode 100644 index 0000000..e9c1fc1 --- /dev/null +++ b/tests/__snapshots__/cli.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 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": "my-project-name", + "private": true, + "main": "index.js", + "dependencies": { + "actions-toolkit": "^1.0.0-static-version-for-test" + } +} +`; diff --git a/tests/cli.test.js b/tests/cli.test.js index e949532..9664e76 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -1,10 +1,85 @@ +// Remove double quotes around snapshots. +// This improves snapshot readability for generated file contents. +expect.addSnapshotSerializer({ + test: value => typeof value === 'string', + print: value => value +}) + jest.mock('fs') +jest.mock('enquirer') + +let mockFs + +const runCLI = (...args) => { + process.exit = jest.fn() + console.log = jest.fn() + console.error = jest.fn() + process.cwd = jest.fn(() => '.') + const bin = require('../bin/actions-toolkit') + return bin(args) +} beforeEach(() => { - require('fs').__reset() + jest.resetModules() + mockFs = require('fs') + mockFs.__reset() }) -test.todo('prints help when no arguments are passed') -test.todo('prints help when --help is passed') -test.todo('fails to start creating project in a directory that already exists') -test.todo('creates project with labels passed to Dockerfile from questionnaire') +test('prints help when no arguments are passed', async () => { + await runCLI() + expect(process.exit).toHaveBeenCalledWith(1) + expect(console.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(console.log).toHaveBeenCalledWith( + expect.stringMatching(/Usage: npx actions-toolkit /) + ) +}) + +test('fails to start creating project in a directory that already exists', async () => { + mockFs.mkdir.mockImplementationOnce((_, cb) => { + const error = new Error() + error.code = 'EEXIST' + cb(error) + }) + + expect(runCLI('my-project-name')).rejects.toThrowError( + /Folder my-project-name already exists!/ + ) +}) + +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' + }) + mockFs.mkdir.mockImplementationOnce((_, cb) => cb(null)) + + await runCLI('my-project-name') + expect(console.log).toHaveBeenCalledWith( + expect.stringMatching(/Creating folder my-project-name.../) + ) + expect(console.log).toHaveBeenCalledWith( + expect.stringMatching(/Welcome to actions-toolkit/) + ) + expect(console.log).toHaveBeenCalledWith( + expect.stringMatching(/Creating package.json/) + ) + expect(console.log).toHaveBeenCalledWith( + expect.stringMatching(/Creating Dockerfile/) + ) + expect(console.log).toHaveBeenCalledWith( + expect.stringMatching(/Creating entrypoint.js/) + ) + expect(mockFs.__getContents('my-project-name/package.json')).toMatchSnapshot('package.json') + expect(mockFs.__getContents('my-project-name/Dockerfile')).toMatchSnapshot('Dockerfile') + expect(mockFs.__getContents('my-project-name/entrypoint.js')).toMatchSnapshot('entrypoint.js') +}) From da14ce960bbb8f66694d0e2685a8915942a33e11 Mon Sep 17 00:00:00 2001 From: Jason Etcovitch Date: Fri, 1 Mar 2019 13:35:44 -0800 Subject: [PATCH 06/19] Update scripts/update-feather-icons.js Co-Authored-By: macklinu --- scripts/update-feather-icons.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update-feather-icons.js b/scripts/update-feather-icons.js index 03a09c7..7b990da 100644 --- a/scripts/update-feather-icons.js +++ b/scripts/update-feather-icons.js @@ -2,7 +2,7 @@ const fs = require('fs') const https = require('https') const path = require('path') -https.get('https://unpkg.com/feather-icons@4.19.0/dist/icons.json', resp => { +https.get('https://unpkg.com/feather-icons/dist/icons.json', resp => { let data = '' resp.on('data', chunk => { From dde28108f6b5232decf7194f556c227705be9555 Mon Sep 17 00:00:00 2001 From: Jason Etcovitch Date: Fri, 1 Mar 2019 16:10:24 -0800 Subject: [PATCH 07/19] Update tests/cli.test.js Co-Authored-By: macklinu --- tests/cli.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli.test.js b/tests/cli.test.js index 9664e76..49dbc14 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -48,7 +48,7 @@ test('fails to start creating project in a directory that already exists', async cb(error) }) - expect(runCLI('my-project-name')).rejects.toThrowError( + await expect(runCLI('my-project-name')).rejects.toThrowError( /Folder my-project-name already exists!/ ) }) From db2c9cb38790add175b11557935991a5b7bdb912 Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Tue, 5 Mar 2019 18:23:04 -0800 Subject: [PATCH 08/19] More updates / WIP --- bin/cli.js | 12 +++++++ bin/{actions-toolkit.js => create-action.js} | 22 ++++++++++--- package.json | 2 +- tests/__mocks__/enquirer.js | 2 +- tests/__mocks__/fs.js | 3 +- ...est.js.snap => create-action.test.js.snap} | 0 tests/{cli.test.js => create-action.test.js} | 33 +++++++++++++++---- 7 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 bin/cli.js rename bin/{actions-toolkit.js => create-action.js} (88%) rename tests/__snapshots__/{cli.test.js.snap => create-action.test.js.snap} (100%) rename tests/{cli.test.js => create-action.test.js} (82%) diff --git a/bin/cli.js b/bin/cli.js new file mode 100644 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/actions-toolkit.js b/bin/create-action.js similarity index 88% rename from bin/actions-toolkit.js rename to bin/create-action.js index c1e5395..4572326 100755 --- a/bin/actions-toolkit.js +++ b/bin/create-action.js @@ -1,4 +1,3 @@ -#!/usr/bin/env node // @ts-check const fs = require('fs') @@ -15,7 +14,10 @@ const mkdir = promisify(fs.mkdir) const templateDir = path.join(__dirname, 'template') -/** A predicate function to ensure a string is not empty. */ +/** + * A predicate function to ensure a string is not empty. + * @returns {boolean} + */ const isNotEmpty = value => value.length > 0 /** @@ -80,6 +82,13 @@ const createDockerfile = async (options) => { .replace(':COLOR', options.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. + */ const createPackageJson = (name) => { const { version } = require('../package.json') return { @@ -92,7 +101,12 @@ const createPackageJson = (name) => { } } -const runCLI = async (argv) => { +/** + * + * @param {string[]} argv The command line arguments to parse. + * @returns {Promise} Nothing. + */ +const createAction = async (argv) => { const args = minimist(argv) const directoryName = args._[0] if (!directoryName || args.help) { @@ -131,4 +145,4 @@ const runCLI = async (argv) => { console.log(`\nDone! Enjoy building your GitHub Action! Get started with:\n\ncd ${directoryName} && npm install`) } -module.exports = runCLI +module.exports = createAction diff --git a/package.json b/package.json index 8ef47df..c24a84f 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", diff --git a/tests/__mocks__/enquirer.js b/tests/__mocks__/enquirer.js index 8a3c6e4..56902ac 100644 --- a/tests/__mocks__/enquirer.js +++ b/tests/__mocks__/enquirer.js @@ -2,7 +2,7 @@ const enquirer = jest.genMockFromModule('enquirer') let answers -enquirer.prompt = async () => answers +enquirer.prompt = jest.fn(async () => answers) enquirer.__setAnswers = obj => { answers = obj } module.exports = enquirer diff --git a/tests/__mocks__/fs.js b/tests/__mocks__/fs.js index 6b0ff7e..4d66a7f 100644 --- a/tests/__mocks__/fs.js +++ b/tests/__mocks__/fs.js @@ -15,8 +15,7 @@ fs.writeFile = jest.fn((path, contents, cb) => { fs.mkdir = jest.fn((path, cb) => { cb(null) }) -// Add some helper methods for getting and setting memory FS. -fs.__reset = () => fileHolder.clear() +// Add a helper method for getting memory FS. fs.__getContents = path => fileHolder.get(path) module.exports = fs diff --git a/tests/__snapshots__/cli.test.js.snap b/tests/__snapshots__/create-action.test.js.snap similarity index 100% rename from tests/__snapshots__/cli.test.js.snap rename to tests/__snapshots__/create-action.test.js.snap diff --git a/tests/cli.test.js b/tests/create-action.test.js similarity index 82% rename from tests/cli.test.js rename to tests/create-action.test.js index 49dbc14..1c12a75 100644 --- a/tests/cli.test.js +++ b/tests/create-action.test.js @@ -11,22 +11,27 @@ jest.mock('enquirer') let mockFs const runCLI = (...args) => { - process.exit = jest.fn() - console.log = jest.fn() - console.error = jest.fn() - process.cwd = jest.fn(() => '.') - const bin = require('../bin/actions-toolkit') - return bin(args) + const createAction = require('../bin/create-action') + return createAction(args) } beforeEach(() => { jest.resetModules() mockFs = require('fs') - mockFs.__reset() + + process.exit = jest.fn() + console.log = jest.fn() + console.error = jest.fn() + process.cwd = jest.fn(() => '.') +}) + +afterEach(() => { + jest.restoreAllMocks() }) test('prints help when no arguments are passed', async () => { await runCLI() + expect(process.exit).toHaveBeenCalledWith(1) expect(console.log).toHaveBeenCalledWith( expect.stringMatching(/Usage: npx actions-toolkit /) @@ -35,6 +40,7 @@ test('prints help when no arguments are passed', async () => { test('prints help when --help is passed', async () => { await runCLI('--help') + expect(process.exit).toHaveBeenCalledWith(1) expect(console.log).toHaveBeenCalledWith( expect.stringMatching(/Usage: npx actions-toolkit /) @@ -53,6 +59,18 @@ test('fails to start creating project in a directory that already exists', async ) }) +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('my-project-name')).rejects.toThrowError( + /Cancelled. Maybe next time!/ + ) +}) + 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({ @@ -64,6 +82,7 @@ test('creates project with labels passed to Dockerfile from questionnaire', asyn mockFs.mkdir.mockImplementationOnce((_, cb) => cb(null)) await runCLI('my-project-name') + expect(console.log).toHaveBeenCalledWith( expect.stringMatching(/Creating folder my-project-name.../) ) From 9232edb1d22a33135b8025220bef084f04b20d5b Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Thu, 7 Mar 2019 16:43:34 -0800 Subject: [PATCH 09/19] More documenatation --- bin/create-action.js | 64 +++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/bin/create-action.js b/bin/create-action.js index 4572326..f12dc4f 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -12,25 +12,38 @@ const readFile = promisify(fs.readFile) const writeFile = promisify(fs.writeFile) const mkdir = promisify(fs.mkdir) -const templateDir = path.join(__dirname, 'template') +/** + * Reads a template file from disk. + * + * @param {string} filename The template filename to read. + * @returns {Promise} The template file string contents. + */ +const 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. - * @returns {boolean} +/** + * 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} PromptOptions - * @property {string} name - * @property {string} description - * @property {string} icon - * @property {string} color + * @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. */ /** - * @returns {Promise} + * Prompts the user with a questionnaire to get key metadata for the GitHub Action. + * + * @returns {Promise} An object containing prompt answers. */ const getActionMetadata = async () => { try { @@ -69,23 +82,25 @@ const getActionMetadata = async () => { } /** + * Creates a Dockerfile contents string, replacing variables in the Dockerfile template + * with values passed in by the user from the CLI prompt. * - * @param {PromptOptions} options + * @param {PromptAnswers} answers The CLI prompt answers. * @returns {Promise} The Dockerfile contents. */ -const createDockerfile = async (options) => { - const dockerfileTemplate = await readFile(path.join(templateDir, 'Dockerfile'), 'utf8') +const createDockerfile = async (answers) => { + const dockerfileTemplate = await readTemplate('Dockerfile') return dockerfileTemplate - .replace(':NAME', options.name) - .replace(':DESCRIPTION', options.description) - .replace(':ICON', options.icon) - .replace(':COLOR', options.color) + .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 + * 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. */ @@ -102,7 +117,9 @@ const createPackageJson = (name) => { } /** - * + * Runs the create action CLI prompt and bootstraps a new directory for the user. + * + * @public * @param {string[]} argv The command line arguments to parse. * @returns {Promise} Nothing. */ @@ -112,6 +129,11 @@ const createAction = async (argv) => { if (!directoryName || args.help) { console.log(`\nUsage: npx actions-toolkit `) process.exit(1) + // Although this return is unreachable, + // for some reason, code after this block is reached in unit tests, + // even while this calls `process.exit(1)`. + // Adding a `return` below fixes that issue in the tests. + // eslint-disable-next-line return } const base = path.join(process.cwd(), directoryName) @@ -131,7 +153,7 @@ const createAction = async (argv) => { const metadata = await getActionMetadata() const dockerfile = await createDockerfile(metadata) const packageJson = createPackageJson(directoryName) - const entrypoint = await readFile(path.join(templateDir, 'entrypoint.js'), 'utf8') + const entrypoint = await readTemplate('entrypoint.js') const files = [ ['package.json', JSON.stringify(packageJson, null, 2)], ['Dockerfile', dockerfile], From 9a027781bc8b50b2213b5780b3e84f781b7f8eb1 Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Thu, 7 Mar 2019 16:51:20 -0800 Subject: [PATCH 10/19] Tweak welcome message spacing --- bin/create-action.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/create-action.js b/bin/create-action.js index f12dc4f..56281a2 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -149,7 +149,7 @@ const createAction = async (argv) => { } } - console.log("\nWelcome to actions-toolkit! Let's get started creating an action.") + console.log("\nWelcome to actions-toolkit! Let's get started creating an action.\n") const metadata = await getActionMetadata() const dockerfile = await createDockerfile(metadata) const packageJson = createPackageJson(directoryName) From 27725ddd200761204c544fa4b46b9fddb99c0bc8 Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Thu, 7 Mar 2019 16:51:31 -0800 Subject: [PATCH 11/19] Fix async write file loop Using Promise.all is better than Array.prototype.forEach for the purposes of writing multiple files to disk. --- bin/create-action.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bin/create-action.js b/bin/create-action.js index 56281a2..ba0b2a9 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -154,15 +154,14 @@ const createAction = async (argv) => { const dockerfile = await createDockerfile(metadata) const packageJson = createPackageJson(directoryName) const entrypoint = await readTemplate('entrypoint.js') - const files = [ + await Promise.all([ ['package.json', JSON.stringify(packageJson, null, 2)], ['Dockerfile', dockerfile], ['entrypoint.js', entrypoint] - ] - files.forEach(async ([filename, contents]) => { + ].map(async ([filename, contents]) => { console.log(`Creating ${filename}...`) await writeFile(path.join(base, filename), contents) - }) + })) console.log(`\nDone! Enjoy building your GitHub Action! Get started with:\n\ncd ${directoryName} && npm install`) } From fc3dd0bc6d810335aabe09fb5a5ea990f395687f Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Thu, 14 Mar 2019 11:46:31 -0700 Subject: [PATCH 12/19] Address PR comments --- bin/create-action.js | 11 +++-------- bin/template/Dockerfile | 2 +- tests/__snapshots__/create-action.test.js.snap | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/bin/create-action.js b/bin/create-action.js index ba0b2a9..71a5772 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -66,7 +66,8 @@ const getActionMetadata = async () => { type: 'autocomplete', name: 'icon', message: 'Choose an icon for your action. Visit https://feathericons.com for a visual reference.', - choices: icons + choices: icons, + limit: 10 }, { type: 'autocomplete', @@ -128,13 +129,7 @@ const createAction = async (argv) => { const directoryName = args._[0] if (!directoryName || args.help) { console.log(`\nUsage: npx actions-toolkit `) - process.exit(1) - // Although this return is unreachable, - // for some reason, code after this block is reached in unit tests, - // even while this calls `process.exit(1)`. - // Adding a `return` below fixes that issue in the tests. - // eslint-disable-next-line - return + return process.exit(1) } const base = path.join(process.cwd(), directoryName) try { diff --git a/bin/template/Dockerfile b/bin/template/Dockerfile index ae624f2..4f65fc5 100644 --- a/bin/template/Dockerfile +++ b/bin/template/Dockerfile @@ -13,7 +13,7 @@ FROM node:slim # Labels for GitHub to read your action LABEL "com.github.actions.name"=":NAME" LABEL "com.github.actions.description"=":DESCRIPTION" -# Here all of the available icons: https://feathericons.com/ +# 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"=":COLOR" diff --git a/tests/__snapshots__/create-action.test.js.snap b/tests/__snapshots__/create-action.test.js.snap index e9c1fc1..92abb0d 100644 --- a/tests/__snapshots__/create-action.test.js.snap +++ b/tests/__snapshots__/create-action.test.js.snap @@ -16,7 +16,7 @@ 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 all of the available icons: https://feathericons.com/ +# 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" From 66c812de51299ff7809f568dc4635603160c2fb2 Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Thu, 14 Mar 2019 12:08:38 -0700 Subject: [PATCH 13/19] Add and update tests for 100% test coverage --- bin/create-action.js | 64 ++++++++++++++++--------------------- tests/create-action.test.js | 14 ++++++-- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/bin/create-action.js b/bin/create-action.js index 71a5772..8ebc538 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -45,42 +45,35 @@ const isNotEmpty = value => value.length > 0 * * @returns {Promise} An object containing prompt answers. */ -const getActionMetadata = async () => { - try { - return await 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 - } - ]) - } catch (err) { - // When prompt() rejects, that means the user has pressed ctrl+c to cancel input. - throw new Error('Cancelled. Maybe next time!') +const getActionMetadata = () => 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 @@ -139,7 +132,6 @@ const createAction = async (argv) => { if (err.code === 'EEXIST') { throw new Error(`Folder ${base} already exists!`) } else { - console.error(err.code) throw err } } diff --git a/tests/create-action.test.js b/tests/create-action.test.js index 1c12a75..bf525dc 100644 --- a/tests/create-action.test.js +++ b/tests/create-action.test.js @@ -59,6 +59,16 @@ test('fails to start creating project in a directory that already exists', async ) }) +test('throws unhandled fs.mkdir errors', async () => { + mockFs.mkdir.mockImplementationOnce((_, cb) => { + const error = new Error("EPERM: operation not permitted, mkdir '/etc'") + error.code = 'EPERM' + cb(error) + }) + + await expect(runCLI('my-project-name')).rejects.toThrowError(/EPERM: operation not permitted, mkdir '\/etc'/) +}) + 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') @@ -66,9 +76,7 @@ test('exits with a failure message when a user cancels the questionnaire', async throw new Error() }) - await expect(runCLI('my-project-name')).rejects.toThrowError( - /Cancelled. Maybe next time!/ - ) + await expect(runCLI('my-project-name')).rejects.toThrowError() }) test('creates project with labels passed to Dockerfile from questionnaire', async () => { From ee6811118c9230df6050ccc2b75e9893cc00ae8a Mon Sep 17 00:00:00 2001 From: Jason Etcovitch Date: Sat, 16 Mar 2019 17:06:08 -0400 Subject: [PATCH 14/19] Couple small tweaks --- bin/create-action.js | 74 +++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/bin/create-action.js b/bin/create-action.js index 8ebc538..e9bf3f2 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -1,5 +1,3 @@ -// @ts-check - const fs = require('fs') const path = require('path') const { promisify } = require('util') @@ -18,7 +16,7 @@ const mkdir = promisify(fs.mkdir) * @param {string} filename The template filename to read. * @returns {Promise} The template file string contents. */ -const readTemplate = filename => { +async function readTemplate (filename) { const templateDir = path.join(__dirname, 'template') return readFile(path.join(templateDir, filename), 'utf8') } @@ -45,35 +43,37 @@ const isNotEmpty = value => value.length > 0 * * @returns {Promise} An object containing prompt answers. */ -const getActionMetadata = () => 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 - } -]) +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 @@ -82,7 +82,7 @@ const getActionMetadata = () => prompt([ * @param {PromptAnswers} answers The CLI prompt answers. * @returns {Promise} The Dockerfile contents. */ -const createDockerfile = async (answers) => { +async function createDockerfile (answers) { const dockerfileTemplate = await readTemplate('Dockerfile') return dockerfileTemplate .replace(':NAME', answers.name) @@ -98,7 +98,7 @@ const createDockerfile = async (answers) => { * @param {string} name The action package name. * @returns {object} The `package.json` contents. */ -const createPackageJson = (name) => { +function createPackageJson (name) { const { version } = require('../package.json') return { name, @@ -117,7 +117,7 @@ const createPackageJson = (name) => { * @param {string[]} argv The command line arguments to parse. * @returns {Promise} Nothing. */ -const createAction = async (argv) => { +module.exports = async function createAction (argv) { const args = minimist(argv) const directoryName = args._[0] if (!directoryName || args.help) { @@ -136,7 +136,7 @@ const createAction = async (argv) => { } } - console.log("\nWelcome to actions-toolkit! Let's get started creating an action.\n") + console.log('\nWelcome to actions-toolkit! Let\'s get started creating an action.\n') const metadata = await getActionMetadata() const dockerfile = await createDockerfile(metadata) const packageJson = createPackageJson(directoryName) @@ -152,5 +152,3 @@ const createAction = async (argv) => { console.log(`\nDone! Enjoy building your GitHub Action! Get started with:\n\ncd ${directoryName} && npm install`) } - -module.exports = createAction From 5ba626d3a21163fe8095a99daa7d6d1bc346d7f7 Mon Sep 17 00:00:00 2001 From: Jason Etcovitch Date: Sat, 16 Mar 2019 17:13:34 -0400 Subject: [PATCH 15/19] Whitespace and comments --- bin/cli.js | 0 bin/create-action.js | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) mode change 100644 => 100755 bin/cli.js diff --git a/bin/cli.js b/bin/cli.js old mode 100644 new mode 100755 diff --git a/bin/create-action.js b/bin/create-action.js index e9bf3f2..1922270 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -137,10 +137,15 @@ module.exports = async function createAction (argv) { } console.log('\nWelcome to actions-toolkit! Let\'s get started creating an action.\n') + + // Collect answers const metadata = await getActionMetadata() + + // 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], @@ -150,5 +155,5 @@ module.exports = async function createAction (argv) { await writeFile(path.join(base, filename), contents) })) - console.log(`\nDone! Enjoy building your GitHub Action! Get started with:\n\ncd ${directoryName} && npm install`) + console.log(`\n✔ Done! Enjoy building your GitHub Action! Get started with:\n\ncd ${directoryName} && npm install`) } From 9bbf52d563a01754e08ed58109b4b52f3f41eff9 Mon Sep 17 00:00:00 2001 From: Jason Etcovitch Date: Sat, 16 Mar 2019 17:34:44 -0400 Subject: [PATCH 16/19] Use Signale --- bin/create-action.js | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/bin/create-action.js b/bin/create-action.js index 1922270..7e14c5f 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -2,6 +2,7 @@ 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') @@ -10,6 +11,12 @@ const readFile = promisify(fs.readFile) const writeFile = promisify(fs.writeFile) const mkdir = promisify(fs.mkdir) +const signale = new Signale({ + config: { + displayLabel: false + } +}) + /** * Reads a template file from disk. * @@ -121,26 +128,28 @@ module.exports = async function createAction (argv) { const args = minimist(argv) const directoryName = args._[0] if (!directoryName || args.help) { - console.log(`\nUsage: npx actions-toolkit `) + signale.info(`\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 { - console.log(`Creating folder ${base}...`) + signale.info(`Creating folder ${base}...`) await mkdir(base) } catch (err) { if (err.code === 'EEXIST') { - throw new Error(`Folder ${base} already exists!`) - } else { - throw err + signale.fatal(`Folder ${base} already exists!`) } + throw err } - console.log('\nWelcome to actions-toolkit! Let\'s get started creating an action.\n') - // Collect answers const metadata = await getActionMetadata() + signale.log('\n------------------------------------\n') + // Create the templated files const dockerfile = await createDockerfile(metadata) const packageJson = createPackageJson(directoryName) @@ -151,9 +160,11 @@ module.exports = async function createAction (argv) { ['Dockerfile', dockerfile], ['entrypoint.js', entrypoint] ].map(async ([filename, contents]) => { - console.log(`Creating ${filename}...`) + signale.info(`Creating ${filename}...`) await writeFile(path.join(base, filename), contents) })) - console.log(`\n✔ Done! Enjoy building your GitHub Action! Get started with:\n\ncd ${directoryName} && npm install`) + signale.log('\n------------------------------------\n') + signale.success(`Done! Enjoy building your GitHub Action!`) + signale.info(`Get started with:\n\ncd ${directoryName} && npm install`) } From b89f13a43c940d76dc8ef8dc0cf543684d93940c Mon Sep 17 00:00:00 2001 From: Mackie Underdown Date: Sat, 23 Mar 2019 13:09:40 -0700 Subject: [PATCH 17/19] Update tests to use real filesystem --- bin/create-action.js | 13 ++- package-lock.json | 8 +- package.json | 1 + tests/__mocks__/fs.js | 21 ----- .../__snapshots__/create-action.test.js.snap | 2 +- tests/create-action.test.js | 91 ++++++++++--------- .../action-already-exists/Dockerfile | 1 + 7 files changed, 60 insertions(+), 77 deletions(-) delete mode 100644 tests/__mocks__/fs.js create mode 100644 tests/fixtures/workspaces/action-already-exists/Dockerfile diff --git a/bin/create-action.js b/bin/create-action.js index 7e14c5f..9713312 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -11,12 +11,6 @@ const readFile = promisify(fs.readFile) const writeFile = promisify(fs.writeFile) const mkdir = promisify(fs.mkdir) -const signale = new Signale({ - config: { - displayLabel: false - } -}) - /** * Reads a template file from disk. * @@ -122,9 +116,14 @@ function createPackageJson (name) { * * @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) { +module.exports = async function createAction (argv, signale = new Signale({ + config: { + displayLabel: false + } +})) { const args = minimist(argv) const directoryName = args._[0] if (!directoryName || args.help) { diff --git a/package-lock.json b/package-lock.json index a22bac8..e91c5a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4945,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": { diff --git a/package.json b/package.json index 6057e35..623d920 100644 --- a/package.json +++ b/package.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", diff --git a/tests/__mocks__/fs.js b/tests/__mocks__/fs.js deleted file mode 100644 index 4d66a7f..0000000 --- a/tests/__mocks__/fs.js +++ /dev/null @@ -1,21 +0,0 @@ -const fs = jest.genMockFromModule('fs') -const realFs = jest.requireActual('fs') - -const fileHolder = new Map() - -// Allow reading from disk. -fs.readFile = realFs.readFile - -// Write file contents to memory. -fs.writeFile = jest.fn((path, contents, cb) => { - fileHolder.set(path, contents) - // In mock world, we can never fail. :') - cb(null) -}) - -fs.mkdir = jest.fn((path, cb) => { cb(null) }) - -// Add a helper method for getting memory FS. -fs.__getContents = path => fileHolder.get(path) - -module.exports = fs diff --git a/tests/__snapshots__/create-action.test.js.snap b/tests/__snapshots__/create-action.test.js.snap index 92abb0d..bd4ecf3 100644 --- a/tests/__snapshots__/create-action.test.js.snap +++ b/tests/__snapshots__/create-action.test.js.snap @@ -45,7 +45,7 @@ console.log(tools.arguments) exports[`creates project with labels passed to Dockerfile from questionnaire: package.json 1`] = ` { - "name": "my-project-name", + "name": "__tmp/my-project-name", "private": true, "main": "index.js", "dependencies": { diff --git a/tests/create-action.test.js b/tests/create-action.test.js index bf525dc..6dd0c2a 100644 --- a/tests/create-action.test.js +++ b/tests/create-action.test.js @@ -1,3 +1,9 @@ +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({ @@ -5,35 +11,44 @@ expect.addSnapshotSerializer({ print: value => value }) -jest.mock('fs') -jest.mock('enquirer') - -let mockFs +/** 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() +}) -const runCLI = (...args) => { - const createAction = require('../bin/create-action') - return createAction(args) -} +let logger beforeEach(() => { jest.resetModules() - mockFs = require('fs') process.exit = jest.fn() - console.log = jest.fn() - console.error = jest.fn() - process.cwd = 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(console.log).toHaveBeenCalledWith( + expect(logger.info).toHaveBeenCalledWith( expect.stringMatching(/Usage: npx actions-toolkit /) ) }) @@ -42,33 +57,18 @@ test('prints help when --help is passed', async () => { await runCLI('--help') expect(process.exit).toHaveBeenCalledWith(1) - expect(console.log).toHaveBeenCalledWith( + expect(logger.info).toHaveBeenCalledWith( expect.stringMatching(/Usage: npx actions-toolkit /) ) }) test('fails to start creating project in a directory that already exists', async () => { - mockFs.mkdir.mockImplementationOnce((_, cb) => { - const error = new Error() - error.code = 'EEXIST' - cb(error) - }) - - await expect(runCLI('my-project-name')).rejects.toThrowError( - /Folder my-project-name already exists!/ + 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('throws unhandled fs.mkdir errors', async () => { - mockFs.mkdir.mockImplementationOnce((_, cb) => { - const error = new Error("EPERM: operation not permitted, mkdir '/etc'") - error.code = 'EPERM' - cb(error) - }) - - await expect(runCLI('my-project-name')).rejects.toThrowError(/EPERM: operation not permitted, mkdir '\/etc'/) -}) - 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') @@ -76,37 +76,40 @@ test('exits with a failure message when a user cancels the questionnaire', async throw new Error() }) - await expect(runCLI('my-project-name')).rejects.toThrowError() + 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' }) - mockFs.mkdir.mockImplementationOnce((_, cb) => cb(null)) - await runCLI('my-project-name') + const readGeneratedFile = name => + fs.readFileSync(path.resolve(__dirname, '../__tmp/my-project-name', name), 'utf-8') + + await runCLI('__tmp/my-project-name') - expect(console.log).toHaveBeenCalledWith( - expect.stringMatching(/Creating folder my-project-name.../) + expect(logger.info).toHaveBeenCalledWith( + expect.stringMatching(/Creating folder .*my-project-name.../) ) - expect(console.log).toHaveBeenCalledWith( + expect(logger.star).toHaveBeenCalledWith( expect.stringMatching(/Welcome to actions-toolkit/) ) - expect(console.log).toHaveBeenCalledWith( + expect(logger.info).toHaveBeenCalledWith( expect.stringMatching(/Creating package.json/) ) - expect(console.log).toHaveBeenCalledWith( + expect(logger.info).toHaveBeenCalledWith( expect.stringMatching(/Creating Dockerfile/) ) - expect(console.log).toHaveBeenCalledWith( + expect(logger.info).toHaveBeenCalledWith( expect.stringMatching(/Creating entrypoint.js/) ) - expect(mockFs.__getContents('my-project-name/package.json')).toMatchSnapshot('package.json') - expect(mockFs.__getContents('my-project-name/Dockerfile')).toMatchSnapshot('Dockerfile') - expect(mockFs.__getContents('my-project-name/entrypoint.js')).toMatchSnapshot('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 From 8bbe9a0df25875050200ada913e7a3b5b6c7d83b Mon Sep 17 00:00:00 2001 From: Jason Etcovitch Date: Sat, 23 Mar 2019 16:53:23 -0400 Subject: [PATCH 18/19] Fix usage log --- bin/create-action.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/create-action.js b/bin/create-action.js index 9713312..dda2882 100755 --- a/bin/create-action.js +++ b/bin/create-action.js @@ -127,7 +127,7 @@ module.exports = async function createAction (argv, signale = new Signale({ const args = minimist(argv) const directoryName = args._[0] if (!directoryName || args.help) { - signale.info(`\nUsage: npx actions-toolkit `) + signale.log('\nUsage: npx actions-toolkit ') return process.exit(1) } From 3a4b2dc6312e8024a9c2d5397096239d32fc4095 Mon Sep 17 00:00:00 2001 From: Jason Etcovitch Date: Sat, 23 Mar 2019 16:56:36 -0400 Subject: [PATCH 19/19] Fix test --- tests/create-action.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/create-action.test.js b/tests/create-action.test.js index 6dd0c2a..62eea3b 100644 --- a/tests/create-action.test.js +++ b/tests/create-action.test.js @@ -48,7 +48,7 @@ test('prints help when no arguments are passed', async () => { await runCLI() expect(process.exit).toHaveBeenCalledWith(1) - expect(logger.info).toHaveBeenCalledWith( + expect(logger.log).toHaveBeenCalledWith( expect.stringMatching(/Usage: npx actions-toolkit /) ) }) @@ -57,7 +57,7 @@ test('prints help when --help is passed', async () => { await runCLI('--help') expect(process.exit).toHaveBeenCalledWith(1) - expect(logger.info).toHaveBeenCalledWith( + expect(logger.log).toHaveBeenCalledWith( expect.stringMatching(/Usage: npx actions-toolkit /) ) })