From bdb3c6cd2a2ca73dfa9d3830921a1a70fa1716f7 Mon Sep 17 00:00:00 2001 From: Kanad Gupta Date: Thu, 24 Feb 2022 12:48:19 -0600 Subject: [PATCH] feat: debugging support (#446) * feat: debug setup This installs a couple packages and sets up a logger for usage in normal and GHA environments * chore: fix imports * feat: use annotations for warnings * temp: attempt to surface warning * revert: skip warn function for now * chore: clean up logger * fix: incorrect import pattern * feat: add debugging statements for top-level + docs * fix: make fetch debug less noisy * fix: specify method in fetch debug * chore: more docs debug statements * chore: add command/opts debug statements to every command file * fix: undefined errors can't wait to use optional chaining lol * chore: cleanup * feat: add logging for handleRes * feat: add logging for `open` command * feat: `oas` command debug * feat: docs:edit debug * feat: version update/delete debug * feat: validate command debug * feat: lots of debugging for openapi command * refactor: prefix GHA logs, cleanup * feat: surface errors in GHA action annotations * chore: temporarily create failing example, see if it works * refactor: is this any cleaner? * revert: restore normal workflow states * Update bin/rdme Co-authored-by: Jon Ursenbach * chore: PR feedback Use ci-info package here instead! Feedback: https://github.com/readmeio/rdme/pull/446#discussion_r813558777 Co-Authored-By: Jon Ursenbach * Revert "chore: PR feedback" This reverts commit 1d2fad0a13720b12ee9c3e240d0c8c55afda028a. Co-authored-by: Jon Ursenbach Co-authored-by: Jon Ursenbach --- bin/rdme | 12 ++++++++ package-lock.json | 55 +++++++++++++++++++++++++++++++++---- package.json | 2 ++ src/cmds/docs/edit.js | 13 +++++++++ src/cmds/docs/index.js | 16 +++++++++++ src/cmds/login.js | 4 +++ src/cmds/logout.js | 3 ++ src/cmds/oas.js | 5 ++++ src/cmds/open.js | 6 ++++ src/cmds/openapi.js | 27 ++++++++++++++++++ src/cmds/swagger.js | 4 +++ src/cmds/validate.js | 7 +++++ src/cmds/versions/create.js | 4 +++ src/cmds/versions/delete.js | 6 ++++ src/cmds/versions/index.js | 4 +++ src/cmds/versions/update.js | 6 ++++ src/cmds/whoami.js | 3 ++ src/index.js | 3 ++ src/lib/fetch.js | 13 ++++----- src/lib/isGitHub.js | 7 +++++ src/lib/logger.js | 14 ++++++++++ 21 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 src/lib/isGitHub.js create mode 100644 src/lib/logger.js diff --git a/bin/rdme b/bin/rdme index e69a47b3b..5828dc862 100755 --- a/bin/rdme +++ b/bin/rdme @@ -1,9 +1,12 @@ #! /usr/bin/env node const chalk = require('chalk'); +const core = require('@actions/core'); const updateNotifier = require('update-notifier'); const pkg = require('../package.json'); +const isGHA = require('../src/lib/isGitHub'); + updateNotifier({ pkg }).notify(); require('../src')(process.argv.slice(2)) @@ -21,6 +24,15 @@ require('../src')(process.argv.slice(2)) message = err.message; } + /** + * If we're in a GitHub Actions environment, log errors with that formatting instead. + * @link: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message + * @link: https://github.com/actions/toolkit/tree/main/packages/core#annotations + */ + if (isGHA) { + return core.setFailed(message); + } + // eslint-disable-next-line no-console console.error(chalk.red(`\n${message}\n`)); return process.exit(1); diff --git a/package-lock.json b/package-lock.json index 64fd4f23f..0ebf20ea5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,14 @@ "version": "6.4.0", "license": "MIT", "dependencies": { + "@actions/core": "^1.6.0", "chalk": "^4.1.2", "cli-table": "^0.3.1", "command-line-args": "^5.2.0", "command-line-usage": "^6.0.2", "config": "^3.1.0", "configstore": "^5.0.0", + "debug": "^4.3.3", "editor": "^1.0.0", "enquirer": "^2.3.0", "form-data": "^4.0.0", @@ -45,6 +47,22 @@ "node": "^12 || ^14 || ^16" } }, + "node_modules/@actions/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", + "integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==", + "dependencies": { + "@actions/http-client": "^1.0.11" + } + }, + "node_modules/@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "dependencies": { + "tunnel": "0.0.6" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", @@ -3244,7 +3262,6 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -7513,8 +7530,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multilang-extract-comments": { "version": "0.4.0", @@ -9512,6 +9528,14 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", @@ -10001,6 +10025,22 @@ } }, "dependencies": { + "@actions/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", + "integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==", + "requires": { + "@actions/http-client": "^1.0.11" + } + }, + "@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "requires": { + "tunnel": "0.0.6" + } + }, "@apidevtools/json-schema-ref-parser": { "version": "9.0.9", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", @@ -12431,7 +12471,6 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -15716,8 +15755,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "multilang-extract-comments": { "version": "0.4.0", @@ -17287,6 +17325,11 @@ "tslib": "^1.8.1" } }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + }, "type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", diff --git a/package.json b/package.json index c091a9f72..7a03f5ce2 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,14 @@ "url": "https://github.com/readmeio/rdme/issues" }, "dependencies": { + "@actions/core": "^1.6.0", "chalk": "^4.1.2", "cli-table": "^0.3.1", "command-line-args": "^5.2.0", "command-line-usage": "^6.0.2", "config": "^3.1.0", "configstore": "^5.0.0", + "debug": "^4.3.3", "editor": "^1.0.0", "enquirer": "^2.3.0", "form-data": "^4.0.0", diff --git a/src/cmds/docs/edit.js b/src/cmds/docs/edit.js index a42ab78e9..b8ab2550c 100644 --- a/src/cmds/docs/edit.js +++ b/src/cmds/docs/edit.js @@ -6,6 +6,7 @@ const APIError = require('../../lib/apiError'); const { getProjectVersion } = require('../../lib/versionSelect'); const fetch = require('../../lib/fetch'); const { cleanHeaders, handleRes } = require('../../lib/fetch'); +const { debug } = require('../../lib/logger'); const writeFile = promisify(fs.writeFile); const readFile = promisify(fs.readFile); @@ -42,6 +43,9 @@ module.exports = class EditDocsCommand { async run(opts) { const { slug, key, version } = opts; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } @@ -54,6 +58,8 @@ module.exports = class EditDocsCommand { return Promise.reject(e); }); + debug(`selectedVersion: ${selectedVersion}`); + const filename = `${slug}.md`; const existingDoc = await fetch(`${config.get('host')}/api/v1/docs/${slug}`, { @@ -66,11 +72,16 @@ module.exports = class EditDocsCommand { await writeFile(filename, existingDoc.body); + debug(`wrote to local file: ${filename}, opening editor`); + return new Promise((resolve, reject) => { (opts.mockEditor || editor)(filename, async code => { + debug(`editor closed with code ${code}`); if (code !== 0) return reject(new Error('Non zero exit code from $EDITOR')); const updatedDoc = await readFile(filename, 'utf8'); + debug(`read edited contents of ${filename}, sending to ReadMe`); + return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { method: 'put', headers: cleanHeaders(key, { @@ -85,6 +96,7 @@ module.exports = class EditDocsCommand { }) .then(res => res.json()) .then(async res => { + debug(`response from PUT request: ${res}`); // The reason we aren't using our handleRes() function here is // because we need to use the `reject` function from // the Promise that's wrapping this function. @@ -93,6 +105,7 @@ module.exports = class EditDocsCommand { } console.info(`Doc successfully updated. Cleaning up local file.`); await unlink(filename); + debug('file unlinked'); // Normally we should resolve with a value that is logged to the console, // but since we need to wait for the temporary file to be removed, // it's okay to resolve the promise with no value. diff --git a/src/cmds/docs/index.js b/src/cmds/docs/index.js index 729811a8a..8c576addf 100644 --- a/src/cmds/docs/index.js +++ b/src/cmds/docs/index.js @@ -8,6 +8,7 @@ const { promisify } = require('util'); const { getProjectVersion } = require('../../lib/versionSelect'); const fetch = require('../../lib/fetch'); const { cleanHeaders, handleRes } = require('../../lib/fetch'); +const { debug } = require('../../lib/logger'); const readFile = promisify(fs.readFile); @@ -42,6 +43,9 @@ module.exports = class DocsCommand { async run(opts) { const { folder, key, version } = opts; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } @@ -55,6 +59,8 @@ module.exports = class DocsCommand { // Ideally we should ignore this parameter entirely if the category is included. const selectedVersion = await getProjectVersion(version, key, false); + debug(`selectedVersion: ${selectedVersion}`); + // Find the files to sync const readdirRecursive = folderToSearch => { const filesInFolder = fs.readdirSync(folderToSearch, { withFileTypes: true }); @@ -70,6 +76,9 @@ module.exports = class DocsCommand { // Strip out non-markdown files const files = readdirRecursive(folder).filter(file => file.endsWith('.md') || file.endsWith('.markdown')); + + debug(`number of files: ${files.length}`); + if (!files.length) { return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`)); } @@ -115,13 +124,17 @@ module.exports = class DocsCommand { const updatedDocs = await Promise.all( files.map(async filename => { + debug(`reading file ${filename}`); const file = await readFile(filename, 'utf8'); const matter = frontMatter(file); + debug(`frontmatter for ${filename}: ${JSON.stringify(matter)}`); // Stripping the subdirectories and markdown extension from the filename and lowercasing to get the default slug. const slug = matter.data.slug || path.basename(filename).replace(path.extname(filename), '').toLowerCase(); const hash = crypto.createHash('sha1').update(file).digest('hex'); + debug(`fetching data for ${slug}`); + return fetch(`${config.get('host')}/api/v1/docs/${slug}`, { method: 'get', headers: cleanHeaders(key, { @@ -131,9 +144,12 @@ module.exports = class DocsCommand { }) .then(res => res.json()) .then(res => { + debug(`GET /docs/:slug API response for ${slug}: ${JSON.stringify(res)}`); if (res.error) { + debug(`error retrieving data for ${slug}, creating doc`); return createDoc(slug, matter, hash, res); } + debug(`data received for ${slug}, updating doc`); return updateDoc(slug, matter, hash, res); }) .catch(err => { diff --git a/src/cmds/login.js b/src/cmds/login.js index 470ccd19a..740181352 100644 --- a/src/cmds/login.js +++ b/src/cmds/login.js @@ -6,6 +6,7 @@ const read = promisify(require('read')); const configStore = require('../lib/configstore'); const fetch = require('../lib/fetch'); const { handleRes } = require('../lib/fetch'); +const { debug } = require('../lib/logger'); const testing = process.env.NODE_ENV === 'testing'; @@ -34,6 +35,9 @@ module.exports = class LoginCommand { async run(opts) { let { email, password, project, token } = opts; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + /* istanbul ignore next */ async function getCredentials() { return { diff --git a/src/cmds/logout.js b/src/cmds/logout.js index e13b02b9d..530ec3591 100644 --- a/src/cmds/logout.js +++ b/src/cmds/logout.js @@ -1,5 +1,6 @@ const config = require('config'); const configStore = require('../lib/configstore'); +const { debug } = require('../lib/logger'); module.exports = class LogoutCommand { constructor() { @@ -13,6 +14,8 @@ module.exports = class LogoutCommand { } async run() { + debug(`command: ${this.command}`); + if (configStore.has('email') && configStore.has('project')) { configStore.clear(); } diff --git a/src/cmds/oas.js b/src/cmds/oas.js index c1f1eaa7a..f9546718c 100644 --- a/src/cmds/oas.js +++ b/src/cmds/oas.js @@ -1,5 +1,6 @@ const { spawn } = require('child_process'); const path = require('path'); +const { debug } = require('../lib/logger'); module.exports = class OASCommand { constructor() { @@ -13,12 +14,16 @@ module.exports = class OASCommand { } async run() { + debug(`command: ${this.command}`); + debug('spawning new process with `oas`'); + const cp = spawn(path.join(__dirname, '..', '..', 'node_modules', '.bin', 'oas'), process.argv.slice(3), { stdio: 'inherit', }); return new Promise((resolve, reject) => { cp.on('close', code => { + debug(`closing \`oas\` process with code: ${code}`); if (code && code > 0) return reject(); return resolve(); diff --git a/src/cmds/open.js b/src/cmds/open.js index c14702554..6991b3e59 100644 --- a/src/cmds/open.js +++ b/src/cmds/open.js @@ -2,6 +2,7 @@ const chalk = require('chalk'); const config = require('config'); const open = require('open'); const configStore = require('../lib/configstore'); +const { debug } = require('../lib/logger'); module.exports = class OpenCommand { constructor() { @@ -15,7 +16,12 @@ module.exports = class OpenCommand { } async run(opts) { + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + const project = configStore.get('project'); + debug(`project: ${project}`); + if (!project) { return Promise.reject(new Error(`Please login using \`${config.get('cli')} login\`.`)); } diff --git a/src/cmds/openapi.js b/src/cmds/openapi.js index d9862d4db..91352f764 100644 --- a/src/cmds/openapi.js +++ b/src/cmds/openapi.js @@ -11,6 +11,7 @@ const { cleanHeaders } = require('../lib/fetch'); const FormData = require('form-data'); const parse = require('parse-link-header'); const { file: tmpFile } = require('tmp-promise'); +const { debug } = require('../lib/logger'); module.exports = class OpenAPICommand { constructor() { @@ -56,6 +57,9 @@ module.exports = class OpenAPICommand { let selectedVersion; let isUpdate; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + if (!key && opts.token) { console.warn( chalk.yellow('⚠️ Warning! The `--token` option has been deprecated. Please use `--key` and `--id` instead.') @@ -74,11 +78,15 @@ module.exports = class OpenAPICommand { ); } + debug(`key (final): ${key}`); + debug(`id (final): ${id}`); + if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } async function callApi(specPath, versionCleaned) { + debug(`bundling and validating spec located at ${specPath}`); // @todo Tailor messaging to what is actually being handled here. If the user is uploading a Swagger file, never mention that they uploaded/updated an OpenAPI file. async function success(data) { @@ -86,7 +94,9 @@ module.exports = class OpenAPICommand { ? "You've successfully uploaded a new OpenAPI file to your ReadMe project!" : "You've successfully updated an OpenAPI file on your ReadMe project!"; + debug(`successful ${data.status} response`); const body = await data.json(); + debug(`successful response payload: ${JSON.stringify(body)}`); return Promise.resolve( [ @@ -103,10 +113,13 @@ module.exports = class OpenAPICommand { } async function error(err) { + debug(`error response received with status code ${err.status}`); try { const parsedError = await err.json(); + debug(`full error response: ${JSON.stringify(parsedError)}`); return Promise.reject(new APIError(parsedError)); } catch (e) { + debug(`error parsing JSON with message: ${e.message}`); if (e.message.includes('Unexpected token < in JSON')) { return Promise.reject( new Error( @@ -121,17 +134,22 @@ module.exports = class OpenAPICommand { let bundledSpec; const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true }); + debug('spec normalized'); await oas.validate(false); + debug('spec validated'); await oas.bundle().then(res => { bundledSpec = JSON.stringify(res); }); + debug('spec bundled'); // Create a temporary file to write the bundled spec to, // which we will then stream into the form data body const { path } = await tmpFile({ prefix: 'rdme-openapi-', postfix: '.json' }); + debug(`creating temporary file at ${path}`); await fs.writeFileSync(path, bundledSpec); const stream = fs.createReadStream(path); + debug('file and stream created, streaming into form data payload'); const formData = new FormData(); formData.append('spec', stream); @@ -178,15 +196,20 @@ module.exports = class OpenAPICommand { } if (!id) { + debug('no id parameter, retrieving list of API specs'); const apiSettings = await getSpecs(`/api/v1/api-specification`); const totalPages = Math.ceil(apiSettings.headers.get('x-total-count') / 10); const parsedDocs = parse(apiSettings.headers.get('link')); + debug(`total pages: ${totalPages}`); + debug(`pagination result: ${JSON.stringify(parsedDocs)}`); const apiSettingsBody = await apiSettings.json(); + debug(`api settings list response payload: ${JSON.stringify(apiSettingsBody)}`); if (!apiSettingsBody.length) return createSpec(); const { option } = await prompt(promptOpts.createOasPrompt(apiSettingsBody, parsedDocs, totalPages, getSpecs)); + debug(`selection result: ${option}`); if (!option) return null; return option === 'create' ? createSpec() : updateSpec(option); } @@ -202,6 +225,8 @@ module.exports = class OpenAPICommand { selectedVersion = await getProjectVersion(version, key, true); } + debug(`selectedVersion: ${selectedVersion}`); + if (spec) { return callApi(spec, selectedVersion); } @@ -210,7 +235,9 @@ module.exports = class OpenAPICommand { // that. If they don't have any, let's let the user know how they can get one going. return new Promise((resolve, reject) => { ['swagger.json', 'swagger.yaml', 'openapi.json', 'openapi.yaml'].forEach(file => { + debug(`looking for definition with filename: ${file}`); if (!fs.existsSync(file)) { + debug(`${file} not found`); return; } diff --git a/src/cmds/swagger.js b/src/cmds/swagger.js index 2bfc4dd28..ade7ccea3 100644 --- a/src/cmds/swagger.js +++ b/src/cmds/swagger.js @@ -1,5 +1,6 @@ const chalk = require('chalk'); const OpenAPICommand = require('./openapi'); +const { debug } = require('../lib/logger'); module.exports = class SwaggerCommand extends OpenAPICommand { constructor() { @@ -12,6 +13,9 @@ module.exports = class SwaggerCommand extends OpenAPICommand { } async run(opts) { + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + console.warn(chalk.yellow('⚠️ Warning! `rdme swagger` has been deprecated. Please use `rdme openapi` instead.')); return super.run(opts); } diff --git a/src/cmds/validate.js b/src/cmds/validate.js index 18a98ac0d..74e900744 100644 --- a/src/cmds/validate.js +++ b/src/cmds/validate.js @@ -1,6 +1,7 @@ const chalk = require('chalk'); const fs = require('fs'); const OASNormalize = require('oas-normalize'); +const { debug } = require('../lib/logger'); module.exports = class ValidateCommand { constructor() { @@ -23,6 +24,9 @@ module.exports = class ValidateCommand { async run(opts) { const { spec } = opts; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + async function validateSpec(specPath) { const oas = new OASNormalize(specPath, { colorizeErrors: true, enablePaths: true }); @@ -35,6 +39,7 @@ module.exports = class ValidateCommand { return Promise.resolve(chalk.green(`${specPath} is a valid OpenAPI API definition!`)); }) .catch(err => { + debug(`raw validation error object: ${JSON.stringify(err)}`); return Promise.reject(new Error(err.message)); }); } @@ -47,7 +52,9 @@ module.exports = class ValidateCommand { // don't have any, let's let the user know how they can get one going. return new Promise((resolve, reject) => { ['swagger.json', 'swagger.yaml', 'openapi.json', 'openapi.yaml'].forEach(file => { + debug(`looking for definition with filename: ${file}`); if (!fs.existsSync(file)) { + debug(`${file} not found`); return; } diff --git a/src/cmds/versions/create.js b/src/cmds/versions/create.js index e9b7b6c6d..d0f6f8b17 100644 --- a/src/cmds/versions/create.js +++ b/src/cmds/versions/create.js @@ -4,6 +4,7 @@ const { prompt } = require('enquirer'); const promptOpts = require('../../lib/prompts'); const fetch = require('../../lib/fetch'); const { cleanHeaders, handleRes } = require('../../lib/fetch'); +const { debug } = require('../../lib/logger'); module.exports = class CreateVersionCommand { constructor() { @@ -57,6 +58,9 @@ module.exports = class CreateVersionCommand { let versionList; const { key, version, codename, fork, main, beta, isPublic } = opts; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } diff --git a/src/cmds/versions/delete.js b/src/cmds/versions/delete.js index b899247d4..a73453919 100644 --- a/src/cmds/versions/delete.js +++ b/src/cmds/versions/delete.js @@ -2,6 +2,7 @@ const config = require('config'); const { getProjectVersion } = require('../../lib/versionSelect'); const fetch = require('../../lib/fetch'); const { cleanHeaders, handleRes } = require('../../lib/fetch'); +const { debug } = require('../../lib/logger'); module.exports = class DeleteVersionCommand { constructor() { @@ -29,6 +30,9 @@ module.exports = class DeleteVersionCommand { async run(opts) { const { key, version } = opts; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } @@ -37,6 +41,8 @@ module.exports = class DeleteVersionCommand { return Promise.reject(e); }); + debug(`selectedVersion: ${selectedVersion}`); + return fetch(`${config.get('host')}/api/v1/version/${selectedVersion}`, { method: 'delete', headers: cleanHeaders(key), diff --git a/src/cmds/versions/index.js b/src/cmds/versions/index.js index dc626c682..0cf5b8c29 100644 --- a/src/cmds/versions/index.js +++ b/src/cmds/versions/index.js @@ -4,6 +4,7 @@ const config = require('config'); const CreateVersionCmd = require('./create'); const fetch = require('../../lib/fetch'); const { cleanHeaders, handleRes } = require('../../lib/fetch'); +const { debug } = require('../../lib/logger'); module.exports = class VersionsCommand { constructor() { @@ -87,6 +88,9 @@ module.exports = class VersionsCommand { async run(opts) { const { key, version, raw } = opts; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } diff --git a/src/cmds/versions/update.js b/src/cmds/versions/update.js index c1fc92a0b..e609c8d08 100644 --- a/src/cmds/versions/update.js +++ b/src/cmds/versions/update.js @@ -4,6 +4,7 @@ const promptOpts = require('../../lib/prompts'); const { getProjectVersion } = require('../../lib/versionSelect'); const fetch = require('../../lib/fetch'); const { cleanHeaders, handleRes } = require('../../lib/fetch'); +const { debug } = require('../../lib/logger'); module.exports = class UpdateVersionCommand { constructor() { @@ -50,6 +51,9 @@ module.exports = class UpdateVersionCommand { async run(opts) { const { key, version, codename, newVersion, main, beta, isPublic, deprecated } = opts; + debug(`command: ${this.command}`); + debug(`opts: ${JSON.stringify(opts)}`); + if (!key) { return Promise.reject(new Error('No project API key provided. Please use `--key`.')); } @@ -58,6 +62,8 @@ module.exports = class UpdateVersionCommand { return Promise.reject(e); }); + debug(`selectedVersion: ${selectedVersion}`); + const foundVersion = await fetch(`${config.get('host')}/api/v1/version/${selectedVersion}`, { method: 'get', headers: cleanHeaders(key), diff --git a/src/cmds/whoami.js b/src/cmds/whoami.js index bae588549..e1fd35e6c 100644 --- a/src/cmds/whoami.js +++ b/src/cmds/whoami.js @@ -1,6 +1,7 @@ const chalk = require('chalk'); const config = require('config'); const configStore = require('../lib/configstore'); +const { debug } = require('../lib/logger'); module.exports = class WhoAmICommand { constructor() { @@ -14,6 +15,8 @@ module.exports = class WhoAmICommand { } async run() { + debug(`command: ${this.command}`); + if (!configStore.has('email') || !configStore.has('project')) { return Promise.reject(new Error(`Please login using \`${config.get('cli')} login\`.`)); } diff --git a/src/index.js b/src/index.js index d75331ec7..2abd6b61b 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,7 @@ const { version } = require('../package.json'); const configStore = require('./lib/configstore'); const help = require('./lib/help'); const commands = require('./lib/commands'); +const { debug } = require('./lib/logger'); /** * @param {Array} processArgv - An array of arguments from the current process. Can be used to mock @@ -41,6 +42,8 @@ module.exports = processArgv => { const argv = cliArgs(mainArgs, { partial: true, argv: processArgv }); const cmd = argv.command || false; + debug(`command-line-args processing: ${JSON.stringify(argv)}`); + // Add support for `-V` as an additional `--version` alias. if (typeof argv._unknown !== 'undefined') { if (argv._unknown.indexOf('-V') !== -1) { diff --git a/src/lib/fetch.js b/src/lib/fetch.js index 69f747bf2..88afe2906 100644 --- a/src/lib/fetch.js +++ b/src/lib/fetch.js @@ -1,16 +1,10 @@ /* eslint-disable no-param-reassign */ +const { debug } = require('./logger'); const fetch = require('node-fetch'); +const isGHA = require('./isGitHub'); const pkg = require('../../package.json'); const APIError = require('./apiError'); -/** - * Small env check to determine if we're in a GitHub Actions environment - * @link https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables - */ -function isGHA() { - return process.env.GITHUB_ACTIONS === 'true'; -} - /** * Wrapper for the `fetch` API so we can add rdme-specific headers to all API requests. * @@ -31,6 +25,8 @@ module.exports = (url, options = { headers: {} }) => { options.headers['x-readme-source'] = source; + debug(`making ${(options.method || 'get').toUpperCase()} request to ${url}`); + return fetch(url, options); }; @@ -52,6 +48,7 @@ module.exports.getUserAgent = function getUserAgent() { */ module.exports.handleRes = async function handleRes(res) { const body = await res.json(); + debug(`received status code ${res.status} with response body: ${JSON.stringify(body)}`); if (body.error) { return Promise.reject(new APIError(body)); } diff --git a/src/lib/isGitHub.js b/src/lib/isGitHub.js new file mode 100644 index 000000000..a8c9fb102 --- /dev/null +++ b/src/lib/isGitHub.js @@ -0,0 +1,7 @@ +/** + * Small env check to determine if we're in a GitHub Actions environment + * @link https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables + */ +module.exports = function isGHA() { + return process.env.GITHUB_ACTIONS === 'true'; +}; diff --git a/src/lib/logger.js b/src/lib/logger.js new file mode 100644 index 000000000..de7edd5d5 --- /dev/null +++ b/src/lib/logger.js @@ -0,0 +1,14 @@ +const config = require('config'); +const core = require('@actions/core'); +const debugPackage = require('debug')(config.get('cli')); +const isGHA = require('./isGitHub'); + +/** + * Wrapper for debug statements. + * @param {String} input + */ +module.exports.debug = function debug(input) { + /* istanbul ignore next */ + if (isGHA() && process.env.NODE_ENV !== 'testing') core.debug(`rdme: ${input}`); + return debugPackage(input); +};