Merge pull request #434 from jdalrymple/146-cli-support

feat: Adding support for CLI
This commit is contained in:
Justin Dalrymple 2019-09-09 10:44:29 -04:00 committed by GitHub
commit b0ee51767d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
124 changed files with 2129 additions and 3192 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ coverage
.rpt2_cache
.idea
.DS_Store
temp

174
README.md
View File

@ -19,15 +19,16 @@
## Table of Contents
- [Install](#install)
- [Usage](#usage)
- [Getting Started](#getting-started)
- [CLI Support](#cli-support)
- [Docs](#docs)
- [Supported APIs](#supported-apis)
- [Import](#import)
- [Bundle Imports](#bundle-imports)
- [Bundle Imports](#bundle-imports)
- [Examples](#examples)
- [Pagination](#pagination)
- [Sudo](#sudo)
- [Custom Request Libraries](#custom-request-libraries)
- [Docs](#docs)
- [Misc](#misc)
- [Development](#development)
- [Testing](#testing)
- [Contributors](#contributors)
@ -41,7 +42,73 @@
npm install gitlab
```
## Usage
## Getting Started
Instantiate the library using a basic token created in your [Gitlab Profile](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html)
```javascript
// ES6 (>=node 10.16.0 LTS)
import { Gitlab } from 'gitlab'; // All Resources
import { Projects } from 'gitlab'; // Just the Project Resource
//...etc
// ES5, assuming native or polyfilled Promise is available
const { Gitlab } = require('gitlab');
```
```javascript
const api = new Gitlab({
token: 'personaltoken',
});
```
Available instantiating options:
| Name | Optional | Default | Description |
| -------------------- | -------- | ----------------------------------------------------- | --------------------------------------------------------------- |
| `host` | Yes | `https://gitlab.com` | Gitlab Instance Host URL |
| `token` | No\* | N/A | Personal Token. Required (one of the three tokens are required) |
| `oauthToken` | No\* | N/A | OAuth Token. Required (one of the three tokens are required) |
| `jobToken` | No\* | N/A | CI Job Token. Required (one of the three tokens are required) |
| `rejectUnauthorized` | Yes | `false` | Http Certificate setting |
| `sudo` | Yes | `false` | Sudo query parameter |
| `version` | Yes | `v4` | API Version ID |
| `camelize` | Yes | `false` | Response Key Camelize. Camelizes all response body keys |
| `requester` | Yes | [KyRequester.ts](./src/infrastructure/KyRequester.ts) | Request Library Wrapper. Currently wraps Ky. |
| `requestTimeout` | Yes | `300000` | Request Library Timeout in ms |
### CLI Support
The CLI export functions in a similar manner, following the pattern:
```bash
gitlab [service name] [method name] --arg1 --arg2 --arg3
```
Where `service name` is any of the supported API names, `method name` is any of the supported commands on that API service (See source for exceptions, but generally all, show, remove, update) and `--arg1...--arg3` are any of the arguments you would normally supply to the function. The names of the args should match the names in the method headers **EXCEPT** all the optional arguments who's names should match what the GitLab API docs request.
There is one small exception with the instantiating arguments however, which must be supplied using a `gl` prefix. ie.
```bash
# To get all the projects
gitlab projects all --gl-token="personaltoken"
# To get a project with id = 2
gitlab projects show --gl-token="personaltoken" --projectId=2
```
To reduce the annoyance of having to pass those configuration properties each time, it is also possible to pass the token and host information through environment variables in the form of `GITLAB_[option name]` ie:
```bash
GITLAB_HOST=http://example.com
GITLAB_TOKEN=personaltoken
```
This could be set globally or using a [.env](https://github.com/motdotla/dotenv#readme) file in the project folder.
## Docs
Although there are the [official docs](https://github.com/gitlabhq/gitlabhq/tree/master/doc/api) for the API, there are some extra goodies offered by this package! After the 3.0.0 release, the next large project will be putting together proper documentation for these goodies [#39]! Stay tuned!!
### Supported APIs
@ -143,46 +210,7 @@ UserGPGKeys
```
### Import
URL to your GitLab instance should not include `/api/v4` path.
Instantiate the library using a basic token created in your [Gitlab Profile](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html)
```javascript
// ES6 (>=node 10.16.0 LTS)
import { Gitlab } from 'gitlab'; // All Resources
import { Projects } from 'gitlab'; // Just the Project Resource
//...etc
// ES5, assuming native or polyfilled Promise is available
const { Gitlab } = require('gitlab');
```
Basic Example
```javascript
const api = new Gitlab({
token: 'personaltoken',
});
```
Available instatiating options:
| Name | Optional | Default | Description |
| -------------------- | -------- | ----------------------------------------------------- | --------------------------------------------------------------- |
| `host` | Yes | `https://gitlab.com` | Gitlab Instance Host URL |
| `token` | No\* | N/A | Personal Token. Required (one of the three tokens are required) |
| `oauthToken` | No\* | N/A | OAuth Token. Required (one of the three tokens are required) |
| `jobToken` | No\* | N/A | CI Job Token. Required (one of the three tokens are required) |
| `rejectUnauthorized` | Yes | `false` | Http Certificate setting |
| `sudo` | Yes | `false` | Sudo query parameter |
| `version` | Yes | `v4` | API Version ID |
| `camelize` | Yes | `false` | Response Key Camelize. Camelizes all response body keys |
| `requester` | Yes | [KyRequester.ts](./src/infrastructure/KyRequester.ts) | Request Library Wrapper. Currently wraps Ky. |
| `requestTimeout` | Yes | `300000` | Request Library Timeout in ms |
#### Bundle Imports
### Bundle Imports
It can be annoying to have to import all the API's pertaining to a specific resource. For example, the Projects resource is composed of many API's, Projects, Issues, Labels, MergeRequests, etc. For convenience, there is a Bundle export for importing and instantiating all these related API's at once.
@ -191,7 +219,7 @@ import { ProjectsBundle } from 'gitlab';
const services = new ProjectsBundle({
host: 'http://example.com',
token: 'abcdefghij123456'
token: 'personaltoken'
})
services.Projects.all()
@ -280,18 +308,6 @@ EpicNotes
EpicDiscussions
```
#### Handling HTTPS certificates
If your Gitlab server is running via HTTPS, the proper way to pass in your certificates is via a `NODE_EXTRA_CA_CERTS` environment key, like this:
```js
"scripts": {
"start": "NODE_EXTRA_CA_CERTS=./secrets/3ShapeCA.pem node bot.js"
},
```
> **NOTE**: _Using `process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'` will not work with the `gitlab` library. The `rejectUnauthorized` key is the only way to allow insecure certificates to be bypassed._
### Examples
Once you have your library instantiated, you can utilize many of the API's functionality:
@ -303,7 +319,7 @@ import { Gitlab } from 'gitlab';
const api = new Gitlab({
host: 'http://example.com',
token: 'abcdefghij123456',
token: 'personaltoken',
});
// Listing users
@ -327,7 +343,7 @@ import { Gitlab } from 'gitlab';
const api = new Gitlab({
host: 'http://example.com',
token: 'abcdefghij123456',
token: 'personaltoken',
});
api.Projects.create(projectId, {
@ -344,7 +360,7 @@ import { Gitlab } from 'gitlab';
const api = new Gitlab({
host: 'http://example.com',
token: 'abcdefghij123456',
token: 'personaltoken',
});
let projects = await api.Projects.all({ maxPages: 2 });
@ -357,7 +373,7 @@ import { Gitlab } from 'gitlab';
const api = new Gitlab({
host: 'http://example.com',
token: 'abcdefghij123456',
token: 'personaltoken',
});
let projects = await api.Projects.all({ maxPages: 2, perPage: 40 });
@ -405,7 +421,7 @@ import { NotificationSettings } from 'gitlab';
const service = new NotificationSettings({
host: 'http://example.com',
token: 'abcdefghij123456'
token: 'personaltoken'
sudo: 8 // Can be the user ID or a username
});
@ -428,22 +444,30 @@ import YourCustomRequester from 'custom-requester';
const api = new Gitlab({
host: 'http://example.com',
token: 'abcdefghij123456',
token: 'personaltoken',
requester: YourCustomRequester,
});
```
## Docs
Although there are the [official docs](https://github.com/gitlabhq/gitlabhq/tree/master/doc/api) for the API, there are some extra goodies offered by this package! After the 3.0.0 release, the next large project will be putting together proper documentation for these goodies [#39]! Stay tuned!!
### Misc
#### Handling HTTPS certificates
If your Gitlab server is running via HTTPS, the proper way to pass in your certificates is via a `NODE_EXTRA_CA_CERTS` environment key, like this:
```js
"scripts": {
"start": "NODE_EXTRA_CA_CERTS=./secrets/3ShapeCA.pem node bot.js"
},
```
> **NOTE**: _Using `process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'` will not work with the `gitlab` library. The `rejectUnauthorized` key is the only way to allow insecure certificates to be bypassed._
#### Non JSON/Text Responses
For responses such as file data that may be returned from the API, the data is exposed as a buffer. For example when trying to write a file, this can be done like:
```
```javascript
let bufferedData = await api.Jobs.downloadLatestArtifactFile(project.id, "test", "job_test);
fs.writeFileSync("test.zip", bufferedData);
@ -464,17 +488,17 @@ $ npm build
And then inside whatever project you are using `node-gitlab` in you change your references to use that repo. In your package.json of that upstream project change:
```json
"dependencies": {
"gitlab": "5.0.0"
}
"dependencies": {
"gitlab": "5.0.0"
}
```
to this
```json
"dependencies": {
"gitlab": "<path-to-your-clone>"
}
"dependencies": {
"gitlab": "<path-to-your-clone>"
}
```
## Testing

4873
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,9 @@
"name": "Justin Dalrymple",
"email": "justin.s.dalrymple@gmail.com"
},
"bin": {
"gitlab": "./dist/cli.js"
},
"browser": "dist/index.browser.js",
"bugs": {
"url": "https://github.com/jdalrymple/node-gitlab/issues"
@ -22,7 +25,8 @@
"ky-universal": "^0.3.0",
"li": "^1.3.0",
"query-string": "^6.8.2",
"universal-url": "^2.0.0"
"universal-url": "^2.0.0",
"yargs": "^14.0.0"
},
"devDependencies": {
"@semantic-release/changelog": "^3.0.4",
@ -30,8 +34,13 @@
"@semantic-release/npm": "^5.1.15",
"@types/humps": "^1.1.2",
"@types/jest": "^24.0.18",
"change-case": "^3.1.0",
"codecov": "^3.5.0",
"cz-conventional-changelog": "^3.0.2",
"dotenv": "^8.1.0",
"esm": "^3.2.25",
"fs-extra": "^8.1.0",
"get-param-names": "github:jdalrymple/get-param-names#1-improve-functionality",
"husky": "^4.0.0-beta.1",
"jest": "24.9.0",
"jest-extended": "^0.11.2",
@ -44,6 +53,7 @@
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.1.1",
"rollup-plugin-typescript2": "^0.24.0",
"semantic-release": "^15.13.24",
@ -64,6 +74,7 @@
"keywords": [
"api",
"browser",
"cli",
"es5",
"es6",
"gitlab",
@ -77,7 +88,8 @@
"url": "https://github.com/jdalrymple/node-gitlab"
},
"scripts": {
"build": "tsc && rollup -c",
"build": "npm run build:cli && tsc && rollup -c",
"build:cli": "tsc -p tsconfig.cli.json && node -r esm temp/bin/generate",
"commit": "npx git-cz",
"coverage": "codecov",
"lint": "prettier --check './src/**/*.ts' './test/**/*.ts' && tslint -p .",
@ -89,5 +101,6 @@
"test:integration": "jest test/integration -c=jest.config.json --detectOpenHandles",
"test:unit": "jest test/unit --debug -c=jest.config.json"
},
"type": "module",
"types": "dist/index.d.ts"
}

View File

@ -5,13 +5,38 @@ import json from 'rollup-plugin-json';
import { terser } from 'rollup-plugin-terser';
import builtins from 'rollup-plugin-node-builtins';
import globals from 'rollup-plugin-node-globals';
import replace from 'rollup-plugin-replace';
import typescript from 'typescript';
import pkg from './package.json';
export default [
// CLI build
{
input: 'src/bin/index.ts',
output: {
banner: '#!/usr/bin/env node',
file: pkg.bin.gitlab,
format: 'cjs',
},
external: [...Object.keys(pkg.dependencies)],
plugins: [
replace({
delimiters: ['', ''],
'#!/usr/bin/env node': '',
}),
globals(),
builtins(),
resolve(),
commonjs(),
json(),
ts({ typescript }),
terser(),
],
},
// Browser-friendly UMD build
{
input: 'src/index.ts',
input: 'src/core/index.ts',
output: {
file: pkg.browser,
name: 'node-gitlab',
@ -31,8 +56,8 @@ export default [
plugins: [
globals(),
builtins(),
resolve({ browser: true }), // so Rollup can find `ms`
commonjs(), // so Rollup can convert `ms` to an ES module
resolve({ browser: true }),
commonjs(),
json(),
ts({ typescript }),
terser(),
@ -41,7 +66,7 @@ export default [
// CommonJS (for Node) (for bundlers) build.
{
input: 'src/index.ts',
input: 'src/core/index.ts',
output: {
file: pkg.main,
format: 'cjs',
@ -52,7 +77,7 @@ export default [
// ES module (for bundlers) build.
{
input: 'src/index.ts',
input: 'src/core/index.ts',
output: {
file: pkg.module,
format: 'es',

66
src/bin/generate.ts Normal file
View File

@ -0,0 +1,66 @@
import getParamNames from 'get-param-names';
import { outputJsonSync, removeSync } from 'fs-extra';
import * as core from '../core';
import { BaseService } from '../core/infrastructure';
function isGetter(x, name) {
return (Object.getOwnPropertyDescriptor(x, name) || {}).get;
}
function isFunction(x, name) {
return typeof x[name] === 'function';
}
function deepFunctions(x) {
return (
x &&
x !== Object.prototype &&
Object.getOwnPropertyNames(x)
.filter(name => isGetter(x, name) || isFunction(x, name))
.concat(deepFunctions(Object.getPrototypeOf(x)) || [])
);
}
function distinctDeepFunctions(x) {
return Array.from(new Set(deepFunctions(x)));
}
function getInstanceMethods(x) {
// @ts-ignore
return distinctDeepFunctions(x).filter(name => name !== 'constructor' && !~name.indexOf('__'));
}
function removeOptionalArg(list) {
if (['options', '_a'].includes(list[list.length - 1])) list.pop();
return list;
}
// @ts-ignore
function buildMap() {
const map = {};
const baseArgs = Object.keys(getParamNames(BaseService)[0]);
for (const [name, service] of Object.entries(core)) {
if (name.includes('Bundle') || name === 'Gitlab') continue;
// @ts-ignore
const s = new service();
map[name] = [{ name: 'constructor', args: baseArgs }];
for (const m of getInstanceMethods(s) as string[]) {
map[name].push({
name: m,
args: removeOptionalArg(getParamNames(s[m])),
});
}
}
return map;
}
// Generate the services map
outputJsonSync('./dist/map.json', buildMap());
removeSync('./temp');

85
src/bin/index.ts Normal file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env node
import 'dotenv';
import { camelizeKeys } from 'humps';
import { param, constant } from 'change-case';
import program from 'yargs';
import pkg from '../../package.json';
import map from '../../dist/map.json';
import * as core from '../core';
// Add default options
program
.alias('v', 'version')
.version(pkg.version)
.describe('v', 'Show version information')
.alias('h', 'help')
.help('help')
.showHelpOnFail(false, 'Specify --help for available options');
// Add all supported API's
Object.entries(map).forEach(([name, methods]: [string, { name: string; args: string[] }[]]) => {
const baseArgs: string[] = methods[0].args;
program.command(param(name), `The ${name} API`, cmdYargs => {
cmdYargs
.option('gl-token', { describe: 'Your Gitlab Personal Token', type: 'string' })
.option('gl-oauth-token', { describe: 'Your Gitlab OAuth Token', type: 'string' })
.option('gl-job-token', { describe: 'Your Gitlab Job Token', type: 'string' })
.option('gl-host', {
describe: 'Your Gitlab API host (Defaults to https://www.gitlab.com)',
type: 'string',
})
.option('gl-version', {
describe: 'The targetted Gitlab API version (Defaults to v4)',
type: 'number',
})
.option('gl-sudo', { type: 'string' })
.option('gl-camelize', { type: 'boolean' });
for (let i = 1; i < methods.length; i += 1) {
const m = methods[i];
cmdYargs.command(
param(m.name),
'',
subCmdYargs => {
m.args.forEach(arg => {
if (arg === 'options') return;
subCmdYargs.option(`${param(arg)}`, {
demandOption: true,
});
});
return subCmdYargs;
},
args => {
const casedArgs = camelizeKeys(args);
const coreArgs = {};
const optionalArgs = {};
const initArgs = {};
for (const [name, value] of Object.entries(casedArgs)) {
const baseOption = baseArgs.find(x => x === name.replace('gl-', ''));
if (baseOption) {
initArgs[baseOption] = value || process.env[`GITLAB_${constant(baseOption)}`];
} else if (m.args.includes(name)) coreArgs[name] = value;
else optionalArgs[name] = value;
}
// Create service
const s = new core[name](initArgs);
// Execute function
return s[m.name](...Object.values(coreArgs), optionalArgs);
},
);
}
return cmdYargs;
});
});
program.epilog('Copyright 2019').argv;

View File

@ -21,7 +21,7 @@ export class BaseService {
rejectUnauthorized = true,
requester = KyRequester,
requestTimeout = 300000,
}: BaseServiceOptions) {
}: BaseServiceOptions = {}) {
this.url = [host, 'api', version, url].join('/');
this.headers = {};
this.rejectUnauthorized = rejectUnauthorized;

Some files were not shown because too many files have changed in this diff Show More