diff --git a/.eslintrc.js b/.eslintrc.js index 8991103..f6d6b22 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1199,10 +1199,10 @@ module.exports = { "error" ], "unicorn/no-null": [ - "error" + "off" ], "unicorn/no-process-exit": [ - "error" + "off" ], "unicorn/no-reduce": [ "off" diff --git a/CHANGELOG.md b/CHANGELOG.md index 810c69c..a085b65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v0.7.1 + +Show additional defaults in CLI help +Fix missing address index in cli +Added env var processing for GITHUB_TOKEN, GITHUB_REPOSITORY, GITHUB_REF and GTR_SEED +As we now support env vars for the required options it is valid to have no command line parameters +All parameter validation errors shown at once +Added --no-color option + ## v0.7.0 Refactored to include CLI diff --git a/README.md b/README.md index 6703650..051f453 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ jobs: steps: - name: Tangle Release id: tangle_release - uses: iotaledger/gh-tangle-release@v0.7.0 + uses: iotaledger/gh-tangle-release@v0.7.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} IOTA_SEED: ${{ secrets.IOTA_SEED }} @@ -107,7 +107,7 @@ gh-tangle-release You will then be presented with the following options. ```shell -GitHub Tangle Release v0.7.0 🚀 +GitHub Tangle Release v0.7.1 🚀 Usage: gh-tangle-release [options] @@ -123,13 +123,22 @@ Options: "https://nodes.iota.cafe:443") --depth Depth to use for attaching the transaction to the tangle (default: "3") --mwm Minimum weight magnitude to use for attaching the transaction to the tangle (default: "14") - --seed 81 Tryte seed used to generate addresses + --seed 81 Tryte seed used to generate addresses (required) --address-index Index number used to generate addresses (default: "0") --transaction-tag Tag to apply to the Tangle transaction (default: "GITHUB9RELEASE") --comment An optional comment to include in the Tangle transaction payload --explorer-url Url of the explorer to use for exploration link (default: "https://utils.iota.org/transaction/:hash") + --no-color Disable colored output --help Display help +You can also supply some of the options through environment variables: + --github-token: GITHUB_TOKEN + --owner: GITHUB_REPOSITORY[0] + --repository: GITHUB_REPOSITORY[1] + where GITHUB_REPOSITORY is formatted owner/repository + --release-tag: GITHUB_REF + --seed: GTR_SEED + Example: gh-tangle-release --github-token a4d936470cb3d66f5434f787c2500bde9764f --owner my-org --repository my-repo --release-tag v1.0.1 --seed AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ``` diff --git a/action/index.js b/action/index.js index 67b98cb..4055911 100644 --- a/action/index.js +++ b/action/index.js @@ -2458,33 +2458,34 @@ const iota_1 = __webpack_require__(586); * @returns The config as non partial. */ function sanitizeInput(config) { + const errors = []; if (!config.githubToken) { - throw new Error("You must provide the GitHub token option"); + errors.push("You must provide the GitHub token option"); } if (!config.owner) { - throw new Error("You must provide the owner option"); + errors.push("You must provide the owner option"); } if (!config.repository) { - throw new Error("You must provide the repository option"); + errors.push("You must provide the repository option"); } if (!config.releaseTag) { - throw new Error("You must provide the release tag option"); + errors.push("You must provide the release tag option"); } if (!config.seed) { - throw new Error("You must provide the seed option"); + errors.push("You must provide the seed option"); } - if (!/[9A-Z]/.test(config.seed)) { - throw new Error("The seed option must be 81 trytes [A-Z9]"); + else if (!/[9A-Z]/.test(config.seed)) { + errors.push("The seed option must be 81 trytes [A-Z9]"); } - if (config.seed.length !== 81) { - throw new Error(`The seed option must be 81 trytes [A-Z9], it is ${config.seed.length}`); + else if (config.seed.length !== 81) { + errors.push(`The seed option must be 81 trytes [A-Z9], it is ${config.seed.length}`); } config.transactionTag = config.transactionTag || "GITHUB9RELEASE"; if (!/[9A-Z]/.test(config.transactionTag)) { - throw new Error("The transaction tag option must be 27 trytes [A-Z9] or less"); + errors.push("The transaction tag option must be 27 trytes [A-Z9] or less"); } - if (config.transactionTag.length >= 27) { - throw new Error(`The transaction tag option must be 27 trytes [A-Z9] or less, it is ${config.transactionTag.length}`); + if (config.transactionTag.length > 27) { + errors.push(`The transaction tag option must be 27 trytes [A-Z9] or less, it is ${config.transactionTag.length}`); } config.explorerUrl = config.explorerUrl || "https://utils.iota.org/transaction/:hash"; config.node = config.node || "https://nodes.iota.cafe:443"; @@ -2518,15 +2519,18 @@ function sanitizeInput(config) { else { mwm = config.mwm; } + if (errors.length > 0) { + throw new Error(errors.join("\n")); + } return { - githubToken: config.githubToken, - owner: config.owner, - repository: config.repository, - releaseTag: config.releaseTag, + githubToken: config.githubToken || "", + owner: config.owner || "", + repository: config.repository || "", + releaseTag: config.releaseTag || "", node: config.node, depth, mwm, - seed: config.seed, + seed: config.seed || "", addressIndex, transactionTag: config.transactionTag, comment: config.comment, diff --git a/dist/cli-core.js b/dist/cli-core.js new file mode 100644 index 0000000..7507f59 --- /dev/null +++ b/dist/cli-core.js @@ -0,0 +1,138 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.cliCore = void 0; +const chalk_1 = __importDefault(require("chalk")); +const commander_1 = require("commander"); +const node_emoji_1 = __importDefault(require("node-emoji")); +const core_1 = require("./core"); +/** + * Execute the cli core. + * @param argv The command line arguments. + * @param env The environment variables. + * @param display Method to output display. + */ +function cliCore(argv, env, display) { + return __awaiter(this, void 0, void 0, function* () { + const program = new commander_1.Command(); + try { + const version = "0.7.1"; + program + .storeOptionsAsProperties(false) + .passCommandToAction(false) + .name(chalk_1.default.yellowBright("gh-tangle-release")) + .version(version, "-v, --version", chalk_1.default.yellowBright("output the current version")) + .description(chalk_1.default.cyan("An application for creating a transaction on the IOTA Tangle from a GitHub release.")) + .option("--github-token ", chalk_1.default.yellowBright("GitHub token for accessing your repository (required)")) + .option("--owner ", chalk_1.default.yellowBright("GitHub repository owner (required)")) + .option("--repository ", chalk_1.default.yellowBright("GitHub repository (required)")) + .option("--release-tag ", chalk_1.default.yellowBright("The release tag from the GitHub repository (required)")) + .option("--node ", chalk_1.default.yellowBright("Url of the node to use for attaching the transaction to the tangle"), "https://nodes.iota.cafe:443") + .option("--depth ", chalk_1.default.yellowBright("Depth to use for attaching the transaction to the tangle"), "3") + .option("--mwm ", chalk_1.default.yellowBright("Minimum weight magnitude to use for attaching the transaction to the tangle"), "14") + .option("--seed ", chalk_1.default.yellowBright("81 Tryte seed used to generate addresses (required)")) + .option("--address-index ", chalk_1.default.yellowBright("Index number used to generate addresses"), "0") + .option("--transaction-tag ", chalk_1.default.yellowBright("Tag to apply to the Tangle transaction"), "GITHUB9RELEASE") + .option("--comment ", chalk_1.default.yellowBright("An optional comment to include in the Tangle transaction payload")) + .option("--explorer-url ", chalk_1.default.yellowBright("Url of the explorer to use for exploration link"), "https://utils.iota.org/transaction/:hash") + .option("--no-color", chalk_1.default.yellowBright("Disable colored output")) + .helpOption("--help", chalk_1.default.yellowBright("Display help")); + program.parse(argv); + const opts = program.opts(); + console.log(opts); + display(chalk_1.default.green(`GitHub Tangle Release v${version} ${opts.color === false ? "" : node_emoji_1.default.get("rocket")}\n`)); + const envRepo = env.GITHUB_REPOSITORY ? env.GITHUB_REPOSITORY.split("/") : []; + if (envRepo.length === 2) { + opts.owner = opts.owner || envRepo[0]; + opts.repository = opts.repository || envRepo[1]; + } + const config = core_1.sanitizeInput({ + githubToken: opts.githubToken || env.GITHUB_TOKEN, + owner: opts.owner, + repository: opts.repository, + releaseTag: opts.releaseTag || env.GITHUB_REF, + node: opts.node, + depth: opts.depth, + mwm: opts.mwm, + seed: opts.seed || env.GTR_SEED, + addressIndex: opts.addressIndex, + transactionTag: opts.transactionTag, + comment: opts.comment, + explorerUrl: opts.explorerUrl + }); + display("Options:"); + display(chalk_1.default.cyan("\tGitHub Token"), chalk_1.default.white("*******")); + display(chalk_1.default.cyan("\tOwner"), chalk_1.default.white(config.owner)); + display(chalk_1.default.cyan("\tRepository"), chalk_1.default.white(config.repository)); + display(chalk_1.default.cyan("\tRelease Tag"), chalk_1.default.white(config.releaseTag)); + display(chalk_1.default.cyan("\tNode"), chalk_1.default.white(config.node)); + display(chalk_1.default.cyan("\tDepth"), chalk_1.default.white(config.depth)); + display(chalk_1.default.cyan("\tMWM"), chalk_1.default.white(config.mwm)); + display(chalk_1.default.cyan("\tSeed"), chalk_1.default.white("*******")); + display(chalk_1.default.cyan("\tAddress Index"), chalk_1.default.white(config.addressIndex)); + display(chalk_1.default.cyan("\tTransaction Tag"), chalk_1.default.white(config.transactionTag)); + if (config.comment) { + display(chalk_1.default.cyan("\tComment"), chalk_1.default.white(config.comment)); + } + display(chalk_1.default.cyan("\tExplorer Url"), chalk_1.default.white(config.explorerUrl)); + display(""); + try { + const transactionDetails = yield core_1.tangleRelease(config, message => display(chalk_1.default.green(message))); + display("Transaction Hash:", chalk_1.default.cyan(transactionDetails.hash)); + display("You can view the transaction on the tangle at:", chalk_1.default.cyan(transactionDetails.url)); + display(chalk_1.default.green("Complete")); + } + catch (err) { + display(""); + display(createErrors(err)); + process.exit(1); + } + } + catch (err) { + program.help(str => `${str}${createEnvHelp()}${createExample()}${createErrors(err)}`); + process.exit(1); + } + }); +} +exports.cliCore = cliCore; +/** + * Show an example on the console. + * @returns The example text. + */ +function createExample() { + // eslint-disable-next-line max-len + return chalk_1.default.magenta("\nExample: gh-tangle-release --github-token a4d936470cb3d66f5434f787c2500bde9764f --owner my-org --repository my-repo --release-tag v1.0.1 --seed AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\n"); +} +/** + * Show additional info about env vars. + * @returns The additional information. + */ +function createEnvHelp() { + return ` +${chalk_1.default.cyan("You can also supply some of the options through environment variables:")} + ${chalk_1.default.cyan("--github-token: GITHUB_TOKEN")} + ${chalk_1.default.cyan("--owner: GITHUB_REPOSITORY[0]")} + ${chalk_1.default.cyan("--repository: GITHUB_REPOSITORY[1]")} + ${chalk_1.default.cyan(" where GITHUB_REPOSITORY is formatted owner/repository")} + ${chalk_1.default.cyan("--release-tag: GITHUB_REF")} + ${chalk_1.default.cyan("--seed: GTR_SEED")}\n\n`; +} +/** + * Show the errors. + * @param error The error that was thrown. + * @returns The formatted errors. + */ +function createErrors(error) { + return chalk_1.default.red(`The following errors occurred:\n ${error.message.replace(/\n/g, "\n ")}`); +} diff --git a/dist/cli.js b/dist/cli.js index 29e1311..472e37b 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -1,95 +1,11 @@ #!/usr/bin/env node "use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; Object.defineProperty(exports, "__esModule", { value: true }); -const chalk_1 = __importDefault(require("chalk")); -const commander_1 = require("commander"); -const node_emoji_1 = __importDefault(require("node-emoji")); -const core_1 = require("./core"); -const version = "0.7.0"; -console.log(chalk_1.default.green(`GitHub Tangle Release v${version} ${node_emoji_1.default.get("rocket")}\n`)); -const program = new commander_1.Command(); -program - .storeOptionsAsProperties(false) - .passCommandToAction(false); -program - .name(chalk_1.default.yellowBright("gh-tangle-release")) - .version(version, "-v, --version", chalk_1.default.yellowBright("output the current version")) - .description(chalk_1.default.cyan("An application for creating a transaction on the IOTA Tangle from a GitHub release.")) - .option("--github-token ", chalk_1.default.yellowBright("GitHub token for accessing your repository")) - .option("--owner ", chalk_1.default.yellowBright("GitHub repository owner")) - .option("--repository ", chalk_1.default.yellowBright("GitHub repository")) - .option("--release-tag ", chalk_1.default.yellowBright("The release tag from the GitHub repository")) - .option("--node ", chalk_1.default.yellowBright("Url of the node to use for attaching the transaction to the tangle"), "https://nodes.iota.cafe:443") - .option("--depth ", chalk_1.default.yellowBright("Depth to use for attaching the transaction to the tangle"), "3") - .option("--mwm ", chalk_1.default.yellowBright("Minimum weight magnitude to use for attaching the transaction to the tangle"), "14") - .option("--seed ", chalk_1.default.yellowBright("81 Tryte seed used to generate addresses")) - .option("--address-index ", chalk_1.default.yellowBright("Index number used to generate addresses", "0")) - .option("--transaction-tag ", chalk_1.default.yellowBright("Tag to apply to the Tangle transaction")) - .option("--comment ", chalk_1.default.yellowBright("An optional comment to include in the Tangle transaction payload")) - .option("--explorer-url ", chalk_1.default.yellowBright("Url of the explorer to use for exploration link"), "https://utils.iota.org/transaction/:hash") - .helpOption("--help", chalk_1.default.yellowBright("Display help")); -if (process.argv.length === 2) { - program.help(str => `${str}${createExample()}`); -} -else { - try { - program.parse(process.argv); - const opts = program.opts(); - const config = core_1.sanitizeInput({ - githubToken: opts.githubToken, - owner: opts.owner, - repository: opts.repository, - releaseTag: opts.releaseTag, - node: opts.node, - depth: opts.depth, - mwm: opts.mwm, - seed: opts.seed, - // eslint-disable-next-line unicorn/no-null - addressIndex: null, - transactionTag: opts.transactionTag, - comment: opts.comment, - explorerUrl: opts.explorerUrl - }); - console.log("Options:"); - console.log(chalk_1.default.cyan("\tGitHub Token"), chalk_1.default.white("*******")); - console.log(chalk_1.default.cyan("\tOwner"), chalk_1.default.white(config.owner)); - console.log(chalk_1.default.cyan("\tRepository"), chalk_1.default.white(config.repository)); - console.log(chalk_1.default.cyan("\tRelease Tag"), chalk_1.default.white(config.releaseTag)); - console.log(chalk_1.default.cyan("\tNode"), chalk_1.default.white(config.node)); - console.log(chalk_1.default.cyan("\tDepth"), chalk_1.default.white(config.depth)); - console.log(chalk_1.default.cyan("\tMWM"), chalk_1.default.white(config.mwm)); - console.log(chalk_1.default.cyan("\tSeed"), chalk_1.default.white("*******")); - console.log(chalk_1.default.cyan("\tAddress Index"), chalk_1.default.white(config.addressIndex)); - console.log(chalk_1.default.cyan("\tTransaction Tag"), chalk_1.default.white(config.transactionTag)); - if (config.comment) { - console.log(chalk_1.default.cyan("\tComment"), chalk_1.default.white(config.comment)); - } - console.log(chalk_1.default.cyan("\tExplorer Url"), chalk_1.default.white(config.explorerUrl)); - console.log(); - core_1.tangleRelease(config, message => console.log(chalk_1.default.green(message))) - .then(transactionDetails => { - console.log("Transaction Hash:", chalk_1.default.cyan(transactionDetails.hash)); - console.log("You can view the transaction on the tangle at:", chalk_1.default.cyan(transactionDetails.url)); - console.log(chalk_1.default.green("Complete")); - }) - .catch(err => { - console.log(); - console.error(chalk_1.default.red(err)); - process.exit(1); - }); +const cli_core_1 = require("./cli-core"); +cli_core_1.cliCore(process.argv, process.env, (message, param) => { + process.stdout.write(message); + if (param) { + process.stdout.write(` ${param}`); } - catch (err) { - program.help(str => `${str}${chalk_1.default.red(`Error: ${err.message}`)}`); - } -} -/** - * Show an example on the console. - * @returns The example text. - */ -function createExample() { - // eslint-disable-next-line max-len - return chalk_1.default.magenta("\nExample: gh-tangle-release --github-token a4d936470cb3d66f5434f787c2500bde9764f --owner my-org --repository my-repo --release-tag v1.0.1 --seed AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"); -} + process.stdout.write("\n"); +}).catch(err => process.stderr.write(err.message)); diff --git a/dist/core.js b/dist/core.js index 7248168..8420601 100644 --- a/dist/core.js +++ b/dist/core.js @@ -20,33 +20,34 @@ const iota_1 = require("./iota"); * @returns The config as non partial. */ function sanitizeInput(config) { + const errors = []; if (!config.githubToken) { - throw new Error("You must provide the GitHub token option"); + errors.push("You must provide the GitHub token option"); } if (!config.owner) { - throw new Error("You must provide the owner option"); + errors.push("You must provide the owner option"); } if (!config.repository) { - throw new Error("You must provide the repository option"); + errors.push("You must provide the repository option"); } if (!config.releaseTag) { - throw new Error("You must provide the release tag option"); + errors.push("You must provide the release tag option"); } if (!config.seed) { - throw new Error("You must provide the seed option"); + errors.push("You must provide the seed option"); } - if (!/[9A-Z]/.test(config.seed)) { - throw new Error("The seed option must be 81 trytes [A-Z9]"); + else if (!/[9A-Z]/.test(config.seed)) { + errors.push("The seed option must be 81 trytes [A-Z9]"); } - if (config.seed.length !== 81) { - throw new Error(`The seed option must be 81 trytes [A-Z9], it is ${config.seed.length}`); + else if (config.seed.length !== 81) { + errors.push(`The seed option must be 81 trytes [A-Z9], it is ${config.seed.length}`); } config.transactionTag = config.transactionTag || "GITHUB9RELEASE"; if (!/[9A-Z]/.test(config.transactionTag)) { - throw new Error("The transaction tag option must be 27 trytes [A-Z9] or less"); + errors.push("The transaction tag option must be 27 trytes [A-Z9] or less"); } - if (config.transactionTag.length >= 27) { - throw new Error(`The transaction tag option must be 27 trytes [A-Z9] or less, it is ${config.transactionTag.length}`); + if (config.transactionTag.length > 27) { + errors.push(`The transaction tag option must be 27 trytes [A-Z9] or less, it is ${config.transactionTag.length}`); } config.explorerUrl = config.explorerUrl || "https://utils.iota.org/transaction/:hash"; config.node = config.node || "https://nodes.iota.cafe:443"; @@ -80,15 +81,18 @@ function sanitizeInput(config) { else { mwm = config.mwm; } + if (errors.length > 0) { + throw new Error(errors.join("\n")); + } return { - githubToken: config.githubToken, - owner: config.owner, - repository: config.repository, - releaseTag: config.releaseTag, + githubToken: config.githubToken || "", + owner: config.owner || "", + repository: config.repository || "", + releaseTag: config.releaseTag || "", node: config.node, depth, mwm, - seed: config.seed, + seed: config.seed || "", addressIndex, transactionTag: config.transactionTag, comment: config.comment, diff --git a/package-lock.json b/package-lock.json index 6305765..56a293d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@iota/gh-tangle-release", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0a400e7..4388317 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iota/gh-tangle-release", - "version": "0.7.0", + "version": "0.7.1", "description": "Create a release and adds metadata to the IOTA Tangle", "main": "dist/index.js", "module": "es/index.js", diff --git a/src/cli-core.ts b/src/cli-core.ts new file mode 100644 index 0000000..cc176b5 --- /dev/null +++ b/src/cli-core.ts @@ -0,0 +1,151 @@ +import chalk from "chalk"; +import { Command } from "commander"; +import emoji from "node-emoji"; +import { sanitizeInput, tangleRelease } from "./core"; + +/** + * Execute the cli core. + * @param argv The command line arguments. + * @param env The environment variables. + * @param display Method to output display. + */ +export async function cliCore( + argv: string[], + env: { [id: string]: string | undefined }, + display: (message: string, param?: string) => void): Promise { + const program = new Command(); + + try { + const version = "0.7.1"; + + program + .storeOptionsAsProperties(false) + .passCommandToAction(false) + .name(chalk.yellowBright("gh-tangle-release")) + .version(version, "-v, --version", chalk.yellowBright("output the current version")) + .description( + chalk.cyan("An application for creating a transaction on the IOTA Tangle from a GitHub release.")) + .option("--github-token ", chalk.yellowBright( + "GitHub token for accessing your repository (required)")) + .option("--owner ", chalk.yellowBright("GitHub repository owner (required)")) + .option("--repository ", chalk.yellowBright("GitHub repository (required)")) + .option("--release-tag ", + chalk.yellowBright("The release tag from the GitHub repository (required)")) + .option("--node ", + chalk.yellowBright("Url of the node to use for attaching the transaction to the tangle"), + "https://nodes.iota.cafe:443") + .option("--depth ", chalk.yellowBright("Depth to use for attaching the transaction to the tangle"), + "3") + .option("--mwm ", + chalk.yellowBright("Minimum weight magnitude to use for attaching the transaction to the tangle"), + "14") + .option("--seed ", chalk.yellowBright("81 Tryte seed used to generate addresses (required)")) + .option("--address-index ", chalk.yellowBright("Index number used to generate addresses"), "0") + .option("--transaction-tag ", chalk.yellowBright("Tag to apply to the Tangle transaction"), + "GITHUB9RELEASE") + .option("--comment ", + chalk.yellowBright("An optional comment to include in the Tangle transaction payload")) + .option("--explorer-url ", chalk.yellowBright("Url of the explorer to use for exploration link"), + "https://utils.iota.org/transaction/:hash") + .option("--no-color", chalk.yellowBright("Disable colored output")) + .helpOption("--help", + chalk.yellowBright("Display help")); + + program.parse(argv); + const opts = program.opts(); + + console.log(opts); + + display(chalk.green(`GitHub Tangle Release v${version} ${ + opts.color === false ? "" : emoji.get("rocket")}\n`)); + + const envRepo: string[] = env.GITHUB_REPOSITORY ? env.GITHUB_REPOSITORY.split("/") : []; + + if (envRepo.length === 2) { + opts.owner = opts.owner || envRepo[0]; + opts.repository = opts.repository || envRepo[1]; + } + + const config = sanitizeInput({ + githubToken: opts.githubToken || env.GITHUB_TOKEN, + owner: opts.owner, + repository: opts.repository, + releaseTag: opts.releaseTag || env.GITHUB_REF, + node: opts.node, + depth: opts.depth, + mwm: opts.mwm, + seed: opts.seed || env.GTR_SEED, + addressIndex: opts.addressIndex, + transactionTag: opts.transactionTag, + comment: opts.comment, + explorerUrl: opts.explorerUrl + }); + + display("Options:"); + display(chalk.cyan("\tGitHub Token"), chalk.white("*******")); + display(chalk.cyan("\tOwner"), chalk.white(config.owner)); + display(chalk.cyan("\tRepository"), chalk.white(config.repository)); + display(chalk.cyan("\tRelease Tag"), chalk.white(config.releaseTag)); + display(chalk.cyan("\tNode"), chalk.white(config.node)); + display(chalk.cyan("\tDepth"), chalk.white(config.depth)); + display(chalk.cyan("\tMWM"), chalk.white(config.mwm)); + display(chalk.cyan("\tSeed"), chalk.white("*******")); + display(chalk.cyan("\tAddress Index"), chalk.white(config.addressIndex)); + display(chalk.cyan("\tTransaction Tag"), chalk.white(config.transactionTag)); + if (config.comment) { + display(chalk.cyan("\tComment"), chalk.white(config.comment)); + } + display(chalk.cyan("\tExplorer Url"), chalk.white(config.explorerUrl)); + display(""); + + try { + const transactionDetails = await tangleRelease(config, message => display(chalk.green(message))); + + display("Transaction Hash:", + chalk.cyan(transactionDetails.hash)); + display("You can view the transaction on the tangle at:", + chalk.cyan(transactionDetails.url)); + display(chalk.green("Complete")); + } catch (err) { + display(""); + display(createErrors(err)); + process.exit(1); + } + } catch (err) { + program.help(str => `${str}${createEnvHelp()}${createExample()}${createErrors(err)}`); + process.exit(1); + } +} + +/** + * Show an example on the console. + * @returns The example text. + */ +function createExample(): string { + // eslint-disable-next-line max-len + return chalk.magenta("\nExample: gh-tangle-release --github-token a4d936470cb3d66f5434f787c2500bde9764f --owner my-org --repository my-repo --release-tag v1.0.1 --seed AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\n"); +} + +/** + * Show additional info about env vars. + * @returns The additional information. + */ +function createEnvHelp(): string { + return ` +${chalk.cyan("You can also supply some of the options through environment variables:")} + ${chalk.cyan("--github-token: GITHUB_TOKEN")} + ${chalk.cyan("--owner: GITHUB_REPOSITORY[0]")} + ${chalk.cyan("--repository: GITHUB_REPOSITORY[1]")} + ${chalk.cyan(" where GITHUB_REPOSITORY is formatted owner/repository")} + ${chalk.cyan("--release-tag: GITHUB_REF")} + ${chalk.cyan("--seed: GTR_SEED")}\n\n`; +} + +/** + * Show the errors. + * @param error The error that was thrown. + * @returns The formatted errors. + */ +function createErrors(error: Error): string { + return chalk.red(`The following errors occurred:\n ${error.message.replace(/\n/g, "\n ")}`); +} diff --git a/src/cli.ts b/src/cli.ts index 742118b..e0127b1 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,106 +1,10 @@ #!/usr/bin/env node -import chalk from "chalk"; -import { Command } from "commander"; -import emoji from "node-emoji"; -import { sanitizeInput, tangleRelease } from "./core"; +import { cliCore } from "./cli-core"; -const version = "0.7.0"; -console.log(chalk.green(`GitHub Tangle Release v${version} ${emoji.get("rocket")}\n`)); - -const program = new Command(); - -program - .storeOptionsAsProperties(false) - .passCommandToAction(false); - -program - .name(chalk.yellowBright("gh-tangle-release")) - .version(version, "-v, --version", chalk.yellowBright("output the current version")) - .description(chalk.cyan("An application for creating a transaction on the IOTA Tangle from a GitHub release.")) - .option("--github-token ", chalk.yellowBright("GitHub token for accessing your repository (required)")) - .option("--owner ", chalk.yellowBright("GitHub repository owner (required)")) - .option("--repository ", chalk.yellowBright("GitHub repository (required)")) - .option("--release-tag ", chalk.yellowBright("The release tag from the GitHub repository (required)")) - .option("--node ", chalk.yellowBright("Url of the node to use for attaching the transaction to the tangle"), - "https://nodes.iota.cafe:443") - .option("--depth ", chalk.yellowBright("Depth to use for attaching the transaction to the tangle"), - "3") - .option("--mwm ", - chalk.yellowBright("Minimum weight magnitude to use for attaching the transaction to the tangle"), - "14") - .option("--seed ", chalk.yellowBright("81 Tryte seed used to generate addresses (required)")) - .option("--address-index ", chalk.yellowBright("Index number used to generate addresses"), "0") - .option("--transaction-tag ", chalk.yellowBright("Tag to apply to the Tangle transaction"), "GITHUB9RELEASE") - .option("--comment ", - chalk.yellowBright("An optional comment to include in the Tangle transaction payload")) - .option("--explorer-url ", chalk.yellowBright("Url of the explorer to use for exploration link"), - "https://utils.iota.org/transaction/:hash") - .helpOption("--help", - chalk.yellowBright("Display help")); - -if (process.argv.length === 2) { - program.help(str => `${str}${createExample()}`); -} else { - try { - program.parse(process.argv); - - const opts = program.opts(); - - const config = sanitizeInput({ - githubToken: opts.githubToken, - owner: opts.owner, - repository: opts.repository, - releaseTag: opts.releaseTag, - node: opts.node, - depth: opts.depth, - mwm: opts.mwm, - seed: opts.seed, - // eslint-disable-next-line unicorn/no-null - addressIndex: null as unknown as number, // opts.addressIndex, - transactionTag: opts.transactionTag, - comment: opts.comment, - explorerUrl: opts.explorerUrl - }); - console.log("Options:"); - console.log(chalk.cyan("\tGitHub Token"), chalk.white("*******")); - console.log(chalk.cyan("\tOwner"), chalk.white(config.owner)); - console.log(chalk.cyan("\tRepository"), chalk.white(config.repository)); - console.log(chalk.cyan("\tRelease Tag"), chalk.white(config.releaseTag)); - console.log(chalk.cyan("\tNode"), chalk.white(config.node)); - console.log(chalk.cyan("\tDepth"), chalk.white(config.depth)); - console.log(chalk.cyan("\tMWM"), chalk.white(config.mwm)); - console.log(chalk.cyan("\tSeed"), chalk.white("*******")); - console.log(chalk.cyan("\tAddress Index"), chalk.white(config.addressIndex)); - console.log(chalk.cyan("\tTransaction Tag"), chalk.white(config.transactionTag)); - if (config.comment) { - console.log(chalk.cyan("\tComment"), chalk.white(config.comment)); - } - console.log(chalk.cyan("\tExplorer Url"), chalk.white(config.explorerUrl)); - console.log(); - - tangleRelease(config, message => console.log(chalk.green(message))) - .then(transactionDetails => { - console.log("Transaction Hash:", - chalk.cyan(transactionDetails.hash)); - console.log("You can view the transaction on the tangle at:", - chalk.cyan(transactionDetails.url)); - console.log(chalk.green("Complete")); - }) - .catch(err => { - console.log(); - console.error(chalk.red(err)); - process.exit(1); - }); - } catch (err) { - program.help(str => `${str}${chalk.red(`Error: ${err.message}`)}`); +cliCore(process.argv, process.env, (message, param) => { + process.stdout.write(message); + if (param) { + process.stdout.write(` ${param}`); } -} - -/** - * Show an example on the console. - * @returns The example text. - */ -function createExample(): string { - // eslint-disable-next-line max-len - return chalk.magenta("\nExample: gh-tangle-release --github-token a4d936470cb3d66f5434f787c2500bde9764f --owner my-org --repository my-repo --release-tag v1.0.1 --seed AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"); -} + process.stdout.write("\n"); +}).catch(err => process.stderr.write(err.message)); diff --git a/src/core.ts b/src/core.ts index 0252da3..32d3e0f 100644 --- a/src/core.ts +++ b/src/core.ts @@ -12,35 +12,34 @@ import { IPayload } from "./models/IPayload"; * @returns The config as non partial. */ export function sanitizeInput(config: IPartialConfig): IConfig { + const errors: string[] = []; if (!config.githubToken) { - throw new Error("You must provide the GitHub token option"); + errors.push("You must provide the GitHub token option"); } if (!config.owner) { - throw new Error("You must provide the owner option"); + errors.push("You must provide the owner option"); } if (!config.repository) { - throw new Error("You must provide the repository option"); + errors.push("You must provide the repository option"); } if (!config.releaseTag) { - throw new Error("You must provide the release tag option"); + errors.push("You must provide the release tag option"); } if (!config.seed) { - throw new Error("You must provide the seed option"); - } - if (!/[9A-Z]/.test(config.seed)) { - throw new Error("The seed option must be 81 trytes [A-Z9]"); - } - if (config.seed.length !== 81) { - throw new Error(`The seed option must be 81 trytes [A-Z9], it is ${config.seed.length}`); + errors.push("You must provide the seed option"); + } else if (!/[9A-Z]/.test(config.seed)) { + errors.push("The seed option must be 81 trytes [A-Z9]"); + } else if (config.seed.length !== 81) { + errors.push(`The seed option must be 81 trytes [A-Z9], it is ${config.seed.length}`); } config.transactionTag = config.transactionTag || "GITHUB9RELEASE"; if (!/[9A-Z]/.test(config.transactionTag)) { - throw new Error("The transaction tag option must be 27 trytes [A-Z9] or less"); + errors.push("The transaction tag option must be 27 trytes [A-Z9] or less"); } if (config.transactionTag.length > 27) { - throw new Error(`The transaction tag option must be 27 trytes [A-Z9] or less, it is ${ + errors.push(`The transaction tag option must be 27 trytes [A-Z9] or less, it is ${ config.transactionTag.length}`); } @@ -75,15 +74,19 @@ export function sanitizeInput(config: IPartialConfig): IConfig { mwm = config.mwm; } + if (errors.length > 0) { + throw new Error(errors.join("\n")); + } + return { - githubToken: config.githubToken, - owner: config.owner, - repository: config.repository, - releaseTag: config.releaseTag, + githubToken: config.githubToken || "", + owner: config.owner || "", + repository: config.repository || "", + releaseTag: config.releaseTag || "", node: config.node, depth, mwm, - seed: config.seed, + seed: config.seed || "", addressIndex, transactionTag: config.transactionTag, comment: config.comment, diff --git a/tests/cli-core.spec.ts b/tests/cli-core.spec.ts new file mode 100644 index 0000000..438ab1f --- /dev/null +++ b/tests/cli-core.spec.ts @@ -0,0 +1,57 @@ +import chalk from "chalk"; +import { cliCore } from "../src/cli-core"; + +describe("CLI", () => { + let processExitSpy: jest.SpyInstance; + let processStdoutWriteSpy: jest.SpyInstance; + let output: string[] = []; + + const appendToOutput = (message: string | Uint8Array): void => { + output = output.concat( + message + .toString() + .split("\n") + .map(s => s.trim()) + ); + }; + + beforeAll(() => { + chalk.level = 0; + + processStdoutWriteSpy = jest.spyOn(process.stdout, "write").mockImplementation(message => { + appendToOutput(message); + return true; + }); + processExitSpy = jest.spyOn(process, "exit").mockImplementation(); + }); + + afterEach(() => { + output = []; + }); + + afterAll(() => { + processStdoutWriteSpy.mockRestore(); + processExitSpy.mockRestore(); + }); + + test("No Params", async () => { + await cliCore([], {}, appendToOutput); + expect(processExitSpy).toHaveBeenCalledWith(1); + expect(output).toContain("You must provide the GitHub token option"); + expect(output).toContain("You must provide the owner option"); + expect(output).toContain("You must provide the repository option"); + expect(output).toContain("You must provide the release tag option"); + expect(output).toContain("You must provide the seed option"); + }); + + test("Env Params", async () => { + await cliCore([], { + GITHUB_TOKEN: "g", + GITHUB_REPOSITORY: "o/r", + GITHUB_REF: "refs/heads/tag", + GTR_SEED: "S".repeat(81) + }, appendToOutput); + expect(processExitSpy).toHaveBeenCalledWith(1); + expect(output).toContain("Bad credentials"); + }); +}); diff --git a/tests/core.spec.ts b/tests/core.spec.ts index 8f6918d..5a5e10d 100644 --- a/tests/core.spec.ts +++ b/tests/core.spec.ts @@ -2,10 +2,6 @@ import { sanitizeInput } from "../src/core"; import { IPartialConfig } from "../src/models/IPartialConfig"; describe("Tangle Release", () => { - afterEach(() => { - jest.clearAllMocks(); - }); - test("No GITHUB_TOKEN", async () => { const config: IPartialConfig = {}; expect(() => sanitizeInput(config)).toThrow("You must provide the GitHub token option"); @@ -45,6 +41,54 @@ describe("Tangle Release", () => { expect(() => sanitizeInput(config)).toThrow("You must provide the seed option"); }); + test("Seed non trytes characters", async () => { + const config: IPartialConfig = { + githubToken: "aaa", + owner: "abc", + repository: "repo1/app1", + releaseTag: "v1", + seed: "aaa" + }; + expect(() => sanitizeInput(config)).toThrow("The seed option must be 81 trytes [A-Z9]"); + }); + + test("Seed wrong length", async () => { + const config: IPartialConfig = { + githubToken: "aaa", + owner: "abc", + repository: "repo1/app1", + releaseTag: "v1", + seed: "AAA" + }; + expect(() => sanitizeInput(config)).toThrow("The seed option must be 81 trytes [A-Z9], it is 3"); + }); + + test("Transaction tag non trytes characters", async () => { + const config: IPartialConfig = { + githubToken: "aaa", + owner: "abc", + repository: "repo1/app1", + releaseTag: "v1", + seed: "A".repeat(81), + transactionTag: "a" + }; + expect(() => + sanitizeInput(config)).toThrow("The transaction tag option must be 27 trytes [A-Z9] or less"); + }); + + test("Transaction tag length too long", async () => { + const config: IPartialConfig = { + githubToken: "aaa", + owner: "abc", + repository: "repo1/app1", + releaseTag: "v1", + seed: "A".repeat(81), + transactionTag: "A".repeat(28) + }; + expect(() => + sanitizeInput(config)).toThrow("The transaction tag option must be 27 trytes [A-Z9] or less, it is 28"); + }); + test("Sanitized partial input", async () => { const config: IPartialConfig = { githubToken: "aaa", @@ -81,7 +125,7 @@ describe("Tangle Release", () => { mwm: "2", addressIndex: 10, explorerUrl: "https://bar", - transactionTag: "TAGTAGTAG", + transactionTag: "T".repeat(27), comment: "Mmmmm" }; expect(sanitizeInput(config)).toEqual({ @@ -95,7 +139,7 @@ describe("Tangle Release", () => { mwm: 2, explorerUrl: "https://bar", seed: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - transactionTag: "TAGTAGTAG", + transactionTag: "T".repeat(27), comment: "Mmmmm" }); });