@zenvia/logger 1.4.3 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3
+
4
+ name: Node.js CI
5
+
6
+ on:
7
+ - push
8
+ - pull_request
9
+
10
+ jobs:
11
+ test:
12
+
13
+ runs-on: ubuntu-latest
14
+
15
+ strategy:
16
+ matrix:
17
+ node-version: [18.x]
18
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v3
23
+ - name: Use Node.js ${{ matrix.node-version }}
24
+ uses: actions/setup-node@v3
25
+ with:
26
+ node-version: ${{ matrix.node-version }}
27
+ cache: 'npm'
28
+ - name: Install dependencies
29
+ run: npm ci
30
+ - name: Run lint
31
+ run: npm run lint
32
+ - name: Run test
33
+ run: npm test
34
+ - name: Coveralls
35
+ uses: coverallsapp/github-action@v2
36
+ with:
37
+ github-token: ${{ secrets.GITHUB_TOKEN }}
38
+ path-to-lcov: ${{ github.workspace }}/coverage/lcov.info
@@ -0,0 +1,42 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Node.js Package
5
+
6
+ on:
7
+ release:
8
+ types: [created]
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v3
15
+ - uses: actions/setup-node@v3
16
+ with:
17
+ node-version: 18
18
+ - name: Install dependencies
19
+ run: npm ci
20
+ - name: Run lint
21
+ run: npm run lint
22
+ - name: Run test
23
+ run: npm test
24
+ - name: Coveralls
25
+ uses: coverallsapp/github-action@v2
26
+ with:
27
+ github-token: ${{ secrets.GITHUB_TOKEN }}
28
+ path-to-lcov: ${{ github.workspace }}/coverage/lcov.info
29
+
30
+ publish-npm:
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v3
35
+ - uses: actions/setup-node@v3
36
+ with:
37
+ node-version: 18
38
+ registry-url: https://registry.npmjs.org/
39
+ - run: npm ci
40
+ - run: npm publish
41
+ env:
42
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Zenvia Logger for Node.js
2
2
 
