Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(docs): move --cleanup option into docs:prune command #644

Merged
merged 9 commits into from
Oct 27, 2022
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,13 @@ rdme docs path-to-markdown-files --version={project-version}

This command also has a dry run mode, which can be useful for initial setup and debugging. You can read more about dry run mode [in our docs](https://docs.readme.com/docs/rdme#dry-run-mode).

#### Cleanup
#### Prune

If you wish to delete documents from ReadMe that are no longer present in your local directory, pass the `--cleanup` option to the command.
If you wish to delete documents from ReadMe that are no longer present in your local directory:

```sh
rdme docs:prune path-to-markdown-files
```

#### Edit a Single ReadMe Doc on Your Local Machine

Expand Down
119 changes: 0 additions & 119 deletions __tests__/cmds/docs/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable no-console */
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';

Expand Down Expand Up @@ -390,124 +389,6 @@ describe('rdme docs', () => {
});
});

describe('cleanup docs', () => {
const folder = `./__tests__/${fixturesBaseDir}/delete-docs`;
const someDocContent = fs.readFileSync(path.join(folder, 'some-doc.md'));
const lastUpdatedHash = crypto.createHash('sha1').update(someDocContent).digest('hex');
let consoleWarnSpy;

function getWarningCommandOutput() {
return [consoleWarnSpy.mock.calls.join('\n\n')].filter(Boolean).join('\n\n');
}

beforeEach(() => {
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
});

afterEach(() => {
consoleWarnSpy.mockRestore();
});

it('should delete doc if file is missing and --cleanup option is used', async () => {
const versionMock = getAPIMock()
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, { version });

const apiMocks = getAPIMockWithVersionHeader(version)
.get('/api/v1/categories?perPage=20&page=1')
.basicAuth({ user: key })
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
.get('/api/v1/categories/category1/docs')
.basicAuth({ user: key })
.reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }, { slug: 'some-doc' }])
.delete('/api/v1/docs/this-doc-should-be-missing-in-folder')
.basicAuth({ user: key })
.reply(204, '')
.get('/api/v1/docs/some-doc')
.basicAuth({ user: key })
.reply(200, { lastUpdatedHash });

await expect(
docs.run({
folder,
key,
version,
cleanup: true,
})
).resolves.toBe(
'🗑️ successfully deleted `this-doc-should-be-missing-in-folder`.\n' +
'`some-doc` was not updated because there were no changes.'
);
const warningOutput = getWarningCommandOutput();
expect(warningOutput).toBe(
"⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs."
);

apiMocks.done();
versionMock.done();
});

it('should return doc delete info for dry run', async () => {
const versionMock = getAPIMock()
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, { version });
const apiMocks = getAPIMockWithVersionHeader(version)
.get('/api/v1/categories?perPage=20&page=1')
.basicAuth({ user: key })
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
.get('/api/v1/categories/category1/docs')
.basicAuth({ user: key })
.reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }])
.get('/api/v1/docs/some-doc')
.basicAuth({ user: key })
.reply(200, { lastUpdatedHash });
await expect(
docs.run({
folder,
key,
version,
cleanup: true,
dryRun: true,
})
).resolves.toBe(
'🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.\n' +
'🎭 dry run! `some-doc` will not be updated because there were no changes.'
);
const warningOutput = getWarningCommandOutput();
expect(warningOutput).toBe(
"⚠️ Warning! We're going to delete from ReadMe any document that isn't found in ./__tests__/__fixtures__/docs/delete-docs."
);

apiMocks.done();
versionMock.done();
});

it('should do nothing if using --cleanup but the folder is empty and the user aborted', async () => {
prompts.inject([false]);

const versionMock = getAPIMock()
.get(`/api/v1/version/${version}`)
.basicAuth({ user: key })
.reply(200, { version });

await expect(
docs.run({
folder: './__tests__/__fixtures__/ref-oas',
key,
version,
cleanup: true,
})
).rejects.toStrictEqual(new Error('Aborting, no changes were made.'));

const warningOutput = getWarningCommandOutput();
expect(warningOutput).toBe('');

versionMock.done();
});
});

