connect-ready 1.0.13 → 2.0.0

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.
package/README.md CHANGED
@@ -1,77 +1,122 @@
1
1
  # connect-ready
2
2
 
3
- [![Build Status](https://travis-ci.org/dcolens/connect-ready.svg?branch=master)](https://travis-ci.org/dcolens/connect-ready) [![Coverage Status](https://coveralls.io/repos/github/dcolens/connect-ready/badge.svg?branch=master)](https://coveralls.io/github/dcolens/connect-ready?branch=master)
3
+ Express/Connect readiness route for Kubernetes applications.
4
4
 
5
- express route that indicates whether a service is ready or not. Mostly created to make graceful restart of node express servers in a Kubernetes environment.
5
+ ## Requirements
6
6
 
7
+ - Node.js 22 or newer
7
8
 
8
- ## Graceful restart of a nodejs express server in Kubernetes
9
+ ## Installation
9
10
 
10
- I intially thought that catching SIGTERM and waiting for server.close()` to finish would be enough to do a graceful restart of a nodejs service. I was wrong.
11
+ ```shell
12
+ npm install connect-ready
13
+ ```
14
+
15
+ ## Readiness route
11
16
 
12
- The reliable way of handling a graceful restart is to use the [readynessProbe](http://kubernetes.io/docs/user-guide/production-pods/#liveness-and-readiness-probes-aka-health-checks) functionality either with a [pre-stop hook](http://kubernetes.io/docs/user-guide/container-environment/#container-hooks) or when catching the SIGTERM signal. The readynessProbes are used by Kubernetes to know if a service is ready and can receive traffic, in its http form Kubernetes checks for the responseCode, anything above 399 is considered not ready.
17
+ The route returns `503` until the application explicitly becomes ready.
13
18
 
14
- When a service receives a stop signal (SIGTERM), it should respond with a 500 responsecode when probed for readiness, this will ensure Kubernetes does not send load to it anymore. Once that's done, `server.close()` can be called to ensure ongoing connections are terminated gracefully.
19
+ ```javascript
20
+ 'use strict';
15
21
 
16
- Note that by default Kubernetes will send a SIGKILL 30s after the SIGTERM if the service did not terminate, this timer is configurable in the manifest.
22
+ const http = require('node:http');
23
+ const express = require('express');
24
+ const ready = require('connect-ready');
25
+
26
+ const app = express();
27
+ const server = http.createServer(app);
28
+
29
+ app.get('/ready', ready.route);
17
30
 
31
+ server.listen(3000, () => {
32
+ ready.setStatus(204);
33
+ });
34
+ ```
18
35
 
19
- ## Example of a graceful node http server for Kubernetes
36
+ Kubernetes considers HTTP responses from 200 through 399 successful. Use a failure status such as `503` whenever the application cannot accept traffic:
20
37
 
21
38
  ```javascript
22
- 'use strict';
23
- var http = require('http');
24
- var express = require('express');
25
- var ready = require('connect-ready');
39
+ ready.setStatus(503);
40
+ ```
26
41
 
27
- var app = express();
28
- var server = http.createServer(app);
42
+ ## Graceful shutdown in Kubernetes
29
43
 
44
+ Modern Kubernetes marks a terminating Pod endpoint as not ready. The application must still stop accepting new connections and allow active requests to finish.
30
45
 
31
- app.get('/ready', ready.route);
46
+ `registerShutdownHandlers()` installs a consistent lifecycle for Node HTTP servers:
32
47
 
33
- /**
34
- * adds a `Connection: close` to all responses stopping.
35
- */
36
- app.use(ready.gracefulShutdownKeepaliveConnections);
48
+ - `SIGTERM` and `SIGINT` drain active requests and exit `0`;
49
+ - `uncaughtException` and `unhandledRejection` attempt bounded cleanup and exit `1`;
50
+ - readiness changes to `503` as soon as shutdown starts;
51
+ - `server.close()` immediately stops new connections and drains active requests;
52
+ - the process force-closes HTTP connections and exits `1` if its deadline expires.
37
53
 
38
- server.listen(3000, function () {
39
- ready.setStatus(204);
40
- console.log('Example app listening on port 3000!');
54
+ ```javascript
55
+ const shutdown = ready.registerShutdownHandlers(server, {
56
+ // Keep this below the Pod's terminationGracePeriodSeconds.
57
+ timeoutMs: Number.parseInt(process.env.SHUTDOWN_TIMEOUT_MS ?? '30000', 10),
58
+
59
+ // Fatal process errors should not drain for as long as a normal rollout.
60
+ fatalTimeoutMs: 30_000,
61
+
62
+ async cleanup() {
63
+ await database.close();
64
+ await log4js.shutdown();
65
+ },
66
+
67
+ onFatal(error, origin) {
68
+ logger.fatal({ error, origin }, 'Fatal process error');
69
+ },
70
+
71
+ // Optional: server.closeAllConnections() does not close upgraded protocols.
72
+ forceClose() {
73
+ webSocketServer.close();
74
+ },
41
75
  });
76
+ ```
42
77
 
78
+ Node.js 22 `server.close()` stops accepting new connections, closes idle keep-alive connections, and waits for active HTTP requests to complete. Dependencies are cleaned up after those requests drain.
43
79
 
44
- //add graceful shutdown
45
- process.on('SIGTERM', function () {
46
- ready.setStatus(500);
47
- console.log('received SIGTERM');
48
-
49
- /**
50
- * delay the server closure by 2s to give kubernetes time to
51
- * know the service is not ready and direct the traffic somewhere else.
52
- * Instead of listening for SIGTERM, one could also configure a
53
- * pre-stop hook in the kubernetes manifest.
54
- */
55
- setTimeout(function() {
56
- server.close(function() {
57
- console.log('all connections closed');
58
- process.exit(0);
59
- });
60
- }, 2000);
61
- });
80
+ Set `timeoutMs` slightly below the Pod's `terminationGracePeriodSeconds`. The Kubernetes grace period must be long enough for the longest active request plus dependency cleanup and a safety margin. Applications with requests lasting up to 45 minutes should configure both deadlines accordingly.
81
+
82
+ An uncaught exception can leave application state inconsistent, so `fatalTimeoutMs` defaults to the smaller of 30 seconds and `timeoutMs`. Fatal shutdown still attempts to drain and clean up, but it must exit non-zero within that shorter deadline.
83
+
84
+ A controller can also initiate shutdown or remove its process listeners explicitly:
85
+
86
+ ```javascript
87
+ shutdown.close(); // Start a normal programmatic shutdown.
88
+ shutdown.dispose(); // Or unregister the handlers if another component takes ownership.
62
89
  ```
63
90
 
64
- ## toobusy option
91
+ ## API
92
+
93
+ ### `setStatus(code)`
94
+
95
+ Sets the readiness HTTP status. The code must be an integer from 100 through 599.
96
+
97
+ ### `getStatus()`
98
+
99
+ Returns the current readiness HTTP status.
100
+
101
+ ### `route(req, res)`
102
+
103
+ Express/Connect route that responds with the current readiness status.
104
+
105
+ ### `registerShutdownHandlers(server, options)`
106
+
107
+ Registers handlers for normal process signals and fatal process events. Returns a shutdown controller.
65
108
 
66
- Another use of the readinessProbe can be to indicate if the server is too busy, connect-ready can use the [toobusy-js](https://github.com/STRML/node-toobusy) module to indicate whether the server is too busy and deflect load to another pod.
109
+ Options:
67
110
 
68
- **The toobusy-js module should be installed to use this functionality, it is not bundled in connect-ready.**
111
+ - `timeoutMs`: normal shutdown deadline; defaults to 30 seconds.
112
+ - `fatalTimeoutMs`: fatal-error deadline; defaults to at most 30 seconds.
113
+ - `cleanup(context)`: async dependency cleanup after HTTP requests drain.
114
+ - `onFatal(error, origin)`: fatal-error reporting hook.
115
+ - `forceClose(context)`: closes upgraded or custom connections at the deadline.
69
116
 
70
- ### Usage
117
+ The controller provides:
71
118
 
72
- 1. npm install toobusy-js
73
- 2. enable toobusy in connect-ready:
74
- ```javascript
75
- ready.enableTooBusy(70)
76
- ```
77
- Where 70 is the lag as defined in [toobusy-js](https://github.com/STRML/node-toobusy)
119
+ - `close()`: starts a normal programmatic shutdown.
120
+ - `shutdown(request)`: starts shutdown with an explicit reason or error.
121
+ - `dispose()`: unregisters the installed process handlers.
122
+ - `isShuttingDown`: indicates whether shutdown has started.
package/index.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ interface ShutdownContext {
2
+ reason: string;
3
+ error?: Error;
4
+ origin?: string;
5
+ fatal: boolean;
6
+ forced: boolean;
7
+ }
8
+
9
+ interface ShutdownOptions {
10
+ timeoutMs?: number;
11
+ fatalTimeoutMs?: number;
12
+ cleanup?: (context: ShutdownContext) => void | Promise<void>;
13
+ forceClose?: (context: ShutdownContext) => void;
14
+ onFatal?: (error: Error, origin: string) => void;
15
+ exit?: (code: number) => void;
16
+ }
17
+
18
+ interface ShutdownRequest {
19
+ reason?: string;
20
+ error?: unknown;
21
+ fatal?: boolean;
22
+ }
23
+
24
+ interface ClosableServer {
25
+ close(callback: (error?: Error) => void): unknown;
26
+ closeAllConnections?(): void;
27
+ }
28
+
29
+ interface ShutdownController {
30
+ readonly isShuttingDown: boolean;
31
+ close(): Promise<number>;
32
+ dispose(): void;
33
+ shutdown(request?: ShutdownRequest): Promise<number>;
34
+ }
35
+
36
+ declare function setStatus(code: number): void;
37
+ declare function getStatus(): number;
38
+ declare function route(req: any, res: any): void;
39
+ declare function registerShutdownHandlers(
40
+ server: ClosableServer,
41
+ options?: ShutdownOptions,
42
+ ): ShutdownController;
43
+
44
+ export {
45
+ ClosableServer,
46
+ ShutdownContext,
47
+ ShutdownController,
48
+ ShutdownOptions,
49
+ ShutdownRequest,
50
+ getStatus,
51
+ registerShutdownHandlers,
52
+ route,
53
+ setStatus,
54
+ };
package/index.js CHANGED
@@ -1,113 +1,252 @@
1
- "use strict";
2
- var fs = require("fs");
1
+ 'use strict';
3
2
 
4
- var status = 500;
5
- var toobusy = false;
6
- var stopping = false;
3
+ let status = 503;
7
4
 
5
+ /**
6
+ * Set the HTTP status returned by the readiness route.
7
+ *
8
+ * Kubernetes considers responses from 200 through 399 successful.
9
+ *
10
+ * @param {number} code HTTP status code
11
+ */
8
12
  function setStatus(code) {
9
- if (!Number.isInteger(code) || code < 1 || code > 999) {
10
- var e = new Error("status should be an integer between 1 and 999");
11
- e.status = code;
12
- throw e;
13
- }
14
- status = code;
15
- }
13
+ if (!Number.isInteger(code) || code < 100 || code > 599) {
14
+ const error = new Error('status should be an integer between 100 and 599');
15
+ error.status = code;
16
+ throw error;
17
+ }
16
18
 
17
- function getStatus() {
18
- return status;
19
+ status = code;
19
20
  }
20
21
 
21
22
  /**
22
- * returns 503 if server is toobusy, status (as defined by setStatus) otherwise.
23
+ * Return the status currently exposed by the readiness route.
24
+ *
25
+ * @returns {number} HTTP status code
23
26
  */
24
- function route(req, res) {
25
- if (toobusy && toobusy()) {
26
- res.sendStatus(503).end();
27
- } else {
28
- res.sendStatus(status).end();
29
- }
27
+ function getStatus() {
28
+ return status;
30
29
  }
31
30
 
32
31
  /**
33
- * adds a `Connection: close` to all responses if app.get('stopping') is true.
32
+ * Express/Connect readiness route.
34
33
  */
35
- function gracefulShutdownKeepaliveConnections(req, res, next) {
36
- if (stopping === true) {
37
- res.set("Connection", "close");
38
- }
39
- next();
34
+ function route(_req, res) {
35
+ res.sendStatus(status);
40
36
  }
41
37
 
42
- function enableTooBusy(lag) {
43
- if (typeof lag === "undefined") {
44
- lag = 70;
45
- }
46
- if (!Number.isInteger(lag) || lag < 10) {
47
- var e = new Error("lag should be an integer greater than 10");
48
- e.lag = lag;
49
- throw e;
50
- }
51
- toobusy = require("toobusy-js");
52
- toobusy.maxLag(lag);
38
+ function validateTimeout(name, value) {
39
+ if (!Number.isInteger(value) || value < 0) {
40
+ throw new TypeError(`${name} should be a non-negative integer`);
41
+ }
42
+ }
43
+
44
+ function validateFunction(name, value) {
45
+ if (value !== undefined && typeof value !== 'function') {
46
+ throw new TypeError(`${name} should be a function`);
47
+ }
48
+ }
49
+
50
+ function normalizeError(value, message) {
51
+ if (value instanceof Error) return value;
52
+ return new Error(message, { cause: value });
53
53
  }
54
54
 
55
55
  /**
56
- * - logs shutdown to graylog (if log4js logger provided).
57
- * - sets `stopping` to true.
58
- * - dumps error in terminationFile if provided.
56
+ * Register a bounded graceful-shutdown lifecycle for a Node HTTP server.
57
+ *
58
+ * Normal process signals drain active HTTP requests and exit successfully.
59
+ * Fatal process events use a shorter deadline and exit unsuccessfully.
60
+ *
61
+ * @param {object} server Node HTTP server
62
+ * @param {object} [options] lifecycle options
63
+ * @returns {object} shutdown controller
59
64
  */
60
- function shutdown(signal, error, cb, terminationFile, logger) {
61
- status = 503;
62
- var reason;
63
- if (error) {
64
- if (logger && logger.fatal) {
65
- let uError = {};
66
- const errorKeys = Object.keys(error);
67
- for (let index = 0; index < errorKeys.length; index++) {
68
- const key = errorKeys[index];
69
- uError[`_${key}`] = error[key];
70
- }
71
- logger.fatal(
72
- { GELF: true, _signal: signal, _stack: error.stack, ...uError },
73
- error.message
74
- );
75
- }
76
- reason = `${signal}\n${error.message}\n${error.stack}`;
77
- } else {
78
- if (logger && logger.info) {
79
- logger.info({ GELF: true, _signal: signal }, "shutdown");
80
- }
81
- reason = "shutdown";
82
- }
83
- if (stopping === true) {
84
- return;
85
- }
86
- stopping = true;
87
-
88
- function callback() {
89
- if (cb) {
90
- return cb(signal ? 1 : 0);
91
- }
92
- }
93
-
94
- if (terminationFile) {
95
- fs.writeFile(terminationFile, reason, function (err) {
96
- if (err) {
97
- console.error(err);
98
- }
99
- callback();
100
- });
101
- } else {
102
- callback();
103
- }
65
+ function registerShutdownHandlers(server, options = {}) {
66
+ if (!server || typeof server.close !== 'function') {
67
+ throw new TypeError('server should provide a close(callback) function');
68
+ }
69
+
70
+ const timeoutMs = options.timeoutMs ?? 30_000;
71
+ const fatalTimeoutMs = options.fatalTimeoutMs ?? Math.min(timeoutMs, 30_000);
72
+ const cleanup = options.cleanup ?? (async () => {});
73
+ const forceClose = options.forceClose;
74
+ const onFatal = options.onFatal;
75
+ const exit = options.exit ?? process.exit.bind(process);
76
+
77
+ validateTimeout('timeoutMs', timeoutMs);
78
+ validateTimeout('fatalTimeoutMs', fatalTimeoutMs);
79
+ validateFunction('cleanup', cleanup);
80
+ validateFunction('forceClose', forceClose);
81
+ validateFunction('onFatal', onFatal);
82
+ validateFunction('exit', exit);
83
+
84
+ let shuttingDown = false;
85
+ let finished = false;
86
+ let exitCode = 0;
87
+ let timer;
88
+ let timerDeadline = Infinity;
89
+ let resolveCompletion;
90
+ let completion;
91
+ let context;
92
+
93
+ function dispose() {
94
+ process.removeListener('SIGTERM', onSigterm);
95
+ process.removeListener('SIGINT', onSigint);
96
+ process.removeListener('uncaughtException', onUncaughtException);
97
+ process.removeListener('unhandledRejection', onUnhandledRejection);
98
+ }
99
+
100
+ function reportFatal(error, origin) {
101
+ exitCode = 1;
102
+ context.error = error;
103
+ context.origin = origin;
104
+ context.fatal = true;
105
+
106
+ if (onFatal) {
107
+ try {
108
+ onFatal(error, origin);
109
+ } catch (reportingError) {
110
+ console.error(reportingError);
111
+ }
112
+ }
113
+ }
114
+
115
+ function complete(code) {
116
+ if (finished) return;
117
+ finished = true;
118
+ clearTimeout(timer);
119
+ dispose();
120
+ resolveCompletion(code);
121
+ exit(code);
122
+ }
123
+
124
+ function forceShutdown() {
125
+ if (finished) return;
126
+
127
+ const forcedContext = { ...context, forced: true };
128
+
129
+ try {
130
+ forceClose?.(forcedContext);
131
+ } catch (error) {
132
+ reportFatal(error, 'forceClose');
133
+ }
134
+
135
+ try {
136
+ server.closeAllConnections?.();
137
+ } catch (error) {
138
+ reportFatal(error, 'server.closeAllConnections');
139
+ }
140
+
141
+ complete(1);
142
+ }
143
+
144
+ function scheduleDeadline(delayMs) {
145
+ const deadline = Date.now() + delayMs;
146
+ if (deadline >= timerDeadline) return;
147
+
148
+ clearTimeout(timer);
149
+ timerDeadline = deadline;
150
+ timer = setTimeout(forceShutdown, delayMs);
151
+ timer.unref?.();
152
+ }
153
+
154
+ async function finishGracefully(serverError) {
155
+ if (finished) return;
156
+
157
+ if (serverError) {
158
+ reportFatal(serverError, 'server.close');
159
+ }
160
+
161
+ try {
162
+ await cleanup({ ...context, forced: false });
163
+ } catch (error) {
164
+ reportFatal(error, 'cleanup');
165
+ }
166
+
167
+ complete(exitCode);
168
+ }
169
+
170
+ function shutdown(request = {}) {
171
+ const reason = request.reason ?? 'manual';
172
+ const error = request.error;
173
+ const fatal = request.fatal ?? error !== undefined;
174
+
175
+ if (shuttingDown) {
176
+ if (fatal) {
177
+ const fatalError = normalizeError(error, `Fatal shutdown requested by ${reason}`);
178
+ reportFatal(fatalError, reason);
179
+ scheduleDeadline(fatalTimeoutMs);
180
+ }
181
+ return completion;
182
+ }
183
+
184
+ shuttingDown = true;
185
+ exitCode = fatal ? 1 : 0;
186
+ context = { reason, error: undefined, origin: undefined, fatal, forced: false };
187
+ completion = new Promise(resolve => {
188
+ resolveCompletion = resolve;
189
+ });
190
+
191
+ setStatus(503);
192
+
193
+ if (fatal) {
194
+ const fatalError = normalizeError(error, `Fatal shutdown requested by ${reason}`);
195
+ reportFatal(fatalError, reason);
196
+ }
197
+
198
+ scheduleDeadline(fatal ? fatalTimeoutMs : timeoutMs);
199
+
200
+ try {
201
+ server.close(serverError => {
202
+ void finishGracefully(serverError);
203
+ });
204
+ } catch (serverError) {
205
+ void finishGracefully(serverError);
206
+ }
207
+
208
+ return completion;
209
+ }
210
+
211
+ function close() {
212
+ return shutdown({ reason: 'manual' });
213
+ }
214
+
215
+ function onSigterm() {
216
+ void shutdown({ reason: 'SIGTERM' });
217
+ }
218
+
219
+ function onSigint() {
220
+ void shutdown({ reason: 'SIGINT' });
221
+ }
222
+
223
+ function onUncaughtException(error, origin) {
224
+ void shutdown({ reason: origin ?? 'uncaughtException', error, fatal: true });
225
+ }
226
+
227
+ function onUnhandledRejection(reason) {
228
+ const error = normalizeError(reason, 'Unhandled promise rejection');
229
+ void shutdown({ reason: 'unhandledRejection', error, fatal: true });
230
+ }
231
+
232
+ process.once('SIGTERM', onSigterm);
233
+ process.once('SIGINT', onSigint);
234
+ process.once('uncaughtException', onUncaughtException);
235
+ process.once('unhandledRejection', onUnhandledRejection);
236
+
237
+ return {
238
+ close,
239
+ dispose,
240
+ shutdown,
241
+ get isShuttingDown() {
242
+ return shuttingDown;
243
+ },
244
+ };
104
245
  }
105
246
 
106
247
  module.exports = {
107
- setStatus: setStatus,
108
- getStatus: getStatus,
109
- route: route,
110
- shutdown: shutdown,
111
- enableTooBusy: enableTooBusy,
112
- gracefulShutdownKeepaliveConnections: gracefulShutdownKeepaliveConnections,
248
+ setStatus,
249
+ getStatus,
250
+ route,
251
+ registerShutdownHandlers,
113
252
  };
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "connect-ready",
3
- "version": "1.0.13",
4
- "description": "express route that indicates whether a service is ready or not",
3
+ "version": "2.0.0",
4
+ "description": "Kubernetes readiness and graceful shutdown for Node.js HTTP services",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "files": [
8
+ "index.js",
9
+ "index.d.ts"
10
+ ],
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
6
14
  "scripts": {
7
- "test": "mocha test",
8
- "test:cover": "c8 --reporter=lcov --reporter=text npm test",
9
- "lint": "eslint *.js"
15
+ "test": "node --test",
16
+ "test:cover": "node --experimental-test-coverage --test",
17
+ "lint": "node --check index.js && node --check test/index.test.js"
10
18
  },
11
19
  "repository": {
12
20
  "type": "git",
@@ -22,19 +30,5 @@
22
30
  "bugs": {
23
31
  "url": "https://github.com/dcolens/connect-ready/issues"
24
32
  },
25
- "homepage": "https://github.com/dcolens/connect-ready#readme",
26
- "devDependencies": {
27
- "c8": "^7.11.0",
28
- "coveralls": "^3.1.1",
29
- "eslint": "^8.9.0",
30
- "eslint-config-prettier": "^8.3.0",
31
- "eslint-plugin-prettier": "^4.0.0",
32
- "mocha": "^9.2.0",
33
- "prettier": "^2.5.1",
34
- "toobusy-js": "^0.5.1"
35
- },
36
- "optionalDependencies": {
37
- "toobusy-js": "^0.5.1"
38
- },
39
- "dependencies": {}
33
+ "homepage": "https://github.com/dcolens/connect-ready#readme"
40
34
  }
package/.editorconfig DELETED
@@ -1,14 +0,0 @@
1
- # EditorConfig is awesome: http://EditorConfig.org
2
-
3
- root = true
4
-
5
- [*.js]
6
- charset = utf-8
7
- end_of_line = lf
8
- indent_size = 1
9
- indent_style = tab
10
- insert_final_newline = true
11
- trim_trailing_whitespace = true
12
-
13
- [*.md]
14
- trim_trailing_whitespace = false
package/.eslintrc.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "extends": ["plugin:prettier/recommended"],
3
- "parserOptions": { "ecmaVersion": 2018 }
4
- }
@@ -1,41 +0,0 @@
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://help.github.com/actions/language-and-framework-guides/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
-
14
- strategy:
15
- matrix:
16
- node-version: [10.x, 12.x, 14.x, 15.x]
17
-
18
- steps:
19
- - uses: actions/checkout@v2
20
- - name: Use Node.js ${{ matrix.node-version }}
21
- uses: actions/setup-node@v1
22
- with:
23
- node-version: ${{ matrix.node-version }}
24
- - run: npm ci
25
- - run: npm run build --if-present
26
- - run: npm test
27
-
28
- publish-npm:
29
- needs: build
30
- runs-on: ubuntu-latest
31
- steps:
32
- - uses: actions/checkout@v2
33
- - uses: actions/setup-node@v1
34
- with:
35
- node-version: 14
36
- registry-url: https://registry.npmjs.org/
37
- - run: npm ci
38
- - run: npm publish
39
- env:
40
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
41
-