@automattic/vip 3.4.1 → 3.5.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.
@@ -58,6 +58,9 @@ services:
58
58
  <% } %>
59
59
  <% if ( autologinKey ) { %>
60
60
  VIP_DEV_AUTOLOGIN_KEY: "<%= autologinKey %>"
61
+ <% } %>
62
+ <% if ( cron ) { %>
63
+ ENABLE_CRON: 1
61
64
  <% } %>
62
65
  LANDO_NO_USER_PERMS: 'enable'
63
66
  LANDO_NEEDS_EXEC: 1
@@ -83,6 +86,7 @@ services:
83
86
  --domain "http://<%= siteSlug %>.<%= domain %>/"
84
87
  --title "<%= wpTitle %>"
85
88
  <% if ( multisite ) { %>--ms-domain "<%= siteSlug %>.<%= domain %>" <% if ( multisite === true || multisite === 'subdomain' ) { %>--subdomain <% } %> <% } %>
89
+
86
90
  database:
87
91
  type: compose
88
92
  services:
@@ -120,12 +124,25 @@ services:
120
124
 
121
125
  <% if ( phpmyadmin ) { %>
122
126
  phpmyadmin:
123
- type: phpmyadmin
124
- hosts:
125
- - database
126
- overrides:
127
+ type: compose
128
+ services:
129
+ image: phpmyadmin:5
130
+ command: /docker-entrypoint.sh apache2-foreground
127
131
  environment:
132
+ MYSQL_ROOT_PASSWORD: ''
133
+ PMA_HOSTS: database
134
+ PMA_PORT: 3306
135
+ PMA_USER: root
136
+ PMA_PASSWORD: ''
128
137
  UPLOAD_LIMIT: 4G
138
+ LANDO_NO_USER_PERMS: 1
139
+ LANDO_NEEDS_EXEC: 1
140
+ ports:
141
+ - 127.0.0.1::80
142
+ volumes:
143
+ - pma_www:/var/www/html
144
+ volumes:
145
+ pma_www:
129
146
  <% } %>
130
147
 
131
148
  <% if ( elasticsearch ) { %>
@@ -20,6 +20,7 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
20
20
  * Internal dependencies
21
21
  */
22
22
  const debug = (0, _debug.default)('@automattic/vip:bin:vip-app-deploy-validate');
23
+ const baseUsage = 'vip app deploy validate';
23
24
  async function appDeployValidateCmd(arg = [], opts = {}) {
24
25
  const app = opts.app;
25
26
  const env = opts.env;
@@ -37,14 +38,15 @@ async function appDeployValidateCmd(arg = [], opts = {}) {
37
38
  } else {
38
39
  await (0, _customDeploy2.validateTarFile)(fileName);
39
40
  }
40
- console.log(_chalk.default.green('✓ Compressed file has been successfully validated with no errors!'));
41
+ console.log(_chalk.default.green('✓ Compressed file has been successfully validated with no errors.'));
41
42
  }
42
43
 
43
44
  // Command examples for the `vip app deploy validate` help prompt
44
45
  const examples = [{
45
- usage: 'vip app @mysite.develop deploy validate <file.zip|file.tar.gz>',
46
- description: 'Validate the compressed file to see if it can be deployed to your site'
46
+ usage: 'vip app deploy validate file.tar.gz',
47
+ description: 'Validate the directory structure of the local archived file named "file.tar.gz".'
47
48
  }];
48
49
  void (0, _command.default)({
49
- requiredArgs: 1
50
- }).examples(examples).option('app', 'The application name or ID').option('env', 'The environment name or ID').argv(process.argv, appDeployValidateCmd);
50
+ requiredArgs: 1,
51
+ usage: baseUsage
52
+ }).examples(examples).argv(process.argv, appDeployValidateCmd);
@@ -35,6 +35,7 @@ const START_DEPLOY_MUTATION = (0, _graphqlTag.default)`
35
35
  }
36
36
  `;
37
37
  const debug = (0, _debug.default)('@automattic/vip:bin:vip-app-deploy');
38
+ const baseUsage = 'vip app deploy';
38
39
  const DEPLOY_PREFLIGHT_PROGRESS_STEPS = [{
39
40
  id: 'upload',
40
41
  name: 'Uploading file'
@@ -221,15 +222,16 @@ Processing the file for deployment to your environment...
221
222
  const examples = [
222
223
  // `app` subcommand
223
224
  {
224
- usage: 'vip app @mysite.develop deploy file.zip',
225
- description: 'Deploy the given compressed file to your site'
225
+ usage: 'WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.zip',
226
+ description: 'Deploy a local archived file named "file.zip" that contains application code to a VIP Platform environment that has Custom Deployment enabled.'
226
227
  }, {
227
- usage: 'vip app @mysite.develop deploy file.zip --message "This is a deploy message"',
228
- description: 'Deploy the given compressed file to your site'
228
+ usage: 'WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.tgz --message="A description for this deploy"',
229
+ description: 'Add a descriptive message for the Custom Deployment of the archived file named "file.tgz".'
229
230
  }, {
230
- usage: 'vip app @mysite.develop deploy file.zip --skip-confirmation',
231
- description: 'Deploy the given compressed file to your site without prompting'
231
+ usage: 'WPVIP_DEPLOY_TOKEN=1234 vip @example-app.develop app deploy file.tar.gz --skip-confirmation',
232
+ description: 'Skip the confirmation prompt for the Custom Deployment of the archived file named "file.tar.gz" to the environment.'
232
233
  }];
233
234
  void (0, _command.default)({
234
- requiredArgs: 1
235
- }).command('validate', 'Validate a file before deploying in Custom Deployments').examples(examples).option('message', 'Custom message for deploy').option('skip-confirmation', 'Skip confirmation prompt').option('force', 'Skip confirmation prompt (deprecated)').option('app', 'The application name or ID').option('env', 'The environment name or ID').argv(process.argv, appDeployCmd);
235
+ requiredArgs: 1,
236
+ usage: baseUsage
237
+ }).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);
@@ -6,27 +6,32 @@ var _api = _interopRequireDefault(require("../lib/api"));
6
6
  var _command = _interopRequireDefault(require("../lib/cli/command"));
