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

deploy: first take to handle bump deploy command 🎉 #10

Merged
merged 2 commits into from
May 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
paulRbr marked this conversation as resolved.
Show resolved Hide resolved
```

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,
];
paulRbr marked this conversation as resolved.
Show resolved Hide resolved
}
}
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
paulRbr marked this conversation as resolved.
Show resolved Hide resolved

$> 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