@automattic/vip 4.0.3 → 4.0.6
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/AGENTS.md +2 -0
- package/dist/bin/vip-app-deploy-token-generate.js +102 -0
- package/dist/bin/vip-app-deploy-token.js +20 -0
- package/dist/bin/vip-app-deploy.js +7 -1
- package/dist/bin/vip-app.js +1 -1
- package/dist/bin/vip-dev-env-update.js +1 -5
- package/dist/commands/dev-env-import-sql.js +4 -1
- package/dist/commands/dev-env-sync-sql.js +3 -4
- package/dist/lib/analytics/clients/tracks.js +2 -2
- package/dist/lib/api/http.js +19 -14
- package/dist/lib/api.js +1 -2
- package/dist/lib/cli/command.js +37 -1
- package/dist/lib/cli/internal-bin-loader.js +2 -0
- package/dist/lib/client-file-uploader.js +110 -52
- package/dist/lib/custom-deploy/custom-deploy.js +37 -0
- package/dist/lib/dev-environment/dev-environment-core.js +5 -5
- package/dist/lib/dev-environment/dev-environment-lando.js +7 -3
- package/dist/lib/http/download-file.js +6 -1
- package/dist/lib/http/proxy-dispatcher.js +97 -0
- package/dist/lib/keychain/secure.js +4 -4
- package/dist/lib/keychain.js +1 -1
- package/dist/lib/media-import/status.js +6 -1
- package/dist/lib/retry.js +3 -2
- package/dist/lib/search-and-replace.js +3 -4
- package/dist/lib/validations/custom-deploy.js +60 -4
- package/dist/lib/validations/line-by-line.js +4 -9
- package/dist/lib/validations/sql.js +2 -2
- package/docs/COMMANDER-MIGRATION.md +1 -0
- package/docs/RELEASING.md +2 -0
- package/docs/TESTING.md +16 -1
- package/helpers/build-sea.js +2 -2
- package/helpers/release-smoke-test.js +869 -0
- package/npm-shrinkwrap.json +873 -1043
- package/package.json +23 -17
- package/test-utils/undici-mock.js +25 -0
package/AGENTS.md
CHANGED
|
@@ -96,3 +96,5 @@ Guide for future agents working on this codebase. Focus on traps, cross-cutting
|
|
|
96
96
|
- Respect exit semantics: uncaught exceptions routed to `exit.withError`; GraphQL errors call `process.exit(1)` unless explicitly disabled; 401 auth errors exit with guidance.
|
|
97
97
|
- Watch shared state: current implementation mutates a global; ensure new parser avoids cross-command bleeding when multiple command instances run in-process (tests/CLI wrappers).
|
|
98
98
|
- Login guardrails in `vip.js` let certain commands bypass auth; preserve equivalent shortcuts or add explicit non-interactive flags for CI.
|
|
99
|
+
- Normalize short-option equals syntax: rewrite `-x=value` to `-x value` before parsing for short options that expect values; track which short options take values to avoid breaking boolean flags that should remain flag-only.
|
|
100
|
+
- Apply tracked defaults after parsing: when an option has both a default and a custom parser function, apply the default only when `opts[optionName] === undefined` after the parser runs, so that explicit values and defaults maintain parity with the parser contract (see test `parses short options using equals syntax without prefixing values` in `__tests__/lib/cli/command.js`).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
exports.__esModule = true;
|
|
5
|
+
exports.appDeployTokenGenerateCmd = appDeployTokenGenerateCmd;
|
|
6
|
+
var _graphqlTag = _interopRequireDefault(require("graphql-tag"));
|
|
7
|
+
var _api = _interopRequireDefault(require("../lib/api"));
|
|
8
|
+
var _command = _interopRequireDefault(require("../lib/cli/command"));
|
|
9
|
+
var exit = _interopRequireWildcard(require("../lib/cli/exit"));
|
|
10
|
+
var _tracker = require("../lib/tracker");
|
|
11
|
+
var _utils = require("../lib/utils");
|
|
12
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
13
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
|
+
const usage = 'vip app deploy-token generate';
|
|
15
|
+
const examples = [{
|
|
16
|
+
usage: 'vip @example-app.develop app deploy-token generate',
|
|
17
|
+
description: 'Generate a custom deploy access token for the selected environment.'
|
|
18
|
+
}, {
|
|
19
|
+
usage: 'vip @example-app.develop app deploy-token generate --format=json',
|
|
20
|
+
description: 'Generate a custom deploy access token and print the result as JSON.'
|
|
21
|
+
}];
|
|
22
|
+
const appQuery = `
|
|
23
|
+
id,
|
|
24
|
+
name,
|
|
25
|
+
environments{
|
|
26
|
+
id
|
|
27
|
+
appId
|
|
28
|
+
type
|
|
29
|
+
name
|
|
30
|
+
uniqueLabel
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
const GENERATE_CUSTOM_DEPLOY_ACCESS_MUTATION = (0, _graphqlTag.default)`
|
|
34
|
+
mutation GenerateCustomDeployAccess($input: GenerateCustomDeployAccessInput) {
|
|
35
|
+
generateCustomDeployAccess(input: $input) {
|
|
36
|
+
token
|
|
37
|
+
expiresAt
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
async function appDeployTokenGenerateCmd(_arg, {
|
|
42
|
+
app,
|
|
43
|
+
env
|
|
44
|
+
}) {
|
|
45
|
+
const trackerFn = (0, _tracker.makeCommandTracker)('deploy_token_generate', {
|
|
46
|
+
app: app.id,
|
|
47
|
+
env: env.uniqueLabel
|
|
48
|
+
});
|
|
49
|
+
await trackerFn('execute');
|
|
50
|
+
const envId = env.id;
|
|
51
|
+
if (typeof envId !== 'number') {
|
|
52
|
+
await trackerFn('error', {
|
|
53
|
+
error: 'Missing environment id'
|
|
54
|
+
});
|
|
55
|
+
exit.withError('Failed to generate deploy token: environment id is missing.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const api = (0, _api.default)();
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await api.mutate({
|
|
62
|
+
mutation: GENERATE_CUSTOM_DEPLOY_ACCESS_MUTATION,
|
|
63
|
+
variables: {
|
|
64
|
+
input: {
|
|
65
|
+
environmentIds: [envId]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const message = (0, _utils.parseApiError)(err) ?? 'Unknown error';
|
|
71
|
+
await trackerFn('error', {
|
|
72
|
+
error: message
|
|
73
|
+
});
|
|
74
|
+
exit.withError(`Failed to generate deploy token: ${message}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const tokenPayload = response.data?.generateCustomDeployAccess;
|
|
78
|
+
if (!tokenPayload?.token || !tokenPayload.expiresAt) {
|
|
79
|
+
await trackerFn('error', {
|
|
80
|
+
error: 'Missing generateCustomDeployAccess payload'
|
|
81
|
+
});
|
|
82
|
+
exit.withError('Failed to generate deploy token: API returned an empty response.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await trackerFn('success');
|
|
86
|
+
return [{
|
|
87
|
+
appId: app.id,
|
|
88
|
+
appName: app.name,
|
|
89
|
+
envId,
|
|
90
|
+
env: env.uniqueLabel,
|
|
91
|
+
token: tokenPayload.token,
|
|
92
|
+
expiresAt: tokenPayload.expiresAt
|
|
93
|
+
}];
|
|
94
|
+
}
|
|
95
|
+
void (0, _command.default)({
|
|
96
|
+
appContext: true,
|
|
97
|
+
appQuery,
|
|
98
|
+
envContext: true,
|
|
99
|
+
format: true,
|
|
100
|
+
requiredArgs: 0,
|
|
101
|
+
usage
|
|
102
|
+
}).examples(examples).argv(process.argv, appDeployTokenGenerateCmd);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var _command = _interopRequireDefault(require("../lib/cli/command"));
|
|
5
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
const usage = 'vip app deploy-token';
|
|
7
|
+
const exampleUsage = 'vip @example-app.develop app deploy-token';
|
|
8
|
+
const examples = [{
|
|
9
|
+
usage: `${exampleUsage} generate`,
|
|
10
|
+
description: 'Generate a custom deploy access token for an environment that already has Custom Deployment enabled.'
|
|
11
|
+
}, {
|
|
12
|
+
usage: `${exampleUsage} generate --format=json | jq -r '.[0].token'`,
|
|
13
|
+
description: 'Generate a token and extract the token value for scripting workflows.'
|
|
14
|
+
}];
|
|
15
|
+
void (0, _command.default)({
|
|
16
|
+
requiredArgs: 0,
|
|
17
|
+
usage
|
|
18
|
+
}).command('generate', 'Generate a custom deploy access token for an environment.').examples(examples).argv(process.argv, () => {
|
|
19
|
+
process.exit(0);
|
|
20
|
+
});
|
|
@@ -79,6 +79,9 @@ async function appDeployCmd(arg = [], opts = {}) {
|
|
|
79
79
|
const track = _tracker.trackEventWithEnv.bind(null, appId, envId);
|
|
80
80
|
debug('Validating file...');
|
|
81
81
|
await (0, _customDeploy.validateFile)(appId, envId, fileMeta);
|
|
82
|
+
if (!opts.skipLargeFileValidation) {
|
|
83
|
+
await (0, _customDeploy.validateLargeArchiveFiles)(appId, envId, fileMeta);
|
|
84
|
+
}
|
|
82
85
|
await track('deploy_app_command_execute');
|
|
83
86
|
|
|
84
87
|
// Upload file as different name to avoid overwriting existing same named files
|
|
@@ -229,8 +232,11 @@ const examples = [
|
|
|
229
232
|
}, {
|
|
230
233
|
usage: 'WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.tar.gz --skip-confirmation',
|
|
231
234
|
description: 'Skip the confirmation prompt for the Custom Deployment of the archived file named "file.tar.gz" to the environment.'
|
|
235
|
+
}, {
|
|
236
|
+
usage: 'WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.zip --skip-large-file-validation',
|
|
237
|
+
description: 'Skip checking the deploy archive for repository archive files larger than 20 MB.'
|
|
232
238
|
}];
|
|
233
239
|
void (0, _command.default)({
|
|
234
240
|
requiredArgs: 1,
|
|
235
241
|
usage: baseUsage
|
|
236
|
-
}).command('validate', 'Validate the directory structure of an archived file.').examples(examples).option('message', 'Add a description of a deployment.').option('skip-confirmation', 'Skip the confirmation prompt.').option('force', 'Skip confirmation prompt (deprecated)').option('app', 'Target an application. Accepts a string value for the application name or an integer for the application ID.').option('env', 'Target an environment. Accepts a string value for the environment type.').argv(process.argv, appDeployCmd);
|
|
242
|
+
}).command('validate', 'Validate the directory structure of an archived file.').examples(examples).option('message', 'Add a description of a deployment.').option('skip-confirmation', 'Skip the confirmation prompt.').option('skip-large-file-validation', 'Skip checking for repository archive files over 20 MB.', false).option('force', 'Skip confirmation prompt (deprecated)').option('app', 'Target an application. Accepts a string value for the application name or an integer for the application ID.').option('env', 'Target an environment. Accepts a string value for the environment type.').argv(process.argv, appDeployCmd);
|
package/dist/bin/vip-app.js
CHANGED
|
@@ -11,7 +11,7 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
11
11
|
(0, _command.default)({
|
|
12
12
|
requiredArgs: 1,
|
|
13
13
|
format: true
|
|
14
|
-
}).example('vip app list', 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.').example('vip app example-app', 'Retrieve information about the application named "example-app" and its environments.').example('WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.zip', 'Deploy a local archived file named "file.zip" that contains application code to a VIP Platform environment that has Custom Deployment enabled.').command('list', 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.').command('deploy', 'Deploy an archived file of application code to an environment that has Custom Deployment enabled.').argv(process.argv, async arg => {
|
|
14
|
+
}).example('vip app list', 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.').example('vip app example-app', 'Retrieve information about the application named "example-app" and its environments.').example('WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.zip', 'Deploy a local archived file named "file.zip" that contains application code to a VIP Platform environment that has Custom Deployment enabled.').example('vip @example-app.develop app deploy-token generate --format=json', 'Generate a custom deploy token for the environment and print it in JSON format for use with tools like jq.').command('list', 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.').command('deploy', 'Deploy an archived file of application code to an environment that has Custom Deployment enabled.').command('deploy-token', 'Generate and manage custom deploy access tokens for environments that have Custom Deployment enabled.').argv(process.argv, async arg => {
|
|
15
15
|
await (0, _tracker.trackEvent)('app_command_execute');
|
|
16
16
|
let res;
|
|
17
17
|
try {
|
|
@@ -53,7 +53,6 @@ cmd.argv(process.argv, async (arg, opt) => {
|
|
|
53
53
|
...opt
|
|
54
54
|
};
|
|
55
55
|
const configurationFileOptions = await (0, _devEnvironmentConfigurationFile.getConfigurationFileOptions)();
|
|
56
|
-
const thereAreOptionsFromConfigFile = Object.keys(configurationFileOptions).length > 0;
|
|
57
56
|
const finalPreselectedOptions = (0, _devEnvironmentConfigurationFile.mergeConfigurationFileOptions)(preselectedOptions, configurationFileOptions);
|
|
58
57
|
|
|
59
58
|
/** @type {InstanceOptions} */
|
|
@@ -74,10 +73,7 @@ cmd.argv(process.argv, async (arg, opt) => {
|
|
|
74
73
|
cron: currentInstanceData.cron,
|
|
75
74
|
adminPassword: currentInstanceData.adminPassword
|
|
76
75
|
};
|
|
77
|
-
const
|
|
78
|
-
.filter(option => !['debug', 'help', 'slug'].includes(option)); // Filter out options that are not related to instance configuration
|
|
79
|
-
|
|
80
|
-
const suppressPrompts = providedOptions.length > 0 || thereAreOptionsFromConfigFile;
|
|
76
|
+
const suppressPrompts = !process.stdin.isTTY;
|
|
81
77
|
const instanceData = await (0, _devEnvironmentCli.promptForArguments)(finalPreselectedOptions, defaultOptions, suppressPrompts, false);
|
|
82
78
|
instanceData.siteSlug = slug;
|
|
83
79
|
await (0, _devEnvironmentCore.updateEnvironment)(lando, instanceData);
|
|
@@ -115,7 +115,10 @@ class DevEnvImportSQLCommand {
|
|
|
115
115
|
if (!dumpDetails.sourceDb) {
|
|
116
116
|
console.log(`${_chalk.default.yellow('!')} Unable to identify source DB. Skipping myloader source-db option`);
|
|
117
117
|
}
|
|
118
|
-
importArg = ['db-myloader',
|
|
118
|
+
importArg = ['db-myloader',
|
|
119
|
+
// Drop existing tables before restoring. Do not use the long form:
|
|
120
|
+
// `--overwrite-tables` is a silent no-op on myloader >= 0.20.
|
|
121
|
+
'-o', ...(dumpDetails.sourceDb ? [`--source-db=${dumpDetails.sourceDb}`] : []), `--threads=${threadCount}`, '--max-threads-for-schema-creation=10', '--max-threads-for-index-creation=10', '--skip-triggers', '--skip-post', '--optimize-keys', '--checksum=SKIP', '--metadata-refresh-interval=2000000', '--stream'].concat(this.options.quiet ? ['--verbose=0'] : ['--verbose=3']);
|
|
119
122
|
}
|
|
120
123
|
return importArg;
|
|
121
124
|
}
|
|
@@ -180,12 +180,11 @@ class DevEnvSyncSQLCommand {
|
|
|
180
180
|
const readStream = _fs.default.createReadStream(this.sqlFile);
|
|
181
181
|
const replacedStream = await (0, _vipSearchReplace.replace)(readStream, replacements);
|
|
182
182
|
const outputFile = `${this.tmpDir}/sql-export-sr.sql`;
|
|
183
|
-
const
|
|
183
|
+
const transforms = [];
|
|
184
184
|
if (this.getSqlDumpType() === _database.SqlDumpType.MYDUMPER) {
|
|
185
|
-
|
|
185
|
+
transforms.push((0, _database.fixMyDumperTransform)());
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
await (0, _promises.pipeline)(streams);
|
|
187
|
+
await (0, _promises.pipeline)(replacedStream, ...transforms, _fs.default.createWriteStream(outputFile));
|
|
189
188
|
_fs.default.renameSync(outputFile, this.sqlFile);
|
|
190
189
|
}
|
|
191
190
|
getSiteUrlsFromSDS() {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.default = void 0;
|
|
5
5
|
var _debug = _interopRequireDefault(require("debug"));
|
|
6
|
-
var _nodeFetch = _interopRequireDefault(require("node-fetch"));
|
|
7
6
|
var _querystring = _interopRequireDefault(require("querystring"));
|
|
7
|
+
var _undici = require("undici");
|
|
8
8
|
var _apiConfig = require("../../cli/apiConfig");
|
|
9
9
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
10
|
const debug = (0, _debug.default)('@automattic/vip:analytics:clients:tracks');
|
|
@@ -94,7 +94,7 @@ class Tracks {
|
|
|
94
94
|
'User-Agent': this.userAgent
|
|
95
95
|
};
|
|
96
96
|
debug('send()', body);
|
|
97
|
-
return (0,
|
|
97
|
+
return (0, _undici.fetch)(Tracks.ENDPOINT, {
|
|
98
98
|
method,
|
|
99
99
|
body,
|
|
100
100
|
headers
|
package/dist/lib/api/http.js
CHANGED
|
@@ -3,19 +3,25 @@
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.default = void 0;
|
|
5
5
|
var _debug = _interopRequireDefault(require("debug"));
|
|
6
|
-
var
|
|
6
|
+
var _undici = require("undici");
|
|
7
7
|
var _api = require("../../lib/api");
|
|
8
8
|
var _env = _interopRequireDefault(require("../../lib/env"));
|
|
9
|
-
var
|
|
9
|
+
var _proxyDispatcher = require("../../lib/http/proxy-dispatcher");
|
|
10
10
|
var _token = _interopRequireDefault(require("../../lib/token"));
|
|
11
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
12
|
const debug = (0, _debug.default)('@automattic/vip:http');
|
|
13
|
+
const isPlainObjectBody = value => {
|
|
14
|
+
if (!value || typeof value !== 'object') {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const prototype = Object.getPrototypeOf(value);
|
|
18
|
+
return prototype === Object.prototype || prototype === null;
|
|
19
|
+
};
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* Call the Public API with an arbitrary path (e.g. to connect to REST endpoints).
|
|
15
23
|
* This will include the token in an Authorization header so requests are "logged-in."
|
|
16
24
|
*
|
|
17
|
-
* This is simply a wrapper around node-fetch
|
|
18
|
-
*
|
|
19
25
|
* @param {string} path API path to pass to `fetch` -- will be prefixed by the API_HOST
|
|
20
26
|
* @param {Object} options options to pass to `fetch`
|
|
21
27
|
* @return {Promise} Return value of the `fetch` call
|
|
@@ -29,26 +35,25 @@ var _default = async (path, options = {}) => {
|
|
|
29
35
|
url = `${_api.API_HOST}${path}`;
|
|
30
36
|
}
|
|
31
37
|
const authToken = await _token.default.get();
|
|
32
|
-
const
|
|
38
|
+
const proxyDispatcher = (0, _proxyDispatcher.createProxyDispatcher)(url);
|
|
39
|
+
const shouldSerializeJsonBody = isPlainObjectBody(options.body);
|
|
33
40
|
debug('running fetch', url);
|
|
34
|
-
const headers = new Headers(
|
|
35
|
-
...options.headers
|
|
36
|
-
});
|
|
41
|
+
const headers = new _undici.Headers(options.headers);
|
|
37
42
|
if (!headers.has('Authorization')) {
|
|
38
43
|
headers.set('Authorization', `Bearer ${authToken.raw}`);
|
|
39
44
|
}
|
|
40
45
|
if (!headers.has('User-Agent')) {
|
|
41
46
|
headers.set('User-Agent', _env.default.userAgent);
|
|
42
47
|
}
|
|
43
|
-
if (!headers.has('Content-Type') && options.method !== 'GET') {
|
|
48
|
+
if (!headers.has('Content-Type') && options.method !== 'GET' && shouldSerializeJsonBody) {
|
|
44
49
|
headers.set('Content-Type', 'application/json');
|
|
45
50
|
}
|
|
46
|
-
const
|
|
51
|
+
const requestBody = shouldSerializeJsonBody ? JSON.stringify(options.body) : options.body;
|
|
52
|
+
return (0, _undici.fetch)(url, {
|
|
47
53
|
...options,
|
|
48
|
-
|
|
54
|
+
dispatcher: proxyDispatcher ?? undefined,
|
|
49
55
|
headers,
|
|
50
|
-
body:
|
|
51
|
-
};
|
|
52
|
-
return (0, _nodeFetch.default)(url, opts);
|
|
56
|
+
body: requestBody
|
|
57
|
+
});
|
|
53
58
|
};
|
|
54
59
|
exports.default = _default;
|
package/dist/lib/api.js
CHANGED
|
@@ -12,7 +12,6 @@ var _retry = require("@apollo/client/link/retry");
|
|
|
12
12
|
var _chalk = _interopRequireDefault(require("chalk"));
|
|
13
13
|
var _debug = _interopRequireDefault(require("debug"));
|
|
14
14
|
var _graphql = require("graphql");
|
|
15
|
-
var _nodeFetch = require("node-fetch");
|
|
16
15
|
var _http = _interopRequireDefault(require("./api/http"));
|
|
17
16
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
17
|
// Config
|
|
@@ -41,7 +40,7 @@ function shouldRetryRequest(attempt, operation, error) {
|
|
|
41
40
|
debug(`Request failed and max retry attempts reached. ${debugSuffix}`, error);
|
|
42
41
|
return false;
|
|
43
42
|
}
|
|
44
|
-
if (error
|
|
43
|
+
if (error?.code === 'ECONNREFUSED') {
|
|
45
44
|
debug(`Request failed. Retrying request due to connection refused error. ${debugSuffix}`);
|
|
46
45
|
return true;
|
|
47
46
|
}
|
package/dist/lib/cli/command.js
CHANGED
|
@@ -323,6 +323,41 @@ class CommanderArgsCompat {
|
|
|
323
323
|
}
|
|
324
324
|
return null;
|
|
325
325
|
}
|
|
326
|
+
hasWpCommandPayload(subcommandArgs) {
|
|
327
|
+
const wpFlagsWithoutValue = new Set(['--help', '-h', '--version', '-v', '--yes', '-y']);
|
|
328
|
+
let expectOptionValue = false;
|
|
329
|
+
for (const arg of subcommandArgs) {
|
|
330
|
+
if (expectOptionValue) {
|
|
331
|
+
if (!isOptionToken(arg)) {
|
|
332
|
+
expectOptionValue = false;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
expectOptionValue = false;
|
|
336
|
+
}
|
|
337
|
+
if (!isOptionToken(arg)) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
if (arg.startsWith('--')) {
|
|
341
|
+
if (arg.includes('=') || wpFlagsWithoutValue.has(arg)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
expectOptionValue = true;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (/^-[A-Za-z0-9]$/.test(arg)) {
|
|
348
|
+
if (wpFlagsWithoutValue.has(arg)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
expectOptionValue = true;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (/^-[A-Za-z0-9]+=/.test(arg)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
326
361
|
async executeSubcommand(argv, parsedAlias, subcommand) {
|
|
327
362
|
const currentScript = argv[1];
|
|
328
363
|
const subcommandName = subcommand.name;
|
|
@@ -334,8 +369,9 @@ class CommanderArgsCompat {
|
|
|
334
369
|
const hasSeparatorBeforeSubcommand = rawArgsBeforeSubcommand.includes('--');
|
|
335
370
|
const argsBeforeSubcommand = rawArgsBeforeSubcommand.filter(arg => arg !== '--');
|
|
336
371
|
const subcommandArgs = parsedAlias.argv.slice(subcommand.index + 1);
|
|
372
|
+
const hasWpCommandPayload = this.hasWpCommandPayload(subcommandArgs);
|
|
337
373
|
const hasSeparator = argv.includes('--');
|
|
338
|
-
if (subcommandName === 'wp' &&
|
|
374
|
+
if (subcommandName === 'wp' && hasWpCommandPayload && !hasSeparator) {
|
|
339
375
|
exit.withError('A double dash ("--") must separate the arguments of "vip" from those of "wp". Run "vip wp --help" for examples.');
|
|
340
376
|
}
|
|
341
377
|
let childArgs = [...argsBeforeSubcommand, ...(hasSeparatorBeforeSubcommand ? ['--'] : []), ...subcommandArgs];
|
|
@@ -8,6 +8,8 @@ const internalBinLoaders = {
|
|
|
8
8
|
vip: () => import('../../bin/vip'),
|
|
9
9
|
'vip-app': () => import('../../bin/vip-app'),
|
|
10
10
|
'vip-app-deploy': () => import('../../bin/vip-app-deploy'),
|
|
11
|
+
'vip-app-deploy-token': () => import('../../bin/vip-app-deploy-token'),
|
|
12
|
+
'vip-app-deploy-token-generate': () => import('../../bin/vip-app-deploy-token-generate'),
|
|
11
13
|
'vip-app-deploy-validate': () => import('../../bin/vip-app-deploy-validate'),
|
|
12
14
|
'vip-app-list': () => import('../../bin/vip-app-list'),
|
|
13
15
|
'vip-backup': () => import('../../bin/vip-backup'),
|
|
@@ -4,12 +4,14 @@ exports.__esModule = true;
|
|
|
4
4
|
exports.MULTIPART_THRESHOLD = exports.COMPRESS_THRESHOLD = void 0;
|
|
5
5
|
exports.checkFileAccess = checkFileAccess;
|
|
6
6
|
exports.detectCompressedMimeType = detectCompressedMimeType;
|
|
7
|
+
exports.fetchWithRetry = fetchWithRetry;
|
|
7
8
|
exports.getFileHash = void 0;
|
|
8
9
|
exports.getFileMeta = getFileMeta;
|
|
9
10
|
exports.getFileSize = getFileSize;
|
|
10
11
|
exports.getPartBoundaries = getPartBoundaries;
|
|
11
12
|
exports.getSignedUploadRequestData = getSignedUploadRequestData;
|
|
12
13
|
exports.isFile = isFile;
|
|
14
|
+
exports.parseEtagHeader = parseEtagHeader;
|
|
13
15
|
exports.unzipFile = void 0;
|
|
14
16
|
exports.uploadImportFileToS3 = uploadImportFileToS3;
|
|
15
17
|
exports.uploadParts = uploadParts;
|
|
@@ -17,28 +19,66 @@ var _chalk = _interopRequireDefault(require("chalk"));
|
|
|
17
19
|
var _crypto = require("crypto");
|
|
18
20
|
var _debug = _interopRequireDefault(require("debug"));
|
|
19
21
|
var _fs = require("fs");
|
|
20
|
-
var _nodeFetch = _interopRequireDefault(require("node-fetch"));
|
|
21
22
|
var _promises = require("node:fs/promises");
|
|
22
23
|
var _promises2 = require("node:stream/promises");
|
|
24
|
+
var _nodeTimers = require("node:timers");
|
|
25
|
+
var _promises3 = require("node:timers/promises");
|
|
23
26
|
var _os = _interopRequireDefault(require("os"));
|
|
24
27
|
var _path = _interopRequireDefault(require("path"));
|
|
25
28
|
var _stream = require("stream");
|
|
29
|
+
var _undici = require("undici");
|
|
26
30
|
var _xml2js = require("xml2js");
|
|
27
31
|
var _zlib = require("zlib");
|
|
28
32
|
var _http = _interopRequireDefault(require("../lib/api/http"));
|
|
29
33
|
var _fileSize = require("../lib/constants/file-size");
|
|
30
34
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
function parseEtagHeader(etag) {
|
|
36
|
+
const normalizedEtag = etag.replace(/^W\//u, '').trim();
|
|
37
|
+
if (normalizedEtag.startsWith('"') && normalizedEtag.endsWith('"')) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(normalizedEtag);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
debug(`Unable to JSON.parse ETag header, falling back to raw value: ${err.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return normalizedEtag.replace(/^"(.*)"$/u, '$1');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wraps `fetch` with exponential-backoff retries.
|
|
48
|
+
*
|
|
49
|
+
* A request body that is a Node stream can only be consumed once. If the body
|
|
50
|
+
* is a stream and we retried with the same `init`, the second attempt would
|
|
51
|
+
* pass an already-consumed (disturbed/locked) stream to `fetch`, which throws
|
|
52
|
+
* the misleading `Response body object should not be disturbed or locked` error
|
|
53
|
+
* and masks the original network failure.
|
|
54
|
+
*
|
|
55
|
+
* To retry safely, callers that send a stream body should pass `createBody`,
|
|
56
|
+
* which is invoked to produce a fresh body for every attempt. If a stream body
|
|
57
|
+
* is supplied without a `createBody` factory, the request is attempted only
|
|
58
|
+
* once so the underlying error surfaces instead of the misleading undici one.
|
|
59
|
+
*/
|
|
60
|
+
async function fetchWithRetry(input, init = {}, retries = 3, createBody) {
|
|
61
|
+
const bodyIsStream = typeof init.body?.pipe === 'function';
|
|
62
|
+
// Only retry when we can hand `fetch` a fresh, replayable body each attempt.
|
|
63
|
+
const maxAttempts = createBody || !bodyIsStream ? retries : 0;
|
|
64
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
65
|
+
const requestInit = createBody ? {
|
|
66
|
+
...init,
|
|
67
|
+
body: createBody()
|
|
68
|
+
} : init;
|
|
69
|
+
try {
|
|
70
|
+
// eslint-disable-next-line no-await-in-loop
|
|
71
|
+
return await (0, _undici.fetch)(input, requestInit);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (attempt === maxAttempts) {
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
// eslint-disable-next-line no-await-in-loop
|
|
77
|
+
await (0, _promises3.setTimeout)(Math.pow(2, attempt) * 1000); // 1000, 2000, 4000
|
|
78
|
+
}
|
|
40
79
|
}
|
|
41
|
-
|
|
80
|
+
throw new Error('unreachable');
|
|
81
|
+
}
|
|
42
82
|
const debug = (0, _debug.default)('vip:lib/client-file-uploader');
|
|
43
83
|
|
|
44
84
|
// Files smaller than COMPRESS_THRESHOLD will not be compressed before upload
|
|
@@ -203,20 +243,28 @@ async function uploadUsingPutObject({
|
|
|
203
243
|
...fetchOptions.headers,
|
|
204
244
|
'Content-Length': `${fileSize}` // This has to be a string
|
|
205
245
|
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
246
|
+
|
|
247
|
+
// Build a fresh request body for every attempt. The upload streams the file
|
|
248
|
+
// from disk, and a stream can only be consumed once; recreating it per
|
|
249
|
+
// attempt lets `fetchWithRetry` retry without reusing a consumed stream
|
|
250
|
+
// (which would otherwise throw `... should not be disturbed or locked`).
|
|
251
|
+
const createBody = () => {
|
|
252
|
+
if (fileContent) {
|
|
253
|
+
return fileContent;
|
|
214
254
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
255
|
+
let readBytes = 0;
|
|
256
|
+
const progressPassThrough = new _stream.PassThrough();
|
|
257
|
+
progressPassThrough.on('data', data => {
|
|
258
|
+
readBytes += data.length;
|
|
259
|
+
const percentage = `${Math.floor(100 * readBytes / fileSize)}%`;
|
|
260
|
+
debug(percentage);
|
|
261
|
+
if (typeof progressCallback === 'function') {
|
|
262
|
+
progressCallback(percentage);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return (0, _fs.createReadStream)(fileName).pipe(progressPassThrough);
|
|
266
|
+
};
|
|
267
|
+
const response = await fetchWithRetry(presignedRequest.url, fetchOptions, 3, createBody);
|
|
220
268
|
if (response.status === 200) {
|
|
221
269
|
return 'ok';
|
|
222
270
|
}
|
|
@@ -411,18 +459,19 @@ async function uploadParts({
|
|
|
411
459
|
progressCallback
|
|
412
460
|
}) {
|
|
413
461
|
let uploadsInProgress = 0;
|
|
414
|
-
let totalBytesRead = 0;
|
|
415
462
|
const partPercentages = new Array(parts.length).fill(0);
|
|
463
|
+
const partBytesRead = new Array(parts.length).fill(0);
|
|
416
464
|
const readyForPartUpload = () => new Promise(resolve => {
|
|
417
|
-
const canDoInterval = setInterval(() => {
|
|
465
|
+
const canDoInterval = (0, _nodeTimers.setInterval)(() => {
|
|
418
466
|
if (uploadsInProgress < MAX_CONCURRENT_PART_UPLOADS) {
|
|
419
467
|
uploadsInProgress++;
|
|
420
|
-
clearInterval(canDoInterval);
|
|
468
|
+
(0, _nodeTimers.clearInterval)(canDoInterval);
|
|
421
469
|
resolve();
|
|
422
470
|
}
|
|
423
471
|
}, 300);
|
|
424
472
|
});
|
|
425
473
|
const updateProgress = () => {
|
|
474
|
+
const totalBytesRead = partBytesRead.reduce((sum, bytes) => sum + bytes, 0);
|
|
426
475
|
const percentage = `${Math.floor(100 * totalBytesRead / fileMeta.fileSize)}%`;
|
|
427
476
|
if (typeof progressCallback === 'function') {
|
|
428
477
|
progressCallback(percentage);
|
|
@@ -434,32 +483,46 @@ async function uploadParts({
|
|
|
434
483
|
return `Part # ${index}: ${partPercentage}% of ${(partSize / _fileSize.MB_IN_BYTES).toFixed(2)}MB`;
|
|
435
484
|
}).join('\n') + `\n\nOverall Progress: ${percentage}% of ${(fileMeta.fileSize / _fileSize.MB_IN_BYTES).toFixed(2)}MB`);
|
|
436
485
|
};
|
|
437
|
-
const updateProgressInterval = setInterval(updateProgress, 500);
|
|
486
|
+
const updateProgressInterval = (0, _nodeTimers.setInterval)(updateProgress, 500);
|
|
438
487
|
const allDone = await Promise.all(parts.map(async part => {
|
|
439
488
|
const {
|
|
440
489
|
index,
|
|
441
|
-
partSize
|
|
490
|
+
partSize,
|
|
491
|
+
start,
|
|
492
|
+
end
|
|
442
493
|
} = part;
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
494
|
+
|
|
495
|
+
// Build a fresh request body for every attempt. A stream can only be
|
|
496
|
+
// consumed once, so recreating the ranged read stream (and its progress
|
|
497
|
+
// PassThrough) per attempt lets `fetchWithRetry` retry without reusing a
|
|
498
|
+
// disturbed/locked stream. Reset this part's progress counters so a retry
|
|
499
|
+
// re-streams the part without double-counting bytes.
|
|
500
|
+
const createBody = () => {
|
|
501
|
+
partBytesRead[index] = 0;
|
|
502
|
+
partPercentages[index] = 0;
|
|
503
|
+
const progressPassThrough = new _stream.PassThrough();
|
|
504
|
+
progressPassThrough.on('data', data => {
|
|
505
|
+
partBytesRead[index] += data.length;
|
|
506
|
+
partPercentages[index] = Math.floor(100 * partBytesRead[index] / partSize);
|
|
507
|
+
});
|
|
508
|
+
return (0, _fs.createReadStream)(fileMeta.fileName, {
|
|
509
|
+
start,
|
|
510
|
+
end
|
|
511
|
+
}).pipe(progressPassThrough);
|
|
512
|
+
};
|
|
450
513
|
await readyForPartUpload();
|
|
451
514
|
const uploadResult = await uploadPart({
|
|
452
515
|
app,
|
|
453
516
|
env,
|
|
454
517
|
fileMeta,
|
|
455
518
|
part,
|
|
456
|
-
|
|
519
|
+
createBody,
|
|
457
520
|
uploadId
|
|
458
521
|
});
|
|
459
522
|
uploadsInProgress--;
|
|
460
523
|
return uploadResult;
|
|
461
524
|
}));
|
|
462
|
-
clearInterval(updateProgressInterval);
|
|
525
|
+
(0, _nodeTimers.clearInterval)(updateProgressInterval);
|
|
463
526
|
updateProgress();
|
|
464
527
|
return allDone;
|
|
465
528
|
}
|
|
@@ -467,18 +530,15 @@ async function uploadPart({
|
|
|
467
530
|
app,
|
|
468
531
|
env,
|
|
469
532
|
fileMeta: {
|
|
470
|
-
basename
|
|
471
|
-
fileName
|
|
533
|
+
basename
|
|
472
534
|
},
|
|
473
535
|
part,
|
|
474
|
-
|
|
536
|
+
createBody,
|
|
475
537
|
uploadId
|
|
476
538
|
}) {
|
|
477
539
|
const {
|
|
478
|
-
end,
|
|
479
540
|
index,
|
|
480
|
-
partSize
|
|
481
|
-
start
|
|
541
|
+
partSize
|
|
482
542
|
} = part;
|
|
483
543
|
const s3PartNumber = index + 1; // S3 multipart is indexed from 1
|
|
484
544
|
|
|
@@ -504,15 +564,13 @@ async function uploadPart({
|
|
|
504
564
|
* ...so it may not be feasible to include with presigned requests.
|
|
505
565
|
*/
|
|
506
566
|
};
|
|
507
|
-
|
|
508
|
-
start,
|
|
509
|
-
end
|
|
510
|
-
}).pipe(progressPassThrough);
|
|
511
|
-
const fetchResponse = await fetchWithRetry(partUploadRequestData.url, fetchOptions);
|
|
567
|
+
const fetchResponse = await fetchWithRetry(partUploadRequestData.url, fetchOptions, 3, createBody);
|
|
512
568
|
if (fetchResponse.status === 200) {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
569
|
+
const etag = fetchResponse.headers.get('etag');
|
|
570
|
+
if (!etag) {
|
|
571
|
+
throw new Error('Unable to upload file part. Missing ETag response header.');
|
|
572
|
+
}
|
|
573
|
+
return parseEtagHeader(etag);
|
|
516
574
|
}
|
|
517
575
|
const result = await fetchResponse.text();
|
|
518
576
|
|