Skip to content

Commit

Permalink
Merge pull request #10 from paulRbr/deploy-basic-command
Browse files Browse the repository at this point in the history
deploy: first take to handle `bump deploy` command 🎉
  • Loading branch information
paulRbr authored May 7, 2021
2 parents d4a94ba + 9dcbe95 commit fe7ecdf
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 18 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,22 @@ USAGE
$ bump [COMMAND]

COMMANDS
deploy Create a new version of your documentation for the given file or URL
help display help for bump
preview Create a documentation preview for the given file
preview Create a documentation preview for the given file or URL
```

## Commands

* [`bump preview [FILE]`](#bump-preview-file)
* [`bump deploy [FILE]`](#bump-deploy-file)

### `bump preview [FILE]`

You can preview your documentation by calling the `preview` command. A temporary preview will be created with a unique URL. This preview will be available for 30 minutes. You don't need any credentials to use this command.

```
Create a documentation preview for the given file
Create a documentation preview for the given file or URL
USAGE
$ bump preview FILE
Expand All @@ -82,6 +84,22 @@ EXAMPLE
* Your preview is visible at: https://bump.sh/preview/45807371-9a32-48a7-b6e4-1cb7088b5b9b
```

### `bump deploy [FILE]`

Deploy the definition file as the current version of the documentation with the following command:

```sh-session
$ bump deploy path/to/your/file.yml --doc DOC_ID_OR_SLUG --token DOC_TOKEN
```

If you already have a hub in your [Bump.sh](https://bump.sh) account, you can automatically create a documentation inside it and deploy to it with:

```sh-session
$ bump deploy path/to/your/file.yml --auto-create --doc DOC_SLUG --hub HUB_ID_OR_SLUG --token HUB_TOKEN
```

Please check `bump deploy --help` for more usage details

## Development

Make sure to have Node.js (At least v10) installed on your machine.
Expand Down
32 changes: 32 additions & 0 deletions src/api/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export default class APIError extends CLIError {
case 422:
[info, exit] = APIError.invalidDefinition(httpError.response.data);
break;
case 401:
[info, exit] = APIError.unauthenticated();
break;
case 404:
case 400:
[info, exit] = APIError.notFound(httpError.response.data);
break;
}

if (info.length) {
Expand All @@ -37,6 +44,21 @@ export default class APIError extends CLIError {
return error instanceof CLIError && 'http' in error;
}

static notFound(error: Error): MessagesAndExitCode {
const genericMessage =
error.message || "It seems the documentation provided doesn't exist.";

return [
[
genericMessage,
`Please check the given ${chalk.underline('--documentation')}, ${chalk.underline(
'--token',
)} or ${chalk.underline('--hub')} flags`,
],
104,
];
}

static invalidDefinition(error: InvalidDefinitionError): MessagesAndExitCode {
let info: string[] = [];
const genericMessage = error.message || 'Invalid definition file';
Expand Down Expand Up @@ -79,4 +101,14 @@ export default class APIError extends CLIError {

return info;
}

static unauthenticated(): MessagesAndExitCode {
return [
[
'You are not allowed to deploy to this documentation.',
'please check your --token flag or BUMP_TOKEN variable',
],
101,
];
}
}
17 changes: 15 additions & 2 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Config from '@oclif/config';
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';

import { PingResponse, PreviewRequest, PreviewResponse } from './models';
import { PingResponse, PreviewRequest, PreviewResponse, VersionRequest } from './models';
import { vars } from './vars';
import APIError from './error';

Expand All @@ -11,7 +11,7 @@ class BumpApi {
// Check https://oclif.io/docs/config for details about Config.IConfig
public constructor(protected config: Config.IConfig) {
const baseURL = `${vars.apiUrl}${vars.apiBasePath}`;
const headers = {
const headers: { 'User-Agent': string; Authorization?: string } = {
'User-Agent': config.userAgent,
};

Expand All @@ -33,11 +33,24 @@ class BumpApi {
return this.client.post<PreviewResponse>('/previews', body);
};

public postVersion = (
body: VersionRequest,
token: string,
): Promise<AxiosResponse<void>> => {
return this.client.post<void>('/versions', body, {
headers: this.authorizationHeader(token),
});
};

private initializeResponseInterceptor = () => {
this.client.interceptors.response.use((data) => data, this.handleError);
};

private handleError = (error: AxiosError) => Promise.reject(new APIError(error));

private authorizationHeader = (token: string) => {
return { Authorization: `Basic ${Buffer.from(token).toString('base64')}` };
};
}

export * from './models';
Expand Down
9 changes: 9 additions & 0 deletions src/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,12 @@ export interface Reference {
location?: string;
content?: string;
}

export interface VersionRequest {
documentation: string;
definition: string;
hub?: string;
documentation_name?: string;
auto_create_documentation?: boolean;
references?: Reference[];
}
70 changes: 70 additions & 0 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Command from '../command';
import * as flags from '../flags';
import { fileArg } from '../args';
import { cli } from '../cli';
import { VersionRequest } from '../api/models';

export default class Deploy extends Command {
static description =
'Create a new version of your documentation from the given file or URL';

static examples = [
`Deploy a new version of an existing documentation
$> bump deploy FILE --doc <your_doc_id_or_slug> --token <your_doc_token>
* Let's deploy a new documentation version on Bump... done
* Your new documentation version will soon be ready
`,
`Deploy a new version of an existing documentation attached to a hub
$> bump deploy FILE --doc <doc_slug> --hub <your_hub_id_or_slug> --token <your_doc_token>
* Let's deploy a new documentation version on Bump... done
* Your new documentation version will soon be ready
`,
];

static flags = {
help: flags.help({ char: 'h' }),
doc: flags.doc(),
'doc-name': flags.docName(),
hub: flags.hub(),
token: flags.token(),
'auto-create': flags.autoCreate(),
};

static args = [fileArg];

/*
Oclif doesn't type parsed args & flags correctly and especially
required-ness which is not known by the compiler, thus the use of
the non-null assertion '!' in this command.
See https://github.com/oclif/oclif/issues/301 for details
*/
async run(): Promise<void> {
const { args, flags } = this.parse(Deploy);
const [api, references] = await this.prepareDefinition(args.FILE);

cli.action.start("* Let's deploy a new documentation version on Bump");

const request: VersionRequest = {
documentation: flags.doc!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
hub: flags.hub,
documentation_name: flags['doc-name'],
auto_create_documentation: flags['auto-create'],
definition: JSON.stringify(api.definition),
references,
};
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const response = await this.bump.postVersion(request, flags.token!);

cli.action.stop();

if (response.status === 201) {
cli.styledSuccess('Your new documentation version will soon be ready');
} else if (response.status === 204) {
this.warn('Your documentation has not changed!');
}

return;
}
}
2 changes: 1 addition & 1 deletion src/commands/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cli } from '../cli';
import { PreviewResponse, PreviewRequest } from '../api/models';

export default class Preview extends Command {
static description = 'Create a documentation preview for the given file';
static description = 'Create a documentation preview for the given file or URL';

static examples = [
`$ bump preview FILE
Expand Down
2 changes: 1 addition & 1 deletion src/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ class API {
throw new UnsupportedFormat('Reference ${absPath} is empty');
}