7
7
  var _tracker = require("../lib/tracker");
8
8
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
+ const baseUsage = 'vip app list';
9
10
  (0, _command.default)({
10
- format: true
11
- }).argv(process.argv, async () => {
11
+ format: true,
12
+ usage: baseUsage
13
+ }).examples([{
14
+ usage: baseUsage + '\n' + ' - ┌──────┬─────────────────┬─────────────────────────────────┐\n' + ' - │ id │ name │ repo │\n' + ' - ┌──────┬─────────────────┬─────────────────────────────────┐\n' + ' - │ 8886 │ example-app │ wpcomvip/my-org-example-app │\n' + ' - ┌──────┬─────────────────┬─────────────────────────────────┐\n' + ' - │ 4325 │ mytestmultisite │ wpcomvip/my-org-mytestmultisite │\n' + ' - └──────┴─────────────────┴─────────────────────────────────┘\n',
15
+ description: 'Retrieve a list of applications that can be accessed by the current authenticated VIP-CLI user.'
16
+ }]).argv(process.argv, async () => {
12
17
  const api = (0, _api.default)();
13
18
  await (0, _tracker.trackEvent)('app_list_command_execute');
14
19
  let response;
15
20
  try {
16
21
  response = await api.query({
17
22
  query: (0, _graphqlTag.default)`
18
- query Apps($first: Int, $after: String) {
19
- apps(first: $first, after: $after) {
20
- total
21
- nextCursor
22
- edges {
23
- id
24
- name
25
- repo
23
+ query Apps($first: Int, $after: String) {
24
+ apps(first: $first, after: $after) {
25
+ total
26
+ nextCursor
27
+ edges {
28
+ id
29
+ name
30
+ repo
31
+ }
26
32
  }
27
33
  }
28
- }
29
- `,
34
+ `,
30
35
  variables: {
31
36
  first: 100,
32
37
  after: null // TODO make dynamic
@@ -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 <app>', 'Pass an app name or ID to get details about that app').example('vip app 123', 'Get details about the app with ID 123').example('vip app vip-test', 'Get details about the app named vip-test').example('vip app @mysite.develop deploy <file.zip>', 'Deploy the given compressed file to your site').command('list', 'List your VIP applications').command('deploy', 'Deploy to your app from a file').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.').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 => {
15
15
  await (0, _tracker.trackEvent)('app_command_execute');
16
16
  let res;
17
17
  try {
@@ -46,7 +46,7 @@ async function deleteEnvVarCommand(arg, opt) {
46
46
  await (0, _tracker.trackEvent)('envvar_delete_user_cancelled_input', trackingParams);
47
47
  (0, _input.cancel)();
48
48
  });
49
- if (!(await (0, _input.confirm)(`Are you sure? ${_chalk.default.bold.red('Deletion is permanent')} (y/N)`))) {
49
+ if (!(await (0, _input.confirm)(`Are you sure? ${_chalk.default.bold.red('Deletion is permanent')}`))) {
50
50
  await (0, _tracker.trackEvent)('envvar_delete_user_cancelled_confirmation', trackingParams);
51
51
  (0, _input.cancel)();
52
52
  }
@@ -65,7 +65,7 @@ async function setEnvVarCommand(arg, opt) {
65
65
  console.log('===== Received value printed above =====');
66
66
  console.log();
67
67
  }
68
- if (!(await (0, _input.confirm)(`Please ${_chalk.default.bold('confirm')} the input value above (y/N)`))) {
68
+ if (!(await (0, _input.confirm)(`Please ${_chalk.default.bold('confirm')} the input value above`))) {
69
69
  await (0, _tracker.trackEvent)('envvar_set_user_cancelled_confirmation', trackingParams);
70
70
  (0, _input.cancel)();
71
71
  }
package/dist/bin/vip.js CHANGED
@@ -22,7 +22,7 @@ const tokenURL = 'https://dashboard.wpvip.com/me/cli/token';
22
22
  const customDeployToken = process.env.WPVIP_DEPLOY_TOKEN;
23
23
  const runCmd = async function () {
24
24
  const cmd = (0, _command.default)();
25
- cmd.command('logout', 'Log out the current authenticated VIP-CLI user.').command('app', 'List and modify your VIP applications').command('backup', 'Generate a backup of an environment.').command('cache', 'Manage page cache for an environment.').command('config', 'Manage environment configurations.').command('dev-env', 'Create and manage VIP Local Development Environments.').command('export', 'Export a copy of data associated with an environment.').command('import', 'Import media or SQL files into your VIP applications').command('logs', 'Get logs from your VIP applications').command('search-replace', 'Perform search and replace tasks on files').command('slowlogs', 'Retrieve MySQL slow query logs from an environment.').command('db', "Access an environment's database.").command('sync', 'Sync production to a development environment').command('whoami', 'Retrieve details about the current authenticated VIP-CLI user.').command('validate', 'Validate your VIP application and environment').command('wp', 'Run WP CLI commands against an environment');
25
+ cmd.command('logout', 'Log out the current authenticated VIP-CLI user.').command('app', 'Interact with applications that the current authenticated VIP-CLI user has permission to access.').command('backup', 'Generate a backup of an environment.').command('cache', 'Manage page cache for an environment.').command('config', 'Manage environment configurations.').command('dev-env', 'Create and manage VIP Local Development Environments.').command('export', 'Export a copy of data associated with an environment.').command('import', 'Import media or SQL files into your VIP applications').command('logs', 'Get logs from your VIP applications').command('search-replace', 'Perform search and replace tasks on files').command('slowlogs', 'Retrieve MySQL slow query logs from an environment.').command('db', "Access an environment's database.").command('sync', 'Sync production to a development environment').command('whoami', 'Retrieve details about the current authenticated VIP-CLI user.').command('validate', 'Validate your VIP application and environment').command('wp', 'Run WP CLI commands against an environment');
26
26
  cmd.argv(process.argv);
27
27
  };
28
28
 
@@ -13,6 +13,7 @@ var _backupStorageAvailability = require("../lib/backup-storage-availability/bac
13
13
  var exit = _interopRequireWildcard(require("../lib/cli/exit"));
14
14
  var _format = require("../lib/cli/format");
15
15
  var _progress = require("../lib/cli/progress");
16
+ var _retry = require("../lib/retry");
16
17
  var _utils = require("../lib/utils");
17
18
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
18
19
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
@@ -83,7 +84,7 @@ const CREATE_EXPORT_JOB_MUTATION = exports.CREATE_EXPORT_JOB_MUTATION = (0, _gra
83
84
  * @param {number} envId Environment ID
84
85
  * @return {Promise} A promise which resolves to the latest backup and job status
85
86
  */
86
- async function fetchLatestBackupAndJobStatus(appId, envId) {
87
+ async function fetchLatestBackupAndJobStatusBase(appId, envId) {
87
88
  const api = (0, _api.default)();
88
89
  const response = await api.query({
89
90
  query: BACKUP_AND_JOB_STATUS_QUERY,
@@ -107,6 +108,13 @@ async function fetchLatestBackupAndJobStatus(appId, envId) {
107
108
  jobs
108
109
  };
109
110
  }
111
+ async function fetchLatestBackupAndJobStatus(appId, envId) {
112
+ return await (0, _retry.retry)({
113
+ retryOnlyIf: options => {
114
+ return (options.error.message || '').indexOf('Unexpected token < in JSON at position 0') !== -1;
115
+ }
116
+ }, () => fetchLatestBackupAndJobStatusBase(appId, envId));
117
+ }
110
118
 
111
119
  /**
112
120
  * Generates a download link for a backup
@@ -32,4 +32,4 @@ const DEV_ENVIRONMENT_DEFAULTS = exports.DEV_ENVIRONMENT_DEFAULTS = {
32
32
  multisite: false,
33
33
  phpVersion: Object.keys(DEV_ENVIRONMENT_PHP_VERSIONS)[0]
34
34
  };
35
- const DEV_ENVIRONMENT_VERSION = exports.DEV_ENVIRONMENT_VERSION = '2.0.3';
35
+ const DEV_ENVIRONMENT_VERSION = exports.DEV_ENVIRONMENT_VERSION = '2.1.0';
@@ -250,13 +250,15 @@ async function promptForArguments(preselectedOptions, defaultOptions, suppressPr
250
250
  xdebugConfig: preselectedOptions.xdebugConfig,
251
251
  siteSlug: '',
252
252
  mailpit: false,
253
- photon: false
253
+ photon: false,
254
+ cron: false
254
255
  };
255
256
  const promptLabels = {
256
257
  xdebug: 'XDebug',
257
258
  phpmyadmin: 'phpMyAdmin',
258
259
  mailpit: 'Mailpit',
259
- photon: 'Photon'
260
+ photon: 'Photon',
261
+ cron: 'Cron'
260
262
  };
261
263
  if (create && !instanceData.mediaRedirectDomain && defaultOptions.mediaRedirectDomain) {
262
264
  const mediaRedirectPromptText = `Would you like to redirect to ${defaultOptions.mediaRedirectDomain} for missing media files?`;
@@ -280,7 +282,7 @@ async function promptForArguments(preselectedOptions, defaultOptions, suppressPr
280
282
  } else {
281
283
  instanceData.elasticsearch = await promptForBoolean('Enable Elasticsearch (needed by Enterprise Search)?', Boolean(defaultOptions.elasticsearch));
282
284
  }
283
- const services = ['phpmyadmin', 'xdebug', 'mailpit', 'photon'];
285
+ const services = ['phpmyadmin', 'xdebug', 'mailpit', 'photon', 'cron'];
284
286
  for (const service of services) {
285
287
  if (service in instanceData) {
286
288
  const preselected = preselectedOptions[service];
@@ -626,7 +628,7 @@ function processVersionOption(value) {
626
628
  const phpVersionsSupported = Object.keys(_devEnvironment.DEV_ENVIRONMENT_PHP_VERSIONS).join(', ');
627
629
  function addDevEnvConfigurationOptions(command) {
628
630
  // We leave the third parameter to undefined on some because the defaults are handled in preProcessInstanceData()
629
- return command.option('wordpress', 'Manage the version of WordPress. Accepts a string value for major versions (6.x). Defaults to the most recent version of WordPress.', undefined, processVersionOption).option(['u', 'mu-plugins'], 'Manage the source for VIP MU plugins. Accepts "demo" (default) for a read-only image of the staging branch, or a path to a built copy of VIP MU plugins on the local machine.').option('app-code', 'Manage the source for application code. Accepts "demo" (default) for a read-only image of WordPress VIP skeleton application code, or a path to a VIP formatted application repo on the local machine.').option('phpmyadmin', 'Enable or disable phpMyAdmin, disabled by default. Accepts "y" (default value) to enable or "n" to disable. When enabled, refer to the value of "PHPMYADMIN URLS" in the information output for a local environment for the URL to access phpMyAdmin.', undefined, processBooleanOption).option('xdebug', 'Enable or disable XDebug, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option('xdebug_config', 'Override some default configuration settings for Xdebug. Accepts a string value that is assigned to the XDEBUG_CONFIG environment variable.').option('elasticsearch', 'Enable or disable Elasticsearch (required by Enterprise Search), disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option(['r', 'media-redirect-domain'], 'Configure media files to be proxied from a VIP Platform environment. Accepts a string value for the primary domain of the VIP Platform environment or "n" to disable the media proxy.', undefined, processMediaRedirectDomainOption).option('php', `Manage the version of PHP. Accepts a string value for minor versions: ${phpVersionsSupported}`, undefined, processVersionOption).option(['A', 'mailpit'], 'Enable or disable Mailpit, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option(['H', 'photon'], 'Enable or disable Photon, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption);
631
+ return command.option('wordpress', 'Manage the version of WordPress. Accepts a string value for major versions (6.x). Defaults to the most recent version of WordPress.', undefined, processVersionOption).option(['u', 'mu-plugins'], 'Manage the source for VIP MU plugins. Accepts "demo" (default) for a read-only image of the staging branch, or a path to a built copy of VIP MU plugins on the local machine.').option('app-code', 'Manage the source for application code. Accepts "demo" (default) for a read-only image of WordPress VIP skeleton application code, or a path to a VIP formatted application repo on the local machine.').option('phpmyadmin', 'Enable or disable phpMyAdmin, disabled by default. Accepts "y" (default value) to enable or "n" to disable. When enabled, refer to the value of "PHPMYADMIN URLS" in the information output for a local environment for the URL to access phpMyAdmin.', undefined, processBooleanOption).option('xdebug', 'Enable or disable XDebug, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option('xdebug_config', 'Override some default configuration settings for Xdebug. Accepts a string value that is assigned to the XDEBUG_CONFIG environment variable.').option('elasticsearch', 'Enable or disable Elasticsearch (required by Enterprise Search), disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option(['r', 'media-redirect-domain'], 'Configure media files to be proxied from a VIP Platform environment. Accepts a string value for the primary domain of the VIP Platform environment or "n" to disable the media proxy.', undefined, processMediaRedirectDomainOption).option('php', `Manage the version of PHP. Accepts a string value for minor versions: ${phpVersionsSupported}`, undefined, processVersionOption).option('cron', 'Enable or disable cron, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option(['A', 'mailpit'], 'Enable or disable Mailpit, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption).option(['H', 'photon'], 'Enable or disable Photon, disabled by default. Accepts "y" (default value) to enable or "n" to disable.', undefined, processBooleanOption);
630
632
  }
631
633
 
632
634
  /**
@@ -84,6 +84,7 @@ function sanitizeConfiguration(configuration, configurationFilePath) {
84
84
  mailpit: stringToBooleanIfDefined(configuration.mailpit),
85
85
  'media-redirect-domain': configuration['media-redirect-domain']?.toString(),
86
86
  photon: stringToBooleanIfDefined(configuration.photon),
87
+ cron: stringToBooleanIfDefined(configuration.cron),
87
88
  meta: configurationMeta
88
89
  };
89
90
 
@@ -121,7 +122,8 @@ function mergeConfigurationFileOptions(preselectedOptions, configurationFileOpti
121
122
  xdebugConfig: configurationFileOptions['xdebug-config'],
122
123
  mailpit: configurationFileOptions.mailpit,
123
124
  mediaRedirectDomain: configurationFileOptions['media-redirect-domain'],
124
- photon: configurationFileOptions.photon
125
+ photon: configurationFileOptions.photon,
126
+ cron: configurationFileOptions.cron
125
127
  };
126
128
  const mergedOptions = {};
127
129
  Object.keys(configurationFileInstanceOptions).forEach(key => {
@@ -179,5 +181,6 @@ elasticsearch: false
179
181
  xdebug: false
180
182
  mailpit: false
181
183
  photon: false
184
+ cron: false
182
185
  `;
183
186
  }
@@ -148,6 +148,9 @@ function preProcessInstanceData(instanceData) {
148
148
  if (!newInstanceData.photon) {
149
149
  newInstanceData.photon = false;
150
150
  }
151
+ if (!newInstanceData.cron) {
152
+ newInstanceData.cron = false;
153
+ }
151
154
 
152
155
  // Mailpit migration
153
156
  newInstanceData.mailpit ??= false;
@@ -80,7 +80,7 @@ async function getLandoConfig() {
80
80
  subdir: '.',
81
81
  namespace: '@lando'
82
82
  }],
83
- disablePlugins: ['@lando/argv', '@lando/mailhog'],
83
+ disablePlugins: ['@lando/argv', '@lando/mailhog', '@lando/phpmyadmin'],
84
84
  proxyName: 'vip-dev-env-proxy',
85
85
  userConfRoot: landoDir,
86
86
  home: fakeHomeDir,
@@ -373,16 +373,19 @@ async function getExtraServicesConnections(lando, app) {
373
373
  // eslint-disable-next-line no-await-in-loop
374
374
  const containerScan = service.id ? await lando.engine.docker.scan(service.id) : null;
375
375
  if (containerScan?.NetworkSettings.Ports) {
376
- const mappings = Object.keys(containerScan.NetworkSettings.Ports).map(internalPort => containerScan.NetworkSettings.Ports[internalPort]).filter(externalMapping => externalMapping?.length);
377
- if (mappings.length) {
378
- const {
379
- HostIp: host,
380
- HostPort: port
381
- } = mappings[0][0];
376
+ const mappings = Object.values(containerScan.NetworkSettings.Ports).filter(externalMapping => externalMapping?.length);
377
+ mappings[0]?.forEach(({
378
+ HostIp: host,
379
+ HostPort: port
380
+ }) => {
382
381
  const label = displayConfiguration.label ?? service.service;
383
382
  const value = (displayConfiguration.protocol ? `${displayConfiguration.protocol}://` : '') + `${host}:${port}`;
384
- extraServices[label] = value;
385
- }
383
+ if (extraServices[label]) {
384
+ extraServices[label] += `, ${value}`;
385
+ } else {
386
+ extraServices[label] = value;
387
+ }
388
+ });
386
389
  }
387
390
  }
388
391
  return extraServices;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.EXPONENTIAL_BACKOFF_STARTING_IN_50_MS = exports.EXPONENTIAL_BACKOFF_STARTING_IN_200_MS = exports.EXPONENTIAL_BACKOFF_STARTING_IN_100_MS = void 0;
5
+ exports.exponentialBackoff = exponentialBackoff;
6
+ exports.retry = retry;
7
+ // copied over from our internal lib
8
+
9
+ const EXPONENTIAL_BACKOFF_STARTING_IN_50_MS = exports.EXPONENTIAL_BACKOFF_STARTING_IN_50_MS = exponentialBackoff(50);
10
+ const EXPONENTIAL_BACKOFF_STARTING_IN_100_MS = exports.EXPONENTIAL_BACKOFF_STARTING_IN_100_MS = exponentialBackoff(100);
11
+ const EXPONENTIAL_BACKOFF_STARTING_IN_200_MS = exports.EXPONENTIAL_BACKOFF_STARTING_IN_200_MS = exponentialBackoff(200);
12
+ const MAX_INTERVAL_BETWEEN_ATTEMPTS = 600000; // 10 minutes
13
+
14
+ const NOOP = async () => {};
15
+ const DEFAULT_OPTIONS = {
16
+ maxRetries: 4,
17
+ interval: EXPONENTIAL_BACKOFF_STARTING_IN_100_MS,
18
+ retryOnlyIf: () => Promise.resolve(true),
19
+ onRetry: NOOP,
20
+ onFailedAttempt: NOOP
21
+ };
22
+ /**
23
+ * We don't need a strict Error instance, which can help in cases (probably
24
+ * just tests) where code throws an object instead of an Error instance. We
25
+ * really just need to check for an object with a message property.
26
+ */
27
+ function isErrorLike(value) {
28
+ return value instanceof Error || null !== value && 'object' === typeof value && 'message' in value;
29
+ }
30
+
31
+ /**
32
+ * Execute a <task> and retries <options.maxRetries> times while it fails.
33
+ * @param {Object} [options] - The options.
34
+ * @param {number} [options.maxRetries] - The max number of retry attempts to be executed.
35
+ * @param {function|number} [options.interval] - The time to wait between retries in milliseconds.
36
+ * It can be either a number or a function.
37
+ * Function arguments: retryAttemptNumber.
38
+ * Expected return: a number representing the time in milliseconds.
39
+ * @param {function} [options.retryOnlyIf] - A function used to determine if a retry should be attempted.
40
+ * Arguments: Object<{ error, attemptNumber }>.
41
+ * Expected return: boolean|Promise<boolean>.
42
+ * @param {function} [options.onRetry] - A callback function executed right before a retry attempt.
43
+ * Arguments: Object<{ retryNumber: attemptNumber }>.
44
+ * Expected return: void|Promise<void>.
45
+ * @param {function} [options.onFailedAttempt] - A callback function executed right after a failed attempt.
46
+ * Arguments: Object<{ error, attemptNumber, attemptDuration }>.
47
+ * Expected return: void|Promise<void>.
48
+ * @param {function} task - The task to be executed.
49
+ * @return {any} - The value returned by the provided <task> function when it is successfully executed
50
+ */
51
+
52
+ async function retry(optionsOrTask, task) {
53
+ let options = optionsOrTask;
54
+ if (arguments.length < 3 && typeof optionsOrTask === 'function') {
55
+ task = optionsOrTask;
56
+ options = {};
57
+ }
58
+ const finalOptions = {
59
+ ...DEFAULT_OPTIONS,
60
+ ...options
61
+ };
62
+ const {
63
+ maxRetries,
64
+ interval,
65
+ retryOnlyIf,
66
+ onRetry,
67
+ onFailedAttempt
68
+ } = finalOptions;
69
+ validateTask(task);
70
+ validateOptions(finalOptions);
71
+ const maxAttempts = maxRetries + 1;
72
+ for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber++) {
73
+ /* eslint-disable no-await-in-loop */
74
+ const attemptStart = Date.now();
75
+ try {
76
+ return await task();
77
+ } catch (caughtError) {
78
+ const attemptEnd = Date.now();
79
+ const attemptDuration = attemptEnd - attemptStart;
80
+ const attemptStartTime = new Date(attemptStart);
81
+ const attemptEndTime = new Date(attemptEnd);
82
+ const error = isErrorLike(caughtError) ? caughtError : new Error(String(caughtError));
83
+ await onFailedAttempt({
84
+ error,
85
+ attemptNumber,
86
+ attemptDuration,
87
+ attemptStartTime,
88
+ attemptEndTime
89
+ });
90
+ const shouldRetry = await retryOnlyIf({
91
+ error,
92
+ attemptNumber
93
+ });
94
+ if (attemptNumber === maxAttempts || shouldRetry !== true) {
95
+ throw error;
96
+ }
97
+ await awaitInterval(interval, attemptNumber);
98
+ await onRetry({
99
+ retryNumber: attemptNumber
100
+ });
101
+ }
102
+ }
103
+ throw new Error('Error, wrong retry options set');
104
+ }
105
+ function isValidInterval(interval) {
106
+ if ('function' === typeof interval) {
107
+ return true;
108
+ }
109
+ return typeof interval === 'number' && Number.isInteger(interval) && interval >= 0 && interval <= MAX_INTERVAL_BETWEEN_ATTEMPTS;
110
+ }
111
+ async function awaitInterval(interval, attemptNumber) {
112
+ let newInterval;
113
+ if (typeof interval === 'function') {
114
+ newInterval = interval(attemptNumber);
115
+ if (!isValidInterval(newInterval)) {
116
+ throw new Error(`Invalid calculated interval for retry attempt ${attemptNumber}: "${newInterval}" (type: ${typeof newInterval})`);
117
+ }
118
+ } else {
119
+ newInterval = interval;
120
+ }
121
+ return new Promise(resolve => setTimeout(resolve, newInterval));
122
+ }
123
+ function validateTask(task) {
124
+ if (typeof task !== 'function') {
125
+ throw new Error('Invalid task: it should be a function');
126
+ }
127
+ }
128
+ function validateOptions({
129
+ maxRetries,
130
+ interval,
131
+ onFailedAttempt,
132
+ retryOnlyIf
133
+ }) {
134
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
135
+ throw new Error('Invalid option "maxRetries": it should be an integer number >= 0');
136
+ }
137
+ if (!isValidInterval(interval)) {
138
+ const definition = `an integer number between 0 and ${MAX_INTERVAL_BETWEEN_ATTEMPTS}`;
139
+ throw new Error(`Invalid option "interval": it should be either ${definition}, or a synchronous function which returns ${definition}`);
140
+ }
141
+ if (!isValidInterval(interval)) {
142
+ throw new Error('Invalid option "interval": it should be either an integer number >= 0, or a synchronous function which returns an integer number >= 0');
143
+ }
144
+ if (typeof onFailedAttempt !== 'function') {
145
+ throw new Error('Invalid option "onFailedAttempt": it should be a function');
146
+ }
147
+ if (typeof retryOnlyIf !== 'function') {
148
+ throw new Error('Invalid option "retryOnlyIf": it should be a function');
149
+ }
150
+ }
151
+ function exponentialBackoff(startMilliseconds) {
152
+ return retryAttemptNumber => Math.pow(2, retryAttemptNumber - 1) * startMilliseconds;
153
+ }
@@ -15,9 +15,9 @@ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return
15
15
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
16
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
17
  const errorMessages = {
18
- missingThemes: 'Missing `themes` directory from root folder!',
18
+ missingThemes: 'Missing `themes` directory from root folder.',
19
19
  symlink: 'Symlink detected: ',
20
- singleRootDir: 'The compressed file must contain a single root directory!',
20
+ singleRootDir: 'The compressed file must contain a single root directory.',
21
21
  invalidExt: 'Invalid file extension. Please provide a .zip, .tar.gz, or a .tgz file.',
22
22
  invalidChars: (filename, invalidChars) => `Filename ${filename} contains disallowed characters: ${invalidChars}`
23
23
  };
@@ -271,9 +271,15 @@ const checks = {
271
271
  recommendation: ''
272
272
  },
273
273
  siteHomeUrlLando: {
274
- matcher: "'(siteurl|home)',\\s?'(.*?)'",
274
+ matcher: "'(siteurl|home)',\\s?'([^']+)'",
275
275
  matchHandler: (lineNumber, results, expectedDomain) => {
276
- const foundDomain = results[2].replace(/https?:\/\//, '');
276
+ let foundDomain = results[2];
277
+ if (!/^https?:\/\//i.test(foundDomain)) {
278
+ return {
279
+ falsePositive: true
280
+ };
281
+ }
282
+ foundDomain = foundDomain.replace(/^https?:\/\//, '');
277
283
  if (!foundDomain.trim()) {
278
284
  return {
279
285
  falsePositive: true
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## Changelog
2
2
 
3
+ ### 3.5.0
4
+
5
+ * feat(dev-env): add support for cron
6
+ * build(deps-dev): bump @automattic/eslint-plugin-wpvip from 0.11.0 to 0.12.0
7
+ * fix(dev-env): display all port mappings
8
+ * refactor(dev-env): migrate from `@lando/phpmyadmin` to `phpmyadmin` Docker image
9
+
10
+ **Full Changelog**: https://github.com/Automattic/vip-cli/compare/3.4.2...3.5.0
11
+
12
+ ### 3.4.2
13
+
14
+ * build(deps-dev): bump @types/node from 18.19.37 to 18.19.38
15
+ * build(deps-dev): bump typescript from 5.4.5 to 5.5.2
16
+ * build(deps-dev): bump @types/uuid from 9.0.8 to 10.0.0
17
+ * build(deps-dev): bump @types/node from 18.19.38 to 18.19.39
18
+ * fix(dev-env): ensure that URLs suggested for replacement start with `http(s)://`
19
+ * Remove unnecessary y/N choice for confirmations
20
+ * Update the vip app command to follow the VIP-CLI style guide
21
+ * Add retries to the fetching of jobs data
22
+
23
+ **Full Changelog**: https://github.com/Automattic/vip-cli/compare/3.4.1...3.4.2
24
+
3
25
  ### 3.4.1
4
26
 
5
27
  * Updating vip cache commands descriptions and examples to follow the VIP-CLI style guide
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "lockfileVersion": 2,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@automattic/vip",
9
- "version": "3.4.1",
9
+ "version": "3.5.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -108,7 +108,7 @@
108
108
  "vip-wp": "dist/bin/vip-wp.js"
109
109
  },
110
110
  "devDependencies": {
111
- "@automattic/eslint-plugin-wpvip": "0.11.0",
111
+ "@automattic/eslint-plugin-wpvip": "0.12.0",
112
112
  "@babel/cli": "7.24.7",
113
113
  "@babel/core": "7.24.7",
114
114
  "@babel/preset-env": "7.24.7",
@@ -132,7 +132,7 @@
132
132
  "@types/single-line-log": "1.1.2",
133
133
  "@types/tar": "^6.1.13",
134
134
  "@types/update-notifier": "^6.0.8",
135
- "@types/uuid": "^9.0.7",
135
+ "@types/uuid": "^10.0.0",
136
136
  "@types/xml2js": "^0.4.14",
137
137
  "dockerode": "^4.0.0",
138
138
  "eslint": "^8.35.0",
@@ -206,9 +206,9 @@
206
206
  }
207
207
  },
208
208
  "node_modules/@automattic/eslint-plugin-wpvip": {
209
- "version": "0.11.0",
210
- "resolved": "https://registry.npmjs.org/@automattic/eslint-plugin-wpvip/-/eslint-plugin-wpvip-0.11.0.tgz",
211
- "integrity": "sha512-GfSGo2KbrRSPmmklL5YRiVONCFDQidF6agv7x9xU8UEmEw6jPNgndrD4yO7KCoEXX4OUYRDi9bZx1afky1TYng==",
209
+ "version": "0.12.0",
210
+ "resolved": "https://registry.npmjs.org/@automattic/eslint-plugin-wpvip/-/eslint-plugin-wpvip-0.12.0.tgz",
211
+ "integrity": "sha512-ARF+Nj0HBHeaTPoWDA+BynaEergBrtHKDoYqIHtLWJEe7xMNRedB3Q4+lo1OuTZGA3ggBwMmOy5OU0izQwPpog==",
212
212
  "dev": true,
213
213
  "dependencies": {
214
214
  "@babel/eslint-parser": "7.24.5",
@@ -3884,9 +3884,9 @@
3884
3884
  "dev": true
3885
3885
  },
3886
3886
  "node_modules/@types/node": {
3887
- "version": "18.19.37",
3888
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.37.tgz",
3889
- "integrity": "sha512-Pi53fdVMk7Ig5IfAMltQQMgtY7xLzHaEous8IQasYsdQbYK3v90FkxI3XYQCe/Qme58pqp14lXJIsFmGP8VoZQ==",
3887
+ "version": "18.19.39",
3888
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz",
3889
+ "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==",
3890
3890
  "dependencies": {
3891
3891
  "undici-types": "~5.26.4"
3892
3892
  }
@@ -3992,9 +3992,9 @@
3992
3992
  }
3993
3993
  },
3994
3994
  "node_modules/@types/uuid": {
3995
- "version": "9.0.8",
3996
- "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
3997
- "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
3995
+ "version": "10.0.0",
3996
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
3997
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
3998
3998
  "dev": true
3999
3999
  },
4000
4000
  "node_modules/@types/xml2js": {
@@ -12643,9 +12643,9 @@
12643
12643
  }
12644
12644
  },
12645
12645
  "node_modules/typescript": {
12646
- "version": "5.4.5",
12647
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
12648
- "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
12646
+ "version": "5.5.2",
12647
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
12648
+ "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
12649
12649
  "dev": true,
12650
12650
  "bin": {
12651
12651
  "tsc": "bin/tsc",
@@ -13585,9 +13585,9 @@
13585
13585
  }
13586
13586
  },
13587
13587
  "@automattic/eslint-plugin-wpvip": {
13588
- "version": "0.11.0",
13589
- "resolved": "https://registry.npmjs.org/@automattic/eslint-plugin-wpvip/-/eslint-plugin-wpvip-0.11.0.tgz",
13590
- "integrity": "sha512-GfSGo2KbrRSPmmklL5YRiVONCFDQidF6agv7x9xU8UEmEw6jPNgndrD4yO7KCoEXX4OUYRDi9bZx1afky1TYng==",
13588
+ "version": "0.12.0",
13589
+ "resolved": "https://registry.npmjs.org/@automattic/eslint-plugin-wpvip/-/eslint-plugin-wpvip-0.12.0.tgz",
13590
+ "integrity": "sha512-ARF+Nj0HBHeaTPoWDA+BynaEergBrtHKDoYqIHtLWJEe7xMNRedB3Q4+lo1OuTZGA3ggBwMmOy5OU0izQwPpog==",
13591
13591
  "dev": true,
13592
13592
  "requires": {
13593
13593
  "@babel/eslint-parser": "7.24.5",
@@ -16306,9 +16306,9 @@
16306
16306
  "dev": true
16307
16307
  },
16308
16308
  "@types/node": {
16309
- "version": "18.19.37",
16310
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.37.tgz",
16311
- "integrity": "sha512-Pi53fdVMk7Ig5IfAMltQQMgtY7xLzHaEous8IQasYsdQbYK3v90FkxI3XYQCe/Qme58pqp14lXJIsFmGP8VoZQ==",
16309
+ "version": "18.19.39",
16310
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz",
16311
+ "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==",
16312
16312
  "requires": {
16313
16313
  "undici-types": "~5.26.4"
16314
16314
  }
@@ -16415,9 +16415,9 @@
16415
16415
  }
16416
16416
  },
16417
16417
  "@types/uuid": {
16418
- "version": "9.0.8",
16419
- "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
16420
- "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
16418
+ "version": "10.0.0",
16419
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
16420
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
16421
16421
  "dev": true
16422
16422
  },
16423
16423
  "@types/xml2js": {
@@ -22653,9 +22653,9 @@
22653
22653
  }
22654
22654
  },
22655
22655
  "typescript": {
22656
- "version": "5.4.5",
22657
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
22658
- "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
22656
+ "version": "5.5.2",
22657
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
22658
+ "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
22659
22659
  "dev": true
22660
22660
  },
22661
22661
  "typical": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/vip",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "The VIP Javascript library & CLI",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -107,7 +107,7 @@
107
107
  },
108
108
  "homepage": "https://github.com/Automattic/vip#readme",
109
109
  "devDependencies": {
110
- "@automattic/eslint-plugin-wpvip": "0.11.0",
110
+ "@automattic/eslint-plugin-wpvip": "0.12.0",
111
111
  "@babel/cli": "7.24.7",
112
112
  "@babel/core": "7.24.7",
113
113
  "@babel/preset-env": "7.24.7",
@@ -131,7 +131,7 @@
131
131
  "@types/single-line-log": "1.1.2",
132
132
  "@types/tar": "^6.1.13",
133
133
  "@types/update-notifier": "^6.0.8",
134
- "@types/uuid": "^9.0.7",
134
+ "@types/uuid": "^10.0.0",
135
135
  "@types/xml2js": "^0.4.14",
136
136
  "dockerode": "^4.0.0",
137
137
  "eslint": "^8.35.0",