nango 0.72.1 → 0.72.3
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/dist/index.js +37 -23
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/zeroYaml/compile.unit.cli-test.js +9 -0
- package/dist/zeroYaml/compile.unit.cli-test.js.map +1 -1
- package/dist/zeroYaml/definitions.d.ts +8 -1
- package/dist/zeroYaml/definitions.js +12 -1
- package/dist/zeroYaml/definitions.js.map +1 -1
- package/dist/zeroYaml/deploy.d.ts +8 -2
- package/dist/zeroYaml/deploy.js +277 -263
- package/dist/zeroYaml/deploy.js.map +1 -1
- package/dist/zeroYaml/deploy.unit.cli-test.js +747 -101
- package/dist/zeroYaml/deploy.unit.cli-test.js.map +1 -1
- package/dist/zeroYaml/project-path.d.ts +8 -0
- package/dist/zeroYaml/project-path.js +16 -0
- package/dist/zeroYaml/project-path.js.map +1 -1
- package/dist/zeroYaml/project-path.unit.cli-test.js +35 -1
- package/dist/zeroYaml/project-path.unit.cli-test.js.map +1 -1
- package/package.json +6 -6
- package/dist/services/clone.service.d.ts +0 -13
- package/dist/services/clone.service.js +0 -184
- package/dist/services/clone.service.js.map +0 -1
- package/dist/utils/githubTemplates.d.ts +0 -69
- package/dist/utils/githubTemplates.js +0 -266
- package/dist/utils/githubTemplates.js.map +0 -1
package/dist/zeroYaml/deploy.js
CHANGED
|
@@ -3,19 +3,33 @@ import path from 'node:path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import columnify from 'columnify';
|
|
5
5
|
import promptly from 'promptly';
|
|
6
|
+
import * as z from 'zod';
|
|
6
7
|
import { cliFetch } from '../tls.js';
|
|
7
|
-
import { getCliHeaders,
|
|
8
|
+
import { getCliHeaders, parseSecretKey, printDebug, resolveHostport } from '../utils.js';
|
|
8
9
|
import { Err, Ok } from '../utils/result.js';
|
|
9
10
|
import { Spinner } from '../utils/spinner.js';
|
|
10
11
|
import { NANGO_VERSION } from '../version.js';
|
|
11
12
|
import { tsToJsPath } from './compile.js';
|
|
12
13
|
import { parseIntegrationDefinitions } from './definitions.js';
|
|
13
|
-
import { resolveProjectPath } from './project-path.js';
|
|
14
|
+
import { resolveEntryPoint, resolveProjectPath } from './project-path.js';
|
|
14
15
|
import { ReadableError } from './utils.js';
|
|
15
|
-
export async function deploy({ fullPath, options, environmentName, interactive
|
|
16
|
+
export async function deploy({ fullPath, options, environmentName, file, interactive }) {
|
|
16
17
|
const { env, version, debug } = options;
|
|
17
18
|
const spinnerFactory = new Spinner({ interactive });
|
|
18
|
-
|
|
19
|
+
if (options.sync && options.action) {
|
|
20
|
+
console.log(chalk.red('The --sync and --action options cannot be used together.'));
|
|
21
|
+
return Err('invalid_options');
|
|
22
|
+
}
|
|
23
|
+
const validArguments = validateDeployArguments({ environment: environmentName, file, options });
|
|
24
|
+
if (validArguments.isErr()) {
|
|
25
|
+
console.log(chalk.red(validArguments.error.message));
|
|
26
|
+
return Err(validArguments.error);
|
|
27
|
+
}
|
|
28
|
+
if (options.sync || options.action) {
|
|
29
|
+
console.log(chalk.yellow('--sync and --action are deprecated. Use `nango deploy <environment> <file>` to deploy a single script.'));
|
|
30
|
+
}
|
|
31
|
+
const deploySource = resolveDeploySource();
|
|
32
|
+
const plan = [];
|
|
19
33
|
const spinnerPackage = spinnerFactory.start('Packaging');
|
|
20
34
|
try {
|
|
21
35
|
const def = await parseIntegrationDefinitions({ fullPath, debug });
|
|
@@ -25,47 +39,46 @@ export async function deploy({ fullPath, options, environmentName, interactive =
|
|
|
25
39
|
console.log(def.error instanceof ReadableError ? def.error.toText() : chalk.red(def.error.message));
|
|
26
40
|
return Err(def.error);
|
|
27
41
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
optionalIntegrationId: options.integration,
|
|
31
|
-
optionalSyncName: options.sync,
|
|
32
|
-
optionalActionName: options.action
|
|
33
|
-
});
|
|
34
|
-
if (deploymentKind === 'mixed') {
|
|
42
|
+
const selections = file !== undefined ? planFileDeployment({ fullPath, file, parsed: def.value }).map((selection) => [selection]) : Ok(planDeployment({ options }));
|
|
43
|
+
if (selections.isErr()) {
|
|
35
44
|
spinnerPackage.fail();
|
|
36
|
-
console.log(chalk.red(
|
|
45
|
+
console.log(chalk.red(selections.error.message));
|
|
37
46
|
return Err('no_data');
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
spinnerPackage.fail();
|
|
59
|
-
console.log(chalk.red('The --version option can only be used with legacy scripts.'));
|
|
60
|
-
return Err('no_data');
|
|
48
|
+
for (const selection of selections.value) {
|
|
49
|
+
if (selection.kind === 'legacy') {
|
|
50
|
+
const postData = await createLegacyPackage({
|
|
51
|
+
parsed: def.value,
|
|
52
|
+
fullPath,
|
|
53
|
+
debug,
|
|
54
|
+
version,
|
|
55
|
+
reconciliationScope: selection.integrationId ? { kind: 'integration', integrationId: selection.integrationId } : { kind: 'environment' },
|
|
56
|
+
optionalSyncName: selection.syncName,
|
|
57
|
+
optionalActionName: selection.actionName
|
|
58
|
+
});
|
|
59
|
+
if (postData.isErr()) {
|
|
60
|
+
spinnerPackage.fail();
|
|
61
|
+
console.log(chalk.red(postData.error.message));
|
|
62
|
+
return Err('no_data');
|
|
63
|
+
}
|
|
64
|
+
for (const legacyPackage of postData.value) {
|
|
65
|
+
plan.push({ kind: 'legacy', package: legacyPackage });
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
else {
|
|
69
|
+
const bundle = await createFunctionsBundle({
|
|
70
|
+
fullPath,
|
|
71
|
+
functionConfigs: def.value.functions,
|
|
72
|
+
reconciliationScope: selection.reconciliationScope,
|
|
73
|
+
source: deploySource
|
|
74
|
+
});
|
|
75
|
+
if (bundle.isErr()) {
|
|
76
|
+
spinnerPackage.fail();
|
|
77
|
+
console.log(chalk.red(bundle.error.message));
|
|
78
|
+
return Err('no_data');
|
|
79
|
+
}
|
|
80
|
+
plan.push({ kind: 'functions', bundle: bundle.value });
|
|
67
81
|
}
|
|
68
|
-
deployment = { kind: 'functions', bundle: bundle.value };
|
|
69
82
|
}
|
|
70
83
|
spinnerPackage.succeed();
|
|
71
84
|
}
|
|
@@ -74,6 +87,9 @@ export async function deploy({ fullPath, options, environmentName, interactive =
|
|
|
74
87
|
console.error(chalk.red('Unknown error'), err);
|
|
75
88
|
return Err('failed');
|
|
76
89
|
}
|
|
90
|
+
if (version && plan.some((deployment) => deployment.kind === 'functions' && deployment.bundle.functions.length > 0)) {
|
|
91
|
+
console.log(chalk.yellow('The --version option only applies to syncs/actions; it has no effect on functions.'));
|
|
92
|
+
}
|
|
77
93
|
// Get the private key before reaching the API
|
|
78
94
|
await parseSecretKey(environmentName, debug);
|
|
79
95
|
const nangoYamlBody = '';
|
|
@@ -81,28 +97,16 @@ export async function deploy({ fullPath, options, environmentName, interactive =
|
|
|
81
97
|
const hostport = resolveHostport(env);
|
|
82
98
|
// Check remote state
|
|
83
99
|
const spinnerState = spinnerFactory.start(`Acquiring remote state ${chalk.gray(`(${new URL(hostport).origin})`)}`);
|
|
84
|
-
|
|
100
|
+
const confirmations = [];
|
|
85
101
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
hostport,
|
|
89
|
-
body: { ...deployment.package, reconcile: false, debug, sdkVersion }
|
|
90
|
-
});
|
|
102
|
+
const confirmationResults = await Promise.all(plan.map((deployment) => previewDeployment({ deployment, hostport, debug, sdkVersion })));
|
|
103
|
+
for (const confirmationRes of confirmationResults) {
|
|
91
104
|
if (confirmationRes.isErr()) {
|
|
92
105
|
spinnerState.fail();
|
|
93
106
|
console.log(chalk.red(confirmationRes.error.message));
|
|
94
107
|
return Err(confirmationRes.error);
|
|
95
108
|
}
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
const confirmationRes = await previewFunctionsDeployment({ hostport, body: deployment.bundle });
|
|
100
|
-
if (confirmationRes.isErr()) {
|
|
101
|
-
spinnerState.fail();
|
|
102
|
-
console.log(chalk.red(confirmationRes.error.message));
|
|
103
|
-
return Err(confirmationRes.error);
|
|
104
|
-
}
|
|
105
|
-
confirmation = { kind: 'functions', value: confirmationRes.value };
|
|
109
|
+
confirmations.push(confirmationRes.value);
|
|
106
110
|
}
|
|
107
111
|
spinnerState.succeed();
|
|
108
112
|
}
|
|
@@ -110,90 +114,157 @@ export async function deploy({ fullPath, options, environmentName, interactive =
|
|
|
110
114
|
spinnerState.fail();
|
|
111
115
|
return Err('failed');
|
|
112
116
|
}
|
|
113
|
-
const deploySource = resolveDeploySource();
|
|
114
117
|
const autoconfirm = process.env['NANGO_DEPLOY_AUTO_CONFIRM'] === 'true' || options.autoConfirm;
|
|
115
|
-
const confirmed =
|
|
116
|
-
|
|
117
|
-
:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
});
|
|
118
|
+
const confirmed = await handleConfirmation({
|
|
119
|
+
autoconfirm,
|
|
120
|
+
allowDestructive: options.allowDestructive || false,
|
|
121
|
+
confirmations,
|
|
122
|
+
interactive
|
|
123
|
+
});
|
|
122
124
|
if (confirmed.isErr()) {
|
|
123
125
|
return Err('not_confirmed');
|
|
124
126
|
}
|
|
125
127
|
console.log('');
|
|
126
128
|
// Actual deploy
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
spinnerDeploy.succeed('Deployed');
|
|
143
|
-
console.log(chalk.green(deployRes.value));
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
const deployRes = await deployFunctions({ hostport, body: deployment.bundle });
|
|
147
|
-
if (deployRes.isErr()) {
|
|
148
|
-
spinnerDeploy.fail();
|
|
149
|
-
console.log(chalk.red(deployRes.error.message));
|
|
150
|
-
return Err('failed_to_deploy');
|
|
129
|
+
const pendingConfirmations = confirmations.filter((confirmation) => !isNoop(confirmation));
|
|
130
|
+
const total = pendingConfirmations.reduce((count, { deployment }) => count + countFunctions(deployment), 0);
|
|
131
|
+
const spinnerDeploy = spinnerFactory.start(total > 0 ? `Deploying ${total} functions` : 'Deploying');
|
|
132
|
+
const results = [];
|
|
133
|
+
// Deployments are intentionally not atomic: completed ones are not rolled back on failure.
|
|
134
|
+
for (const [failedIndex, confirmation] of pendingConfirmations.entries()) {
|
|
135
|
+
const deployRes = await executeDeployment({ confirmation, hostport, debug, nangoYamlBody, sdkVersion, source: deploySource });
|
|
136
|
+
if (deployRes.isErr()) {
|
|
137
|
+
spinnerDeploy.fail();
|
|
138
|
+
printDeploymentMessage(results);
|
|
139
|
+
console.log(chalk.red(deployRes.error.message));
|
|
140
|
+
console.log(chalk.red(failedIndex > 0 ? 'Deployment partially completed.' : 'Deployment failed.'));
|
|
141
|
+
for (const [index, { kind }] of pendingConfirmations.entries()) {
|
|
142
|
+
const status = index < failedIndex ? 'completed' : index === failedIndex ? 'failed' : 'not attempted';
|
|
143
|
+
console.log(`- ${kind === 'legacy' ? 'Syncs/actions/on-events' : 'Functions'}: ${status}`);
|
|
151
144
|
}
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
console.log(chalk.yellow('The failed request may have applied changes. Check remote state before retrying.'));
|
|
146
|
+
return Err('failed_to_deploy');
|
|
154
147
|
}
|
|
155
|
-
|
|
148
|
+
results.push(deployRes.value);
|
|
156
149
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
150
|
+
spinnerDeploy.succeed('Deployed');
|
|
151
|
+
if (results.every((result) => result.deployed.length + result.deleted.length === 0)) {
|
|
152
|
+
console.log(chalk.gray('No changes were deployed.'));
|
|
160
153
|
}
|
|
154
|
+
else {
|
|
155
|
+
printDeploymentMessage(results);
|
|
156
|
+
}
|
|
157
|
+
return Ok(true);
|
|
161
158
|
}
|
|
162
|
-
function
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
: parsed.integrations;
|
|
169
|
-
const hasLegacyScripts = integrations.some((integration) => integration.syncs.length > 0 || integration.actions.length > 0 || Object.values(integration.onEventScripts).some((scripts) => scripts.length > 0));
|
|
170
|
-
const hasFunctions = parsed.functions.some((config) => !optionalIntegrationId || config.integrationId === optionalIntegrationId);
|
|
171
|
-
if (hasLegacyScripts && hasFunctions) {
|
|
172
|
-
return 'mixed';
|
|
173
|
-
}
|
|
174
|
-
if (hasLegacyScripts) {
|
|
175
|
-
return 'legacy';
|
|
176
|
-
}
|
|
177
|
-
return 'functions';
|
|
159
|
+
function countFunctions(deployment) {
|
|
160
|
+
if (deployment.kind === 'legacy') {
|
|
161
|
+
const onEventScriptsCount = deployment.package.onEventScriptsByProvider?.reduce((count, { scripts }) => count + scripts.length, 0) || 0;
|
|
162
|
+
return deployment.package.flowConfigs.length + onEventScriptsCount;
|
|
163
|
+
}
|
|
164
|
+
return deployment.bundle.functions.length;
|
|
178
165
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
166
|
+
function isNoop(confirmation) {
|
|
167
|
+
if (countFunctions(confirmation.deployment) > 0) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (confirmation.kind === 'legacy') {
|
|
171
|
+
const { deletedSyncs, deletedActions, deletedOnEventScripts } = confirmation.value;
|
|
172
|
+
return deletedSyncs.length + deletedActions.length + deletedOnEventScripts.length === 0;
|
|
173
|
+
}
|
|
174
|
+
return confirmation.value.deleted.length === 0;
|
|
175
|
+
}
|
|
176
|
+
async function previewDeployment({ deployment, hostport, debug, sdkVersion }) {
|
|
177
|
+
if (deployment.kind === 'legacy') {
|
|
178
|
+
const result = await postLegacyConfirmation({ hostport, body: { ...deployment.package, reconcile: false, debug, sdkVersion } });
|
|
179
|
+
return result.map((value) => ({ kind: 'legacy', value, deployment }));
|
|
180
|
+
}
|
|
181
|
+
const result = await previewFunctionsDeployment({ hostport, body: deployment.bundle });
|
|
182
|
+
return result.map((value) => ({ kind: 'functions', value, deployment }));
|
|
183
|
+
}
|
|
184
|
+
async function executeDeployment({ confirmation, hostport, debug, nangoYamlBody, sdkVersion, source }) {
|
|
185
|
+
try {
|
|
186
|
+
if (confirmation.kind === 'legacy') {
|
|
187
|
+
const { deployment, value } = confirmation;
|
|
188
|
+
const result = await postLegacyDeploy({ hostport, body: { ...deployment.package, reconcile: true, debug, nangoYamlBody, sdkVersion, source } });
|
|
189
|
+
return result.map((deployed) => ({
|
|
190
|
+
deployed: deployed.map(({ providerConfigKey, name, version }) => ({ integrationId: providerConfigKey, name, version })),
|
|
191
|
+
// The legacy API only returns deployed entries.
|
|
192
|
+
// Getting deleted entries from the confirmation response instead.
|
|
193
|
+
deleted: [...value.deletedSyncs, ...value.deletedActions, ...value.deletedOnEventScripts].map(({ providerConfigKey, name }) => ({
|
|
194
|
+
integrationId: providerConfigKey,
|
|
195
|
+
name
|
|
196
|
+
}))
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
const result = await deployFunctions({ hostport, body: confirmation.deployment.bundle });
|
|
200
|
+
return result.map((value) => ({ deployed: [...value.created, ...value.updated], deleted: value.deleted }));
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
return Err(new Error(`Unexpected deploy response:\n${err instanceof Error ? err.message : String(err)}`));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function planDeployment({ options }) {
|
|
207
|
+
const { integration: integrationId, sync: syncName, action: actionName } = options;
|
|
208
|
+
if (syncName || actionName) {
|
|
209
|
+
return [{ kind: 'legacy', integrationId, syncName, actionName }];
|
|
210
|
+
}
|
|
211
|
+
// Reconcile both families even when their last local script was removed.
|
|
212
|
+
return [
|
|
213
|
+
{ kind: 'legacy', integrationId },
|
|
214
|
+
{ kind: 'functions', reconciliationScope: integrationId ? { kind: 'integration', integrationId } : { kind: 'environment' } }
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
const environmentNameSchema = z
|
|
218
|
+
.string()
|
|
219
|
+
.regex(/^[a-z0-9_-]+$/)
|
|
220
|
+
.max(255);
|
|
221
|
+
export function validateDeployArguments({ environment, file, options }) {
|
|
222
|
+
if (!environmentNameSchema.safeParse(environment).success) {
|
|
223
|
+
return Err(new Error(`'${environment}' is not a valid environment name. Usage: nango deploy <environment> [file]`));
|
|
224
|
+
}
|
|
225
|
+
if (file === undefined) {
|
|
226
|
+
return Ok(undefined);
|
|
227
|
+
}
|
|
228
|
+
if (file.trim() === '') {
|
|
229
|
+
return Err(new Error('The file argument is empty.'));
|
|
230
|
+
}
|
|
231
|
+
if (options.sync || options.action || options.integration) {
|
|
232
|
+
return Err(new Error('A file cannot be combined with --sync, --action or --integration.'));
|
|
233
|
+
}
|
|
234
|
+
return Ok(undefined);
|
|
235
|
+
}
|
|
236
|
+
function planFileDeployment({ fullPath, file, parsed }) {
|
|
237
|
+
const resolved = resolveEntryPoint({ fullPath, file, entryPoints: parsed.entryPoints });
|
|
238
|
+
if (resolved.isErr()) {
|
|
239
|
+
return Err(resolved.error);
|
|
240
|
+
}
|
|
241
|
+
const entryPoint = resolved.value;
|
|
242
|
+
const { integrationId, name } = entryPoint;
|
|
243
|
+
switch (entryPoint.type) {
|
|
244
|
+
case 'function':
|
|
245
|
+
return Ok({ kind: 'functions', reconciliationScope: { kind: 'function', integrationId, name } });
|
|
246
|
+
case 'sync':
|
|
247
|
+
return Ok({ kind: 'legacy', integrationId, syncName: name });
|
|
248
|
+
case 'action':
|
|
249
|
+
return Ok({ kind: 'legacy', integrationId, actionName: name });
|
|
250
|
+
case 'on-event':
|
|
251
|
+
return Err(new Error(`'${file}' is an on-event script and can't be deployed on its own. Use \`nango deploy <environment> --integration ${integrationId}\` or deploy the whole project.`));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function createFunctionsBundle({ fullPath, functionConfigs, reconciliationScope, source }) {
|
|
255
|
+
const selectedConfigs = functionConfigs.filter((config) => {
|
|
256
|
+
switch (reconciliationScope.kind) {
|
|
257
|
+
case 'environment':
|
|
258
|
+
return true;
|
|
259
|
+
case 'function':
|
|
260
|
+
return config.integrationId === reconciliationScope.integrationId && config.name === reconciliationScope.name;
|
|
261
|
+
case 'integration':
|
|
262
|
+
return config.integrationId === reconciliationScope.integrationId;
|
|
194
263
|
}
|
|
264
|
+
});
|
|
265
|
+
if (reconciliationScope.kind === 'function' && selectedConfigs.length === 0) {
|
|
266
|
+
return Err(new Error(`No compiled function found for "${reconciliationScope.name}"`));
|
|
195
267
|
}
|
|
196
|
-
const selectedConfigs = optionalIntegrationId ? functionConfigs.filter((config) => config.integrationId === optionalIntegrationId) : functionConfigs;
|
|
197
268
|
const functions = [];
|
|
198
269
|
for (const config of selectedConfigs) {
|
|
199
270
|
const { filePath, ...artifact } = config;
|
|
@@ -211,27 +282,24 @@ async function createFunctionsBundle({ fullPath, optionalIntegrationId }) {
|
|
|
211
282
|
if (!fileBody) {
|
|
212
283
|
return Err(new Error(`No function files found for "${config.name}"`));
|
|
213
284
|
}
|
|
214
|
-
functions.push({ ...artifact, fileBody });
|
|
285
|
+
functions.push({ ...artifact, fileBody, source });
|
|
215
286
|
}
|
|
216
|
-
return Ok({
|
|
217
|
-
reconciliationScope: optionalIntegrationId ? { kind: 'integration', integrationId: optionalIntegrationId } : { kind: 'environment' },
|
|
218
|
-
functions
|
|
219
|
-
});
|
|
287
|
+
return Ok({ reconciliationScope, functions });
|
|
220
288
|
}
|
|
221
289
|
/**
|
|
222
290
|
* Maps NangoYamlParsed (which is a list of integrations and its function definitions) into the shape expected by the API,
|
|
223
291
|
* while also loading the content of the related script files.
|
|
224
292
|
* It also supports filtering by integration, sync or action name for single deploys.
|
|
225
293
|
*/
|
|
226
|
-
async function
|
|
294
|
+
async function createLegacyPackage({ parsed, fullPath, debug, version = '', reconciliationScope, optionalSyncName, optionalActionName }) {
|
|
227
295
|
printDebug('Packaging', debug);
|
|
296
|
+
const isSingleScript = Boolean(optionalSyncName || optionalActionName);
|
|
228
297
|
const postData = [];
|
|
229
|
-
const onEventScriptsByProvider =
|
|
230
|
-
const
|
|
231
|
-
const deployMode = hasSingleScript ? 'single' : optionalIntegrationId ? 'integration' : 'all';
|
|
298
|
+
const onEventScriptsByProvider = isSingleScript ? undefined : []; // only load on-event scripts if we're not deploying a single sync or action
|
|
299
|
+
const integrationId = reconciliationScope.kind === 'integration' ? reconciliationScope.integrationId : undefined;
|
|
232
300
|
for (const integration of parsed.integrations) {
|
|
233
301
|
const { providerConfigKey, onEventScripts } = integration;
|
|
234
|
-
if (
|
|
302
|
+
if (integrationId && integration.providerConfigKey !== integrationId) {
|
|
235
303
|
continue;
|
|
236
304
|
}
|
|
237
305
|
if (onEventScriptsByProvider) {
|
|
@@ -322,14 +390,16 @@ async function createDeployConfirmationPackage({ parsed, fullPath, debug, versio
|
|
|
322
390
|
}
|
|
323
391
|
}
|
|
324
392
|
}
|
|
325
|
-
if (postData.length
|
|
393
|
+
if (isSingleScript && postData.length === 0) {
|
|
326
394
|
return Err(new Error('No syncs or actions to deploy'));
|
|
327
395
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
396
|
+
if (isSingleScript) {
|
|
397
|
+
return Ok(postData.map((flow) => ({
|
|
398
|
+
flowConfigs: [flow],
|
|
399
|
+
reconciliationScope: { kind: 'function', integrationId: flow.providerConfigKey, name: flow.syncName }
|
|
400
|
+
})));
|
|
401
|
+
}
|
|
402
|
+
return Ok([{ flowConfigs: postData, onEventScriptsByProvider, reconciliationScope }]);
|
|
333
403
|
}
|
|
334
404
|
/**
|
|
335
405
|
* Load source and bundled files
|
|
@@ -433,19 +503,11 @@ async function postLegacyDeploy({ hostport, body }) {
|
|
|
433
503
|
if ('error' in json) {
|
|
434
504
|
return Err(new Error(`Error deploying:\n${json.error.message} ${chalk.gray(`(${json.error.code})`)}`));
|
|
435
505
|
}
|
|
436
|
-
|
|
437
|
-
return Ok(`Successfully removed the syncs/actions.`);
|
|
438
|
-
}
|
|
439
|
-
const nameAndVersions = json.map((result) => `${result.name}@v${result.version}`);
|
|
440
|
-
return Ok(`Successfully deployed the functions: \r\n${nameAndVersions
|
|
441
|
-
.map((row) => {
|
|
442
|
-
return `- ${row}`;
|
|
443
|
-
})
|
|
444
|
-
.join('\r\n')}`);
|
|
506
|
+
return Ok(json);
|
|
445
507
|
}
|
|
446
508
|
catch (err) {
|
|
447
509
|
const errorMessage = getFetchError(err);
|
|
448
|
-
return Err(new Error(`Error deploying
|
|
510
|
+
return Err(new Error(`Error deploying:\n${errorMessage}`));
|
|
449
511
|
}
|
|
450
512
|
}
|
|
451
513
|
function formatFunctionsDeploymentError(error) {
|
|
@@ -499,21 +561,32 @@ async function deployFunctions({ hostport, body }) {
|
|
|
499
561
|
return Err(new Error(`Error deploying functions:\n${getFetchError(err)}`));
|
|
500
562
|
}
|
|
501
563
|
}
|
|
502
|
-
function
|
|
503
|
-
const deployed =
|
|
504
|
-
|
|
505
|
-
|
|
564
|
+
function printDeploymentMessage(results) {
|
|
565
|
+
const deployed = results.flatMap((result) => result.deployed);
|
|
566
|
+
const deleted = results.flatMap((result) => result.deleted);
|
|
567
|
+
const messages = [];
|
|
568
|
+
if (deployed.length > 0) {
|
|
569
|
+
messages.push(`Successfully deployed: \r\n${deployed.map((func) => `- ${func.integrationId} → ${func.name}${func.version ? `@v${func.version}` : ''}`).join('\r\n')}`);
|
|
570
|
+
}
|
|
571
|
+
if (deleted.length > 0) {
|
|
572
|
+
messages.push(`Successfully removed: \r\n${deleted.map((func) => `- ${func.integrationId} → ${func.name}`).join('\r\n')}`);
|
|
573
|
+
}
|
|
574
|
+
if (messages.length > 0) {
|
|
575
|
+
console.log(chalk.green(messages.join('\r\n')));
|
|
506
576
|
}
|
|
507
|
-
return `Successfully deployed the functions: \r\n${deployed.map((func) => `- ${func.integrationId} → ${func.name}`).join('\r\n')}`;
|
|
508
577
|
}
|
|
509
|
-
|
|
510
|
-
* Handle state, display plan and eventually ask for confirmation if destructive
|
|
511
|
-
*/
|
|
512
|
-
async function handleLegacyConfirmation({ autoconfirm, allowDestructive, confirmation }) {
|
|
578
|
+
function renderLegacyConfirmation(confirmation) {
|
|
513
579
|
const { newSyncs, updatedSyncs, deletedSyncs, newActions, updatedActions, deletedActions, newOnEventScripts, updatedOnEventScripts, deletedOnEventScripts, deletedModels } = confirmation;
|
|
580
|
+
const scriptSummaries = [
|
|
581
|
+
{ name: 'Actions', newItems: newActions, updatedItems: updatedActions, deleteItems: deletedActions },
|
|
582
|
+
{ name: 'OnEvents', newItems: newOnEventScripts, updatedItems: updatedOnEventScripts, deleteItems: deletedOnEventScripts }
|
|
583
|
+
];
|
|
584
|
+
const summaries = [{ name: 'Syncs', newItems: newSyncs, updatedItems: updatedSyncs, deleteItems: deletedSyncs }, ...scriptSummaries];
|
|
585
|
+
if (deletedModels.length === 0 &&
|
|
586
|
+
summaries.every(({ newItems, updatedItems, deleteItems }) => newItems.length + updatedItems.length + deleteItems.length === 0)) {
|
|
587
|
+
return { rows: [], requiresConfirmation: false };
|
|
588
|
+
}
|
|
514
589
|
let deletedSyncsConnectionsCount = 0;
|
|
515
|
-
console.log('');
|
|
516
|
-
console.log('', chalk.underline('Nango will perform this plan:'));
|
|
517
590
|
// Syncs
|
|
518
591
|
if (newSyncs.length > 0 || deletedSyncs.length > 0) {
|
|
519
592
|
console.log('');
|
|
@@ -544,101 +617,32 @@ async function handleLegacyConfirmation({ autoconfirm, allowDestructive, confirm
|
|
|
544
617
|
});
|
|
545
618
|
console.log(columns);
|
|
546
619
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
console.log(shortSummaryMessage({ name: 'Actions', newItems: newActions, deleteItems: deletedActions }));
|
|
551
|
-
for (const action of newActions) {
|
|
552
|
-
console.log(` ${chalk.green('+')} ${action.providerConfigKey} → ${action.name}`);
|
|
553
|
-
}
|
|
554
|
-
for (const action of deletedActions) {
|
|
555
|
-
console.log(` ${chalk.red('-')} ${action.providerConfigKey} → ${action.name}`);
|
|
620
|
+
for (const { name, newItems, deleteItems } of scriptSummaries) {
|
|
621
|
+
if (newItems.length === 0 && deleteItems.length === 0) {
|
|
622
|
+
continue;
|
|
556
623
|
}
|
|
557
|
-
}
|
|
558
|
-
// OnEvents
|
|
559
|
-
if (newOnEventScripts.length > 0 || deletedOnEventScripts.length > 0) {
|
|
560
624
|
console.log('');
|
|
561
|
-
console.log(shortSummaryMessage({ name
|
|
562
|
-
for (const
|
|
563
|
-
console.log(` ${chalk.green('+')} ${
|
|
625
|
+
console.log(shortSummaryMessage({ name, newItems, deleteItems }));
|
|
626
|
+
for (const item of newItems) {
|
|
627
|
+
console.log(` ${chalk.green('+')} ${item.providerConfigKey} → ${item.name}`);
|
|
564
628
|
}
|
|
565
|
-
for (const
|
|
566
|
-
console.log(` ${chalk.red('-')} ${
|
|
629
|
+
for (const item of deleteItems) {
|
|
630
|
+
console.log(` ${chalk.red('-')} ${item.providerConfigKey} → ${item.name}`);
|
|
567
631
|
}
|
|
568
632
|
}
|
|
569
633
|
if (deletedModels.length > 0) {
|
|
570
634
|
console.log(chalk.red(`The following models have been removed: ${deletedModels.join(', ')}. `));
|
|
571
635
|
console.log(chalk.red("WARNING: Renaming a model is the equivalent of deleting the old model and creating a new one. Records from the old model won't be transferred to the new model. Consider running a full sync to transfer records."));
|
|
572
636
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
newOnEventScripts.length <= 0 &&
|
|
578
|
-
deletedOnEventScripts.length <= 0 &&
|
|
579
|
-
deletedModels.length <= 0) {
|
|
580
|
-
console.log('');
|
|
581
|
-
console.log(chalk.gray.italic(' Only updates'));
|
|
582
|
-
}
|
|
583
|
-
console.log('');
|
|
584
|
-
console.log('', chalk.underline('Summary'));
|
|
585
|
-
const columns = columnify([
|
|
586
|
-
summaryMessageColumns({ name: 'Syncs', newItems: newSyncs, updatedItems: updatedSyncs, deleteItems: deletedSyncs }),
|
|
587
|
-
summaryMessageColumns({ name: 'Actions', newItems: newActions, updatedItems: updatedActions, deleteItems: deletedActions }),
|
|
588
|
-
summaryMessageColumns({ name: 'OnEvents', newItems: newOnEventScripts, updatedItems: updatedOnEventScripts, deleteItems: deletedOnEventScripts })
|
|
589
|
-
], {
|
|
590
|
-
showHeaders: false,
|
|
591
|
-
minWidth: 18,
|
|
592
|
-
config: {
|
|
593
|
-
name: {
|
|
594
|
-
dataTransform: (a) => {
|
|
595
|
-
return `\u2063\u2063${a}`;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
console.log(columns);
|
|
601
|
-
console.log('');
|
|
602
|
-
const shouldConfirmDestructive = deletedSyncsConnectionsCount > 0 || deletedModels.length > 0;
|
|
603
|
-
if (!shouldConfirmDestructive) {
|
|
604
|
-
console.log(chalk.grey('No sync deleted with active connections, proceeding without confirmation'));
|
|
605
|
-
return Ok(true);
|
|
606
|
-
}
|
|
607
|
-
if (autoconfirm && !shouldConfirmDestructive) {
|
|
608
|
-
console.log(chalk.yellow('autoconfirm flag is on, proceeding without confirmation'));
|
|
609
|
-
return Ok(true);
|
|
610
|
-
}
|
|
611
|
-
else if (autoconfirm && shouldConfirmDestructive && allowDestructive) {
|
|
612
|
-
console.log(chalk.yellow('allowDestructive flag is on, proceeding without confirmation'));
|
|
613
|
-
return Ok(true);
|
|
614
|
-
}
|
|
615
|
-
// Can't do anything here
|
|
616
|
-
if (isCI) {
|
|
617
|
-
console.log(chalk.red(`Syncs/Actions were not deployed. Confirm the deploy by passing the --auto-confirm flag${shouldConfirmDestructive ? ' and --allow-destructive flag' : ''}. Exiting`));
|
|
618
|
-
return Err('is_ci');
|
|
619
|
-
}
|
|
620
|
-
try {
|
|
621
|
-
const wait = await promptly.confirm(`Are you sure you want to continue y/n?`);
|
|
622
|
-
if (!wait) {
|
|
623
|
-
console.log(chalk.yellow('Deployed aborted. Exiting'));
|
|
624
|
-
return Err('not_confirmed');
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
catch {
|
|
628
|
-
console.log('');
|
|
629
|
-
console.log(chalk.yellow('Deployed aborted. Exiting'));
|
|
630
|
-
return Err('not_confirmed');
|
|
631
|
-
}
|
|
632
|
-
return Ok(true);
|
|
637
|
+
return {
|
|
638
|
+
rows: summaries.map(summaryMessageColumns),
|
|
639
|
+
requiresConfirmation: deletedSyncsConnectionsCount > 0 || deletedModels.length > 0
|
|
640
|
+
};
|
|
633
641
|
}
|
|
634
|
-
|
|
642
|
+
function renderFunctionsConfirmation(confirmation) {
|
|
635
643
|
const { created, updated, deleted } = confirmation;
|
|
636
|
-
console.log('');
|
|
637
|
-
console.log('', chalk.underline('Nango will perform this plan:'));
|
|
638
644
|
if (created.length + updated.length + deleted.length === 0) {
|
|
639
|
-
|
|
640
|
-
console.log(chalk.gray.italic(' No changes'));
|
|
641
|
-
return Ok(true);
|
|
645
|
+
return { rows: [], requiresConfirmation: false };
|
|
642
646
|
}
|
|
643
647
|
else if (created.length > 0 || deleted.length > 0) {
|
|
644
648
|
console.log('');
|
|
@@ -650,32 +654,42 @@ async function handleFunctionsConfirmation({ autoconfirm, allowDestructive, conf
|
|
|
650
654
|
console.log(` ${chalk.red('-')} ${func.integrationId} → ${func.name}`);
|
|
651
655
|
}
|
|
652
656
|
}
|
|
653
|
-
|
|
657
|
+
return {
|
|
658
|
+
rows: [summaryMessageColumns({ name: 'Functions', newItems: created, updatedItems: updated, deleteItems: deleted })],
|
|
659
|
+
requiresConfirmation: deleted.length > 0
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function handleConfirmation({ autoconfirm, allowDestructive, confirmations, interactive }) {
|
|
663
|
+
console.log('');
|
|
664
|
+
console.log('', chalk.underline('Nango will perform this plan:'));
|
|
665
|
+
const summaries = confirmations.map((confirmation) => confirmation.kind === 'legacy' ? renderLegacyConfirmation(confirmation.value) : renderFunctionsConfirmation(confirmation.value));
|
|
666
|
+
const rows = summaries.flatMap((summary) => summary.rows);
|
|
667
|
+
if (rows.length > 0) {
|
|
668
|
+
console.log('');
|
|
669
|
+
console.log('', chalk.underline('Summary'));
|
|
670
|
+
console.log(columnify(rows, {
|
|
671
|
+
showHeaders: false,
|
|
672
|
+
minWidth: 18,
|
|
673
|
+
config: { name: { dataTransform: (value) => `\u2063\u2063${value}` } }
|
|
674
|
+
}));
|
|
675
|
+
console.log('');
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
654
678
|
console.log('');
|
|
655
|
-
console.log(chalk.gray.italic('
|
|
679
|
+
console.log(chalk.gray.italic(' No changes'));
|
|
656
680
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
showHeaders: false,
|
|
661
|
-
minWidth: 18,
|
|
662
|
-
config: {
|
|
663
|
-
name: {
|
|
664
|
-
dataTransform: (value) => `\u2063\u2063${value}`
|
|
665
|
-
}
|
|
681
|
+
if (!summaries.some((summary) => summary.requiresConfirmation)) {
|
|
682
|
+
if (rows.length > 0) {
|
|
683
|
+
console.log(chalk.grey('No destructive changes, proceeding without confirmation'));
|
|
666
684
|
}
|
|
667
|
-
}));
|
|
668
|
-
console.log('');
|
|
669
|
-
if (deleted.length === 0) {
|
|
670
|
-
console.log(chalk.grey('No functions deleted, proceeding without confirmation'));
|
|
671
685
|
return Ok(true);
|
|
672
686
|
}
|
|
673
687
|
if (autoconfirm && allowDestructive) {
|
|
674
688
|
console.log(chalk.yellow('allowDestructive flag is on, proceeding without confirmation'));
|
|
675
689
|
return Ok(true);
|
|
676
690
|
}
|
|
677
|
-
if (
|
|
678
|
-
console.log(chalk.red('
|
|
691
|
+
if (!interactive) {
|
|
692
|
+
console.log(chalk.red('Nothing was deployed. Confirm the deploy by passing the --auto-confirm and --allow-destructive flags. Exiting'));
|
|
679
693
|
return Err('is_ci');
|
|
680
694
|
}
|
|
681
695
|
try {
|