describe('slug metadata', () => {
it('should use provided slug', async () => {
const slug = 'new-doc-slug';
Expand Down
120 changes: 120 additions & 0 deletions __tests__/cmds/docs/prune.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import nock from 'nock';
import prompts from 'prompts';

import DocsPruneCommand from '../../../src/cmds/docs/prune';
import getAPIMock, { getAPIMockWithVersionHeader } from '../../helpers/get-api-mock';

const docsPrune = new DocsPruneCommand();

const fixturesBaseDir = '__fixtures__/docs';

const key = 'API_KEY';
const version = '1.0.0';

describe('rdme docs:prune', () => {
const folder = `./__tests__/${fixturesBaseDir}/delete-docs`;

beforeAll(() => nock.disableNetConnect());

afterAll(() => nock.cleanAll());

it('should prompt for login if no API key provided', async () => {
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
prompts.inject(['this-is-not-an-email', 'password', 'subdomain']);
await expect(docsPrune.run({})).rejects.toStrictEqual(new Error('You must provide a valid email address.'));
consoleInfoSpy.mockRestore();
});

it('should error in CI if no API key provided', async () => {
process.env.TEST_CI = 'true';
await expect(docsPrune.run({})).rejects.toStrictEqual(
new Error('No project API key provided. Please use `--key`.')
);
delete process.env.TEST_CI;
});

it('should error if no folder provided', () => {
return expect(docsPrune.run({ key, version: '1.0.0' })).rejects.toStrictEqual(
new Error('No folder provided. Usage `rdme docs:prune <folder> [options]`.')
);
});

it('should error if the argument is not a folder', async () => {
const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

await expect(docsPrune.run({ key, version: '1.0.0', folder: 'not-a-folder' })).rejects.toThrow(
"ENOENT: no such file or directory, scandir 'not-a-folder'"
);

versionMock.done();
});

it('should do nothing if the user aborted', async () => {
prompts.inject([false]);

const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

await expect(
docsPrune.run({
folder,
key,
version,
})
).rejects.toStrictEqual(new Error('Aborting, no changes were made.'));

versionMock.done();
});

it('should delete doc if file is missing', async () => {
prompts.inject([true]);

const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });

const apiMocks = getAPIMockWithVersionHeader(version)
.get('/api/v1/categories?perPage=20&page=1')
.basicAuth({ user: key })
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
.get('/api/v1/categories/category1/docs')
.basicAuth({ user: key })
.reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }, { slug: 'some-doc' }])
.delete('/api/v1/docs/this-doc-should-be-missing-in-folder')
.basicAuth({ user: key })
.reply(204, '');

await expect(
docsPrune.run({
folder,
key,
version,
})
).resolves.toBe('🗑️ successfully deleted `this-doc-should-be-missing-in-folder`.');

apiMocks.done();
versionMock.done();
});

it('should return doc delete info for dry run', async () => {
prompts.inject([true]);

const versionMock = getAPIMock().get(`/api/v1/version/${version}`).basicAuth({ user: key }).reply(200, { version });
const apiMocks = getAPIMockWithVersionHeader(version)
.get('/api/v1/categories?perPage=20&page=1')
.basicAuth({ user: key })
.reply(200, [{ slug: 'category1', type: 'guide' }], { 'x-total-count': '1' })
.get('/api/v1/categories/category1/docs')
.basicAuth({ user: key })
.reply(200, [{ slug: 'this-doc-should-be-missing-in-folder' }]);

await expect(
docsPrune.run({
folder,
key,
version,
dryRun: true,
})
).resolves.toBe('🎭 dry run! This will delete `this-doc-should-be-missing-in-folder`.');

apiMocks.done();
versionMock.done();
});
});
8 changes: 7 additions & 1 deletion __tests__/lib/__snapshots__/commands.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,17 @@ exports[`utils #listByCategory should list commands by category 1`] = `
"name": "docs",
"position": 1,
},
{
"description": "Delete any docs from ReadMe if their slugs are not found in the target folder.",
"hidden": false,
"name": "docs:prune",
"position": 2,
},
{
"description": "Edit a single file from your ReadMe project without saving locally. [deprecated]",
"hidden": true,
"name": "docs:edit",
"position": 2,
"position": 3,
},
{
"description": "Sync a single Markdown file to your ReadMe project.",
Expand Down
4 changes: 2 additions & 2 deletions src/cmds/docs/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ export type Options = {
};

@isHidden
export default class EditDocsCommand extends Command {
export default class DocsEditCommand extends Command {
constructor() {
super();

this.command = 'docs:edit';
this.usage = 'docs:edit <slug> [options]';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly not opposed to deprecating this command now and removing it in v9.

Copy link
Member Author

@kanadgupta kanadgupta Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, will do in a separate PR!1

edit: see #646

Footnotes

  1. ticketed in RM-5649

this.description = 'Edit a single file from your ReadMe project without saving locally. [deprecated]';
this.cmdCategory = CommandCategories.DOCS;
this.position = 2;
this.position = 3;

this.hiddenArgs = ['slug'];
this.args = [
Expand Down
42 changes: 4 additions & 38 deletions src/cmds/docs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,15 @@ import config from 'config';

import Command, { CommandCategories } from '../../lib/baseCommand';
import createGHA from '../../lib/createGHA';
import deleteDoc from '../../lib/deleteDoc';
import getDocs from '../../lib/getDocs';
import * as promptHandler from '../../lib/prompts';
import promptTerminal from '../../lib/promptWrapper';
import pushDoc from '../../lib/pushDoc';
import readdirRecursive from '../../lib/readdirRecursive';
import readDoc from '../../lib/readDoc';
import { getProjectVersion } from '../../lib/versionSelect';

export type Options = {
dryRun?: boolean;
folder?: string;
cleanup?: boolean;
};

function getSlug(filename: string): string {
const { slug } = readDoc(filename);
return slug;
}

export default class DocsCommand extends Command {
constructor() {
super();
Expand All @@ -50,18 +39,13 @@ export default class DocsCommand extends Command {
type: Boolean,
description: 'Runs the command without creating/updating any docs in ReadMe. Useful for debugging.',
},
{
name: 'cleanup',
type: Boolean,
description: 'Delete any docs from ReadMe if their slugs are not found in the target folder.',
},
];
}

async run(opts: CommandOptions<Options>) {
await super.run(opts);

const { dryRun, folder, key, version, cleanup } = opts;
const { dryRun, folder, key, version } = opts;

if (!folder) {
return Promise.reject(new Error(`No folder provided. Usage \`${config.get('cli')} ${this.usage}\`.`));
Expand All @@ -81,25 +65,7 @@ export default class DocsCommand extends Command {

Command.debug(`number of files: ${files.length}`);

const changes: string[] = [];
if (cleanup) {
if (!files.length) {
const { deleteAll } = await promptTerminal(promptHandler.deleteDocsPrompt(selectedVersion));
if (!deleteAll) {
return Promise.reject(new Error('Aborting, no changes were made.'));
}
}

Command.warn(`We're going to delete from ReadMe any document that isn't found in ${folder}.`);
const docs = await getDocs(key, selectedVersion);
const docSlugs = docs.map(({ slug }: { slug: string }) => slug);
const fileSlugs = new Set(files.map(getSlug));
const slugsToDelete = docSlugs.filter((slug: string) => !fileSlugs.has(slug));
const deletedDocs = await Promise.all(
slugsToDelete.map((slug: string) => deleteDoc(key, selectedVersion, dryRun, slug, this.cmdCategory))
);
changes.push(...deletedDocs);
} else if (!files.length) {
if (!files.length) {
return Promise.reject(new Error(`We were unable to locate Markdown files in ${folder}.`));
}

Expand All @@ -108,8 +74,8 @@ export default class DocsCommand extends Command {
return pushDoc(key, selectedVersion, dryRun, filename, this.cmdCategory);
})
);
changes.push(...updatedDocs);
return Promise.resolve(chalk.green(changes.join('\n'))).then(msg =>

return Promise.resolve(chalk.green(updatedDocs.join('\n'))).then(msg =>
createGHA(msg, this.command, this.args, { ...opts, version: selectedVersion })
);
}
Expand Down
Loading