/* eslint-disable @typescript-eslint/no-explicit-any */
/* The internals of the $RefParser doesn't have types exposed */
/* thus the need to cast 'as any' to be able to dig into the obj */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const refType = ($refs as any)._$refs[absPath].pathType;
/* Resolve all reference paths to the main api definition file */
const location: string =
Expand Down
18 changes: 13 additions & 5 deletions src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export * from '@oclif/command/lib/flags';
// Custom flags for bum-cli
const doc = flags.build({
char: 'd',
description: 'Documentation public id or slug, default: ""',
required: true,
description:
'Documentation public id or slug. Can be provided via BUMP_ID environment variable',
default: () => {
const envDoc = process.env.BUMP_ID;
if (envDoc) return envDoc;
Expand All @@ -23,7 +25,7 @@ const docName = flags.build({

const hub = flags.build({
char: 'b',
description: 'Hub id or slug',
description: 'Hub id or slug. Can be provided via BUMP_HUB_ID environment variable',
default: () => {
const envHub = process.env.BUMP_HUB_ID;
if (envHub) return envHub;
Expand All @@ -33,14 +35,20 @@ const hub = flags.build({

const token = flags.build({
char: 't',
description: 'Documentation or Hub token, default: ""',
required: true,
description:
'Documentation or Hub token. Can be provided via BUMP_TOKEN environment variable',
default: () => {
const envToken = process.env.BUMP_TOKEN;
if (envToken) return envToken;
},
});

const autoCreate = (options = {}): Parser.flags.IBooleanFlag<boolean> => {
return flags.boolean({
description:
'Automatically create the documentation if needed (only available with a --hub and when specifying a name for documentation --doc-name), default: false',
dependsOn: ['hub', 'doc-name'],
'Automatically create the documentation if needed (only available with a --hub flag). Documentation name can be provided with --doc-name flag. Default: false',
dependsOn: ['hub'],
...options,
});
};
Expand Down
108 changes: 108 additions & 0 deletions test/commands/deploy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import base, { expect } from '@oclif/test';
import nock from 'nock';

nock.disableNetConnect();

const test = base.env({ BUMP_TOKEN: 'BAR' });

describe('deploy subcommand', () => {
describe('Successful deploy', () => {
test
.nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(201))
.stdout()
.stderr()
.command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'])
.it('sends version to Bump', ({ stdout, stderr }) => {
expect(stderr).to.match(/Let's deploy a new documentation version/);
expect(stdout).to.contain('Your new documentation version will soon be ready');
});

test
.nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(204))
.stderr()
.command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'])
.it('sends version to Bump', ({ stderr }) => {
expect(stderr).to.contain("Let's deploy a new documentation version");
expect(stderr).to.contain('Your documentation has not changed!');
});

test
.env({ BUMP_ID: 'coucou' })
.nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(201))
.stdout()
.stderr()
.command(['deploy', 'examples/valid/openapi.v3.json'])
.it(
'sends version to Bump with doc read from env variable',
({ stdout, stderr }) => {
expect(stderr).to.match(/Let's deploy a new documentation version/);
expect(stdout).to.contain('Your new documentation version will soon be ready');
},
);
});

describe('Server errors', () => {
describe('Authentication error', () => {
test
.nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(401))
.stdout()
.stderr()
.command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'])
.catch((err) => {
expect(err.message).to.contain('not allowed to deploy');
throw err;
})
.exit(101)
.it("Doesn't create a deployed version", ({ stdout }) => {
expect(stdout).to.not.contain(
'Your new documentation version will soon be ready',
);
});
});

describe('Not found error', () => {
test
.nock('https://bump.sh', (api) => api.post('/api/v1/versions').reply(404))
.stdout()
.stderr()
.command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'])
.catch((err) => {
expect(err.message).to.contain(
"It seems the documentation provided doesn't exist",
);
throw err;
})
.exit(104)
.it("Doesn't create a deployed version", ({ stdout }) => {
expect(stdout).to.not.contain(
'Your new documentation version will soon be ready',
);
});
});
});

describe('User bad usages', () => {
test
.command(['deploy', 'FILE', '--doc', 'coucou'])
.catch((err) => expect(err.message).to.match(/no such file or directory/))
.it('Fails deploying an inexistant file');

test
.command(['deploy'])
.exit(2)
.it('exits with status 2 when no file argument is provided');

test
.command(['deploy', 'examples/valid/openapi.v3.json'])
.catch((err) => expect(err.message).to.match(/missing required flag(.|\n)+--doc/im))
.it('fails when no documentation id or slug is provided');

test
.env({ BUMP_TOKEN: '' }, { clear: true })
.command(['deploy', 'examples/valid/openapi.v3.json', '--doc', 'coucou'])
.catch((err) =>
expect(err.message).to.match(/missing required flag(.|\n)+--token/im),
)
.it('fails when no access token is provided');
});
});
Loading

0 comments on commit fe7ecdf

Please sign in to comment.