3
- A wrapper for [Winston](https://github.com/winstonjs/winston) Logging [Node.js](https://nodejs.org/) library that formats the output on STDOUT as [Logstash](https://www.elastic.co/logstash) JSON format.
3
+ A wrapper for [Winston](https://github.com/winstonjs/winston) Logging [Node.js](https://nodejs.org/) library that formats the output on STDOUT as [Logstash](https://www.elastic.co/logstash) JSON format. Since version 1.5.0 it is possible to use log tracking. Zenvia Logger uses the [cls-trace](https://www.npmjs.com/package/cls-rtracer) to perform the tracking
4
4
 
5
5
  [![License](https://img.shields.io/github/license/zenvia/zenvia-logger-node.svg)](LICENSE.md)
6
6
  [![Build Status](https://travis-ci.com/zenvia/zenvia-logger-node.svg?branch=master)](https://travis-ci.com/zenvia/zenvia-logger-node)
7
7
  [![Coverage Status](https://coveralls.io/repos/github/zenvia/zenvia-logger-node/badge.svg?branch=master)](https://coveralls.io/github/zenvia/zenvia-logger-node?branch=master)
8
8
  [![Dependencies](https://img.shields.io/david/zenvia/zenvia-logger-node.svg)](https://david-dm.org/zenvia/zenvia-logger-node)
9
9
 
10
- [![Twitter Follow](https://img.shields.io/twitter/follow/ZenviaMobile.svg?style=social)](https://twitter.com/intent/follow?screen_name=ZenviaMobile)
10
+ [![Twitter Follow](https://img.shields.io/twitter/follow/ZENVIA_.svg?style=social)](https://twitter.com/intent/follow?screen_name=ZENVIA_)
11
11
 
12
12
 
13
13
 
@@ -28,17 +28,62 @@ The following environment variables can be used for increase the log information
28
28
  - **HOST** or **HOSTNAME**: value to filled the "host" field in the output JSON.
29
29
  - **LOGGING_LEVEL**: set the level of messages that the logger should log. Default to `DEBUG`.
30
30
  - **LOGGING_FORMATTER_DISABLED** *(version 1.1.0 and above)*: When `true`, the output logging will not be formatted to JSON. Useful during development time. Default to `false`.
31
+ - **LOGGING_FRAMEWORK_MIDDLEWARE** *(version 1.5.0 and above)*: Value that defines which middleware will be used. It is possible to choose between the middlewares: EXPRESS, FASTIFY, HAPI and KOA. If empty, the middleware default is `EXPRESS`.
32
+ - **LOGGING_TRACE_HEADER** *(version 1.5.0 and above)*: Value indicating the header name that must be obtained from the traceId value in the request. Default is `X-TraceId`.
31
33
 
32
34
 
35
+ ## Basic Usage (Express users)
33
36
 
34
- ## Basic Usage
37
+ ```js
38
+ // ES6 or Typescript
39
+ import express from 'express';
40
+ import logger, { traceMiddleware } from '@zenvia/logger';
41
+
42
+ const app = express();
43
+ app.use(traceMiddleware);
44
+
45
+ logger.info('some message');
46
+ ```
47
+ ## Basic Usage (FASTIFY users)
48
+
49
+ ```js
50
+ // ES6 or Typescript
51
+ import fastify from 'fastify'
52
+ import logger, { traceMiddleware } from '@zenvia/logger';
53
+
54
+ fastify.register(traceMiddleware);
55
+
56
+ logger.info('some message');
57
+ ```
58
+ ## Basic Usage (KOA users)
35
59
 
36
60
  ```js
37
- // ES5
38
- const logger = require('@zenvia/logger');
61
+ // ES6 or Typescript
62
+ import Koa from 'koa';
63
+ import logger, { traceMiddleware } from '@zenvia/logger';
64
+
65
+ const app = new Koa();
66
+ app.use(traceMiddleware);
39
67
 
68
+ logger.info('some message');
69
+ ```
70
+ ## Basic Usage (HAPI users)
71
+
72
+ ```js
40
73
  // ES6 or Typescript
41
- import * as logger from '@zenvia/logger';
74
+ import Koa from 'koa';
75
+ import logger, { traceMiddleware } from '@zenvia/logger';
76
+
77
+ const init = async () => {
78
+ const server = Hapi.server({
79
+ port: 3000,
80
+ host: 'localhost'
81
+ });
82
+
83
+ await server.register({
84
+ plugin: traceMiddleware
85
+ });
86
+ }
42
87
 
43
88
  logger.info('some message');
44
89
  ```
@@ -51,7 +96,8 @@ Output:
51
96
  "@version": 1,
52
97
  "application": "application-name",
53
98
  "message": "some message",
54
- "level": "INFO"
99
+ "level": "INFO",
100
+ "traceID": "123e4567-e32b-12d3-a432-626614174888"
55
101
  }
56
102
  ```
57
103
 
@@ -111,7 +157,7 @@ Output:
111
157
  Due to limitations of winston lib, when a text, an error and extra key/value fields are logged at once, the output message field will contain the text message, the error message and the full stack trace as shown.
112
158
 
113
159
  ```js
114
- logger.fatal('Ops!', new Error('Something goes wrong'), { keyA: 'value A', keyB: 'value B' });
160
+ logger.fatal('Ops!', { new Error('Something goes wrong'), { keyA: 'value A', keyB: 'value B' } });
115
161
  ```
116
162
 
117
163
  Output:
@@ -128,7 +174,49 @@ Output:
128
174
  }
129
175
  ```
130
176
 
177
+ ### Using trace logs
178
+ From version 1.5.0 it is possible to track logs. To do traceability, the cls-rTrace package is used. To use it, just add the middleware in the framework you are using. In this way it is possible to propagate the traceId received in a request to the logs throughout your project. If no traceId is passed in the request, Zenvia Logger generates a random traceId for the request being processed.
131
179
 
180
+ **Request example sending traceId:**
181
+ ```bash
182
+ curl 'http://localhost/your-application' \
183
+ --header 'X-TraceId: dbcdd40e-10cd-40a7-b912-1b0a17483d67' \
184
+ ```
185
+ Log
186
+ ```javascript
187
+ logger.info('message with traceID');
188
+ ```
189
+ Log Output
190
+ ```json
191
+ {
192
+ "@timestamp": "2018-06-05T18:20:42.345Z",
193
+ "@version": 1,
194
+ "application": "application-name",
195
+ "message": "message with traceID",
196
+ "level": "INFO",
197
+ "traceID": "dbcdd40e-10cd-40a7-b912-1b0a17483d67'"
198
+ }
199
+ ```
200
+
201
+ **Request example without sending traceId:**
202
+ ```bash
203
+ curl 'http://localhost/your-application'
204
+ ```
205
+ Log
206
+ ```javascript
207
+ logger.info('message without traceID');
208
+ ```
209
+ Log Output
210
+ ```json
211
+ {
212
+ "@timestamp": "2018-06-05T18:20:42.345Z",
213
+ "@version": 1,
214
+ "application": "application-name",
215
+ "message": "message with traceID",
216
+ "level": "INFO",
217
+ "traceID": "912c029c-c38f-49e7-9968-e575c5108178'"
218
+ }
219
+ ```
132
220
 
133
221
  ## License
134
222
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@zenvia/logger",
3
- "version": "1.4.3",
3
+ "version": "1.6.1",
4
4
  "description": "A wrapper for Winston Logging Node.js library that formats the output on STDOUT as Logstash JSON format.",
5
5
  "license": "MIT",
6
- "main": "./src/lib/logger",
6
+ "main": "./src/index",
7
7
  "private": false,
8
8
  "publishConfig": {
9
9
  "access": "public"
@@ -33,18 +33,19 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "app-root-dir": "^1.0.2",
36
- "winston": "^3.3.3"
36
+ "cls-rtracer": "^2.6.3",
37
+ "winston": "^3.11.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "chai": "^4.2.0",
40
41
  "chai-as-promised": "^7.1.1",
41
42
  "coveralls": "^3.1.0",
42
- "eslint": "^7.6.0",
43
+ "eslint": "^7.10.0",
43
44
  "eslint-config-airbnb-base": "^14.2.0",
44
- "eslint-plugin-import": "^2.22.0",
45
- "mocha": "^8.1.0",
45
+ "eslint-plugin-import": "^2.22.1",
46
+ "mocha": "^8.1.3",
46
47
  "nyc": "^15.1.0",
47
- "sinon": "^9.0.2",
48
+ "sinon": "^9.2.0",
48
49
  "sinon-chai": "^3.5.0",
49
50
  "std-mocks": "^1.0.1"
50
51
  },
package/src/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default } from './lib/logger';
2
+ export { default as traceMiddleware } from './middleware/trace';
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ Object.defineProperty(exports, '__esModule', {
2
+ value: true,
3
+ });
4
+
5
+ const logger = require('./lib/logger');
6
+ const traceMiddleware = require('./middleware/trace');
7
+
8
+ exports.default = logger;
9
+ exports.traceMiddleware = traceMiddleware();
@@ -1,11 +1,9 @@
1
- declare module '@zenvia/logger' {
2
- import { Logger, LeveledLogMethod } from 'winston';
1
+ import { Logger, LeveledLogMethod } from 'winston';
3
2
 
4
- interface ZenviaLogger extends Logger {
5
- fatal: LeveledLogMethod;
6
- isFatalEnabled(): boolean;
7
- }
8
-
9
- const logger: ZenviaLogger;
10
- export = logger;
3
+ interface ZenviaLogger extends Logger {
4
+ fatal: LeveledLogMethod;
5
+ isFatalEnabled(): boolean;
11
6
  }
7
+
8
+ declare const logger: ZenviaLogger;
9
+ export default logger;
package/src/lib/logger.js CHANGED
@@ -6,10 +6,29 @@
6
6
  const winston = require('winston');
7
7
  const path = require('path');
8
8
  const appRootDir = require('app-root-dir').get();
9
+ const rTrace = require('cls-rtracer');
9
10
 
10
11
  const appPackage = require(path.join(appRootDir, 'package'));
11
12
 
13
+ const sanitizeInfo = (info) => {
14
+ const sanitizeCRLFInjection = (str) => str
15
+ .replace(/\n|\r/g, (x) => (x === '\n' ? '#n' : '#r'));
16
+
17
+ Object.keys(info).forEach((key) => {
18
+ if (typeof info[key] === 'string') {
19
+ info[key] = sanitizeCRLFInjection(info[key]);
20
+ return;
21
+ }
22
+
23
+ if (info[key] instanceof Function) {
24
+ delete info[key];
25
+ }
26
+ });
27
+ };
28
+
12
29
  const customFormatJson = winston.format((info) => {
30
+ sanitizeInfo(info);
31
+
13
32
  let stack;
14
33
 
15
34
  if (info.stack) {
@@ -27,12 +46,14 @@ const customFormatJson = winston.format((info) => {
27
46
  message: info.message || '',
28
47
  level: ['verbose', 'silly'].includes(info.level) ? 'DEBUG' : info.level.toUpperCase(),
29
48
  stack_trace: stack,
49
+ traceId: rTrace.id(),
30
50
  };
31
51
 
32
52
  return info;
33
53
  });
34
54
 
35
55
  const customCombineJson = winston.format.combine(
56
+ winston.format.splat(),
36
57
  customFormatJson(),
37
58
  winston.format.json(),
38
59
  );
@@ -0,0 +1,9 @@
1
+ import rTracer from 'cls-rtracer';
2
+
3
+ declare const traceMiddleware: () =>
4
+ ReturnType<typeof rTracer.expressMiddleware> |
5
+ ReturnType<typeof rTracer.fastifyPlugin> |
6
+ typeof rTracer.hapiPlugin |
7
+ ReturnType<typeof rTracer.koaMiddleware>;
8
+
9
+ export default traceMiddleware;
@@ -0,0 +1,25 @@
1
+ const rTracer = require('cls-rtracer');
2
+
3
+ module.exports = () => {
4
+ const frameworkMiddleware = process.env.LOGGING_FRAMEWORK_MIDDLEWARE;
5
+
6
+ const RTRACER_OPTIONS = {
7
+ useHeader: true,
8
+ headerName: process.env.LOGGING_TRACE_HEADER || 'X-TraceId',
9
+ echoHeader: true,
10
+ };
11
+
12
+ const middleware = {
13
+ EXPRESS: rTracer.expressMiddleware(RTRACER_OPTIONS),
14
+ FASTIFY: rTracer.fastifyPlugin,
15
+ HAPI: rTracer.hapiPlugin,
16
+ KOA: rTracer.koaMiddleware(RTRACER_OPTIONS),
17
+ };
18
+
19
+ // eslint-disable-next-line no-prototype-builtins
20
+ if (frameworkMiddleware && !middleware.hasOwnProperty(frameworkMiddleware)) {
21
+ throw new Error(`Invalid framework middleware value. Please check environment variable "LOGGING_FRAMEWORK_MIDDLEWARE". Allowed values: [${Object.keys(middleware)}]`);
22
+ }
23
+
24
+ return middleware[frameworkMiddleware || 'EXPRESS'];
25
+ };
@@ -0,0 +1,9 @@
1
+ const { assert } = require('chai');
2
+ const { default: logger, traceMiddleware } = require('../src');
3
+
4
+ describe('Logger test', () => {
5
+ it('should load correct module', () => {
6
+ assert.isNotNull(logger);
7
+ assert.isNotNull(traceMiddleware);
8
+ });
9
+ });
@@ -241,6 +241,50 @@ describe('Logger test', () => {
241
241
  });
242
242
 
243
243
  describe('Logging format', () => {
244
+ it('should replace LF characters from log (POSIX systems)', () => {
245
+ logger.debug(`some message
246
+ other CRLF injection message`);
247
+ const expectedOutput = {
248
+ '@timestamp': '2018-06-05T18:20:42.345Z',
249
+ '@version': 1,
250
+ application: 'application-name',
251
+ host: os.hostname(),
252
+ message: 'some message#nother CRLF injection message',
253
+ level: 'DEBUG',
254
+ };
255
+
256
+ const actualOutput = stdMocks.flush().stdout[0];
257
+ JSON.parse(actualOutput).should.be.deep.equal(expectedOutput);
258
+
259
+ logger.debug('some\n CRLF\n injection\n message');
260
+ const expectedOutput2 = {
261
+ '@timestamp': '2018-06-05T18:20:42.345Z',
262
+ '@version': 1,
263
+ application: 'application-name',
264
+ host: os.hostname(),
265
+ message: 'some#n CRLF#n injection#n message',
266
+ level: 'DEBUG',
267
+ };
268
+
269
+ const actualOutput2 = stdMocks.flush().stdout[0];
270
+ JSON.parse(actualOutput2).should.be.deep.equal(expectedOutput2);
271
+ });
272
+
273
+ it('should replace CRLF characters from log (Windows systems)', () => {
274
+ logger.debug('some\r\n CRLF\r\n injection\r\n message');
275
+ const expectedOutput = {
276
+ '@timestamp': '2018-06-05T18:20:42.345Z',
277
+ '@version': 1,
278
+ application: 'application-name',
279
+ host: os.hostname(),
280
+ message: 'some#r#n CRLF#r#n injection#r#n message',
281
+ level: 'DEBUG',
282
+ };
283
+
284
+ const actualOutput = stdMocks.flush().stdout[0];
285
+ JSON.parse(actualOutput).should.be.deep.equal(expectedOutput);
286
+ });
287
+
244
288
  it('should get not format when LOGGING_FORMATTER_DISABLED environment is true', () => {
245
289
  delete require.cache[require.resolve('../../src/lib/logger')];
246
290
  process.env.LOGGING_FORMATTER_DISABLED = 'true';
@@ -0,0 +1,117 @@
1
+ const stdMocks = require('std-mocks');
2
+ const rTrace = require('cls-rtracer');
3
+ const { assert } = require('chai');
4
+ const sinon = require('sinon');
5
+ const traceMiddleware = require('../../src/middleware/trace');
6
+
7
+ const DEFAULT_HEADER_NAME = 'X-TraceId';
8
+
9
+ describe('Trace middleware test', () => {
10
+ let rTraceSpy;
11
+
12
+ before(() => {
13
+ stdMocks.use({ print: true });
14
+ rTraceSpy = {
15
+ EXPRESS: sinon.spy(rTrace, 'expressMiddleware'),
16
+ KOA: sinon.spy(rTrace, 'koaMiddleware'),
17
+ };
18
+ });
19
+
20
+ beforeEach(() => {
21
+ stdMocks.flush();
22
+ });
23
+
24
+ afterEach(() => {
25
+ stdMocks.restore();
26
+ delete process.env.LOGGING_FRAMEWORK_MIDDLEWARE;
27
+ delete process.env.LOGGING_TRACE_HEADER;
28
+ });
29
+
30
+ describe('Return correct middleware', () => {
31
+ it('should return correct middlware EXPRESS when it is use default values', () => {
32
+ const RTRACER_OPTIONS = {
33
+ useHeader: true,
34
+ headerName: DEFAULT_HEADER_NAME,
35
+ echoHeader: true,
36
+ };
37
+ const middleware = traceMiddleware();
38
+
39
+ assert.isFunction(middleware);
40
+ assert.isTrue(rTraceSpy.EXPRESS.calledWith(RTRACER_OPTIONS));
41
+ assert.equal(rTrace.expressMiddleware(RTRACER_OPTIONS).toString(), middleware.toString());
42
+ });
43
+
44
+ it('should return correct KOA middleware when environment is set to KOA', () => {
45
+ process.env.LOGGING_FRAMEWORK_MIDDLEWARE = 'KOA';
46
+ const RTRACER_OPTIONS = {
47
+ useHeader: true,
48
+ headerName: DEFAULT_HEADER_NAME,
49
+ echoHeader: true,
50
+ };
51
+ const middleware = traceMiddleware();
52
+
53
+ assert.isFunction(middleware);
54
+ assert.isTrue(rTraceSpy.KOA.calledWith(RTRACER_OPTIONS));
55
+ assert.equal(rTrace.koaMiddleware(RTRACER_OPTIONS).toString(), middleware.toString());
56
+ });
57
+
58
+ it('should return correct FASTIFY middleware when environment is set to FASTIFY', () => {
59
+ process.env.LOGGING_FRAMEWORK_MIDDLEWARE = 'FASTIFY';
60
+ const middleware = traceMiddleware();
61
+
62
+ assert.isFunction(middleware);
63
+ assert.equal(rTrace.fastifyPlugin.toString(), middleware.toString());
64
+ });
65
+
66
+ it('should return correct HAPI middleware when environment is set to HAPI', () => {
67
+ process.env.LOGGING_FRAMEWORK_MIDDLEWARE = 'HAPI';
68
+ const middleware = traceMiddleware();
69
+
70
+ assert.isNotNull(middleware);
71
+ assert.equal(rTrace.hapiPlugin, middleware);
72
+ });
73
+
74
+ it('should throw error "Invalid framework middleware value" when is set invalid value in LOGGING_FRAMEWORK_MIDDLEWARE environment variable', () => {
75
+ process.env.LOGGING_FRAMEWORK_MIDDLEWARE = 'INVALID_VALUE';
76
+ try {
77
+ traceMiddleware();
78
+ } catch (error) {
79
+ assert.equal(
80
+ error.message,
81
+ 'Invalid framework middleware value. Please check environment variable "LOGGING_FRAMEWORK_MIDDLEWARE". Allowed values: [EXPRESS,FASTIFY,HAPI,KOA]',
82
+ );
83
+ }
84
+ });
85
+ });
86
+
87
+ describe('Use correct rTracer options', () => {
88
+ it('should use default header name when is not set LOGGING_TRACE_HEADER', () => {
89
+ process.env.LOGGING_FRAMEWORK_MIDDLEWARE = 'EXPRESS';
90
+ const RTRACER_OPTIONS = {
91
+ useHeader: true,
92
+ headerName: DEFAULT_HEADER_NAME,
93
+ echoHeader: true,
94
+ };
95
+ const middleware = traceMiddleware();
96
+
97
+ assert.isFunction(middleware);
98
+ assert.isTrue(rTraceSpy.EXPRESS.calledWith(RTRACER_OPTIONS));
99
+ });
100
+
101
+ it('should use correct header name when is set LOGGING_TRACE_HEADER', () => {
102
+ const headerName = 'newHeaderName';
103
+ process.env.LOGGING_FRAMEWORK_MIDDLEWARE = 'EXPRESS';
104
+ process.env.LOGGING_TRACE_HEADER = headerName;
105
+ const RTRACER_OPTIONS = {
106
+ useHeader: true,
107
+ headerName,
108
+ echoHeader: true,
109
+ };
110
+ const middleware = traceMiddleware();
111
+
112
+ assert.isFunction(middleware);
113
+ assert.isTrue(rTraceSpy.EXPRESS.calledWith(RTRACER_OPTIONS));
114
+ assert.equal(rTrace.expressMiddleware(RTRACER_OPTIONS).toString(), middleware.toString());
115
+ });
116
+ });
117
+ });
package/.travis.yml DELETED
@@ -1,27 +0,0 @@
1
- language: node_js
2
-
3
- node_js:
4
- - 12
5
-
6
- before_script:
7
- - npm ci
8
-
9
- script:
10
- - npm run lint
11
- - npm test
12
-
13
- after_success:
14
- - npm run test:coveralls
15
-
16
- deploy:
17
- provider: npm
18
- email: $NPM_EMAIL
19
- api_key: $NPM_TOKEN
20
- skip_cleanup: true
21
- on:
22
- tags: true
23
-
24
- notifications:
25
- email:
26
- recipients:
27
- - apisupport@zenvia.com