GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

askChoice()   B
last analyzed

Complexity

Conditions 9
Paths 8

Size

Total Lines 37
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
cc 9
eloc 20
nc 8
nop 4
dl 0
loc 37
ccs 0
cts 0
cp 0
crap 90
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* (c) Anton Medvedev <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Deployer;
12
13
use Deployer\Exception\Exception;
14
use Deployer\Exception\GracefulShutdownException;
15
use Deployer\Exception\RunException;
16
use Deployer\Exception\TimeoutException;
17
use Deployer\Exception\WillAskUser;
18
use Deployer\Host\Host;
19
use Deployer\Host\Localhost;
20
use Deployer\Host\Range;
21
use Deployer\Importer\Importer;
22
use Deployer\Ssh\RunParams;
23
use Deployer\Support\ObjectProxy;
24
use Deployer\Task\Context;
25
use Deployer\Task\GroupTask;
26
use Deployer\Task\Task;
27
use Deployer\Utility\Httpie;
28
use Symfony\Component\Console\Helper\QuestionHelper;
29
use Symfony\Component\Console\Input\InputInterface;
30
use Symfony\Component\Console\Input\InputOption;
31
use Symfony\Component\Console\Output\OutputInterface;
32
use Symfony\Component\Console\Question\ChoiceQuestion;
33
use Symfony\Component\Console\Question\ConfirmationQuestion;
34
use Symfony\Component\Console\Question\Question;
35
36
use function Deployer\Support\array_merge_alternate;
37
use function Deployer\Support\is_closure;
38
39
/**
40 1
 * Defines a host or hosts.
41 1
 * ```php
42
 * host('example.org');
43 1
 * host('prod.example.org', 'staging.example.org');
44 1
 * ```
45
 *
46
 * Inside task can be used to get `Host` instance of an alias.
47
 * ```php
48
 * task('test', function () {
49
 *     $port = host('example.org')->get('port');
50
 * });
51
 * ```
52
 */
53
function host(string ...$hostname): Host|ObjectProxy
54
{
55
    $deployer = Deployer::get();
56 1
    if (count($hostname) === 1 && $deployer->hosts->has($hostname[0])) {
57 1
        return $deployer->hosts->get($hostname[0]);
58 1
    }
59 1
    $aliases = Range::expand($hostname);
60
61
    foreach ($aliases as $alias) {
62 1
        if ($deployer->hosts->has($alias)) {
63 1
            $host = $deployer->hosts->get($alias);
64 1
            throw new \InvalidArgumentException("Host \"$host\" already exists.");
65 1
        }
66 1
    }
67
68
    if (count($aliases) === 1) {
69
        $host = new Host($aliases[0]);
70
        $deployer->hosts->set($aliases[0], $host);
71
        return $host;
72
    } else {
73
        $hosts = array_map(function ($hostname) use ($deployer): Host {
74
            $host = new Host($hostname);
75
            $deployer->hosts->set($hostname, $host);
76 13
            return $host;
77 13
        }, $aliases);
78
        return new ObjectProxy($hosts);
79 13
    }
80 13
}
81 13
82 13
/**
83
 * Define a local host.
84
 * Deployer will not connect to this host, but will execute commands locally instead.
85
 *
86
 * ```php
87
 * localhost('ci'); // Alias and hostname will be "ci".
88
 * ```
89
 */
90
function localhost(string ...$hostnames): Localhost|ObjectProxy
91
{
92
    $deployer = Deployer::get();
93
    $hostnames = Range::expand($hostnames);
94
95
    if (count($hostnames) <= 1) {
96
        $host = count($hostnames) === 1 ? new Localhost($hostnames[0]) : new Localhost();
97
        $deployer->hosts->set($host->getAlias(), $host);
98
        return $host;
99
    } else {
100
        $hosts = array_map(function ($hostname) use ($deployer): Localhost {
101 5
            $host = new Localhost($hostname);
102
            $deployer->hosts->set($host->getAlias(), $host);
103
            return $host;
104
        }, $hostnames);
105
        return new ObjectProxy($hosts);
106
    }
107
}
108
109
/**
110
 * Returns current host.
111 8
 */
112
function currentHost(): Host
113
{
114
    return Context::get()->getHost();
115
}
116
117
/**
118
 * Returns hosts based on provided selector.
119
 *
120
 * ```php
121
 * on(select('stage=prod, role=db'), function (Host $host) {
122
 *     ...
123
 * });
124
 * ```
125
 *
126
 * @return Host[]
127
 */
128
function select(string $selector): array
129
{
130
    return Deployer::get()->selector->select($selector);
131
}
132
133
/**
134
 * Returns array of hosts selected by user via CLI.
135
 *
136
 * @return Host[]
137
 */
138
function selectedHosts(): array
139
{
140
    $hosts = [];
141
    foreach (get('selected_hosts', []) as $alias) {
142
        $hosts[] = Deployer::get()->hosts->get($alias);
143 15
    }
144
    return $hosts;
145 15
}
146 15
147
/**
148 8
 * Import other php or yaml recipes.
149
 *
150
 * ```php
151
 * import('recipe/common.php');
152
 * ```
153
 *
154
 * ```php
155
 * import(__DIR__ . '/config/hosts.yaml');
156
 * ```
157
 */
158
function import(string $file): void
159
{
160
    Importer::import($file);
161
}
162
163 15
/**
164
 * Set task description.
165 15
 */
166 15
function desc(?string $title = null): ?string
167
{
168
    static $store = null;
169 15
170 15
    if ($title === null) {
171 9
        return $store;
172 9
    } else {
173
        return $store = $title;
174
    }
175
}
176
177 15
/**
178 15
 * Define a new task and save to tasks list.
179
 *
180 15
 * Alternatively get a defined task.
181 8
 *
182 8
 * @param string $name Name of current task.
183
 * @param callable|array|null $body Callable task, array of other tasks names or nothing to get a defined tasks
184
 * @return Task
185 15
 */
186
function task(string $name, callable|array|null $body = null): Task
187
{
188
    $deployer = Deployer::get();
189
190
    if (empty($body)) {
191
        return $deployer->tasks->get($name);
192
    }
193
194
    if (is_callable($body)) {
195
        $task = new Task($name, $body);
196
    } elseif (is_array($body)) {
0 ignored issues
show
introduced by
The condition is_array($body) is always true.
Loading history...
197 1
        $task = new GroupTask($name, $body);
198 1
    } else {
199 1
        throw new \InvalidArgumentException('Task body should be a function or an array.');
200 1
    }
201
202 1
    if ($deployer->tasks->has($name)) {
203 1
        // If task already exists, try to replace.
204
        $existingTask = $deployer->tasks->get($name);
205
        if (get_class($existingTask) !== get_class($task)) {
206
            // There is no "up" or "down"casting in PHP.
207
            throw new \Exception('Tried to replace Task \'' . $name . '\' with a GroupTask or vice-versa. This is not supported. If you are sure you want to do that, remove the old task `Deployer::get()->tasks->remove(<taskname>)` and then re-add the task.');
208
        }
209
        if ($existingTask instanceof GroupTask) {
210
            $existingTask->setGroup($body);
211
        } elseif ($existingTask instanceof Task) {
212
            $existingTask->setCallback($body);
213
        }
214 13
        $task = $existingTask;
215 5
    } else {
216 5
        // If task does not exist, add it to the Collection.
217 5
        $deployer->tasks->set($name, $task);
218
    }
219 13
220 13
    $task->saveSourceLocation();
221
222
    if (!empty(desc())) {
223
        $task->desc(desc());
0 ignored issues
show
Bug introduced by
It seems like desc() can also be of type null; however, parameter $description of Deployer\Task\Task::desc() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

223
        $task->desc(/** @scrutinizer ignore-type */ desc());
Loading history...
224
        desc(''); // Clear title.
225
    }
226
227
    return $task;
228
}
229
230
/**
231 8
 * Call that task before specified task runs.
232
 *
233
 * @param string $task The task before $that should be run.
234
 * @param string|callable $do The task to be run.
235
 *
236 8
 * @return ?Task
237 8
 */
238 8
function before(string $task, string|callable $do): ?Task
239
{
240
    if (is_closure($do)) {
241
        $newTask = task("before:$task", $do);
242
        before($task, "before:$task");
243
        return $newTask;
244
    }
245
    task($task)->addBefore($do);
0 ignored issues
show
Bug introduced by
It seems like $do can also be of type callable; however, parameter $task of Deployer\Task\Task::addBefore() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

245
    task($task)->addBefore(/** @scrutinizer ignore-type */ $do);
Loading history...
246
247
    return null;
248
}
249
250
/**
251 8
 * Call that task after specified task runs.
252 8
 *
253
 * @param string $task The task after $that should be run.
254 8
 * @param string|callable $do The task to be run.
255
 *
256
 * @return ?Task
257
 */
258
function after(string $task, string|callable $do): ?Task
259
{
260
    if (is_closure($do)) {
261
        $newTask = task("after:$task", $do);
262
        after($task, "after:$task");
263 6
        return $newTask;
264 6
    }
265
    task($task)->addAfter($do);
0 ignored issues
show
Bug introduced by
It seems like $do can also be of type callable; however, parameter $task of Deployer\Task\Task::addAfter() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

265
    task($task)->addAfter(/** @scrutinizer ignore-type */ $do);
Loading history...
266
267
    return null;
268
}
269
270
/**
271
 * Setup which task run on failure of $task.
272
 * When called multiple times for a task, previous fail() definitions will be overridden.
273
 *
274
 * @param string $task The task which need to fail so $that should be run.
275
 * @param string|callable $do The task to be run.
276
 *
277
 * @return ?Task
278
 */
279
function fail(string $task, string|callable $do): ?Task
280
{
281
    if (is_callable($do)) {
282
        $newTask = task("fail:$task", $do);
283
        fail($task, "fail:$task");
284
        return $newTask;
285
    }
286
    $deployer = Deployer::get();
287
    $deployer->fail->set($task, $do);
288
289
    return null;
290
}
291
292
/**
293 8
 * Add users options.
294
 *
295 8
 * @param string $name The option name
296 8
 * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
297
 * @param int|null $mode The option mode: One of the VALUE_* constants
298 8
 * @param string $description A description text
299 6
 * @param string|string[]|int|bool|null $default The default value (must be null for self::VALUE_NONE)
300
 */
301
function option(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): void
302 8
{
303 8
    Deployer::get()->inputDefinition->addOption(
304
        new InputOption($name, $shortcut, $mode, $description, $default),
305
    );
306
}
307
308 8
/**
309 8
 * Change the current working directory.
310 8
 *
311
 * ```php
312
 * cd('~/myapp');
313
 * run('ls'); // Will run `ls` in ~/myapp.
314
 * ```
315
 */
316 8
function cd(string $path): void
317 8
{
318
    set('working_path', parse($path));
319 8
}
320
321
/**
322
 * Change the current user.
323
 *
324
 * Usage:
325
 * ```php
326
 * $restore = become('deployer');
327
 *
328
 * // do something
329
 *
330
 * $restore(); // revert back to the previous user
331
 * ```
332
 *
333
 * @param string $user
334
 * @return \Closure
335 8
 */
336
function become(string $user): \Closure
337
{
338
    $currentBecome = get('become');
339
    set('become', $user);
340
    return function () use ($currentBecome) {
341
        set('become', $currentBecome);
342
    };
343
}
344
345
/**
346
 * Execute a callback within a specific directory and revert back to the initial working directory.
347
 *
348
 * @return mixed Return value of the $callback function or null if callback doesn't return anything
349 6
 * @throws Exception
350 6
 */
351
function within(string $path, callable $callback): mixed
352 6
{
353 6
    $lastWorkingPath = get('working_path', '');
354 1
    try {
355 1
        set('working_path', parse($path));
356
        return $callback();
357
    } finally {
358 6
        set('working_path', $lastWorkingPath);
359
    }
360 6
}
361
362
/**
363
 * Executes given command on remote host.
364
 *
365
 * Examples:
366
 *
367
 * ```php
368
 * run('echo hello world');
369
 * run('cd {{deploy_path}} && git status');
370
 * run('password %secret%', secret: getenv('CI_SECRET'));
371
 * run('curl medv.io', timeout: 5);
372
 * ```
373
 *
374 8
 * ```php
375
 * $path = run('readlink {{deploy_path}}/current');
376
 * run("echo $path");
377
 * ```
378
 *
379
 * @param string $command Command to run on remote host.
380
 * @param string|null $cwd Sets the process working directory. If not set {{working_path}} will be used.
381
 * @param int|null $timeout Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec; see {{default_timeout}}, `null` to disable).
382
 * @param int|null $idleTimeout Sets the process idle timeout (max. time since last output) in seconds.
383
 * @param string|null $secret Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs.
384
 * @param array|null $env Array of environment variables: `run('echo $KEY', env: ['key' => 'value']);`
385
 * @param bool|null $forceOutput Print command output in real-time.
386
 * @param bool|null $nothrow Don't throw an exception of non-zero exit code.
387
 * @return string
388
 * @throws RunException
389
 * @throws TimeoutException
390
 * @throws WillAskUser
391
 */
392
function run(
393
    string  $command,
394
    ?string $cwd = null,
395
    ?array  $env = null,
396
    ?string $secret = null,
397
    ?bool   $nothrow = false,
398
    ?bool   $forceOutput = false,
399
    ?int    $timeout = null,
400
    ?int    $idleTimeout = null,
401
): string {
402
    $runParams = new RunParams(
403
        shell: currentHost()->getShell(),
404
        cwd: $cwd ?? has('working_path') ? get('working_path') : null,
405
        env: array_merge_alternate(get('env', []), $env ?? []),
406
        nothrow: $nothrow,
0 ignored issues
show
Bug introduced by
It seems like $nothrow can also be of type null; however, parameter $nothrow of Deployer\Ssh\RunParams::__construct() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

406
        /** @scrutinizer ignore-type */ nothrow: $nothrow,
Loading history...
407
        timeout: $timeout ?? get('default_timeout', 300),
408
        idleTimeout: $idleTimeout,
409
        forceOutput: $forceOutput,
0 ignored issues
show
Bug introduced by
It seems like $forceOutput can also be of type null; however, parameter $forceOutput of Deployer\Ssh\RunParams::__construct() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

409
        /** @scrutinizer ignore-type */ forceOutput: $forceOutput,
Loading history...
410
        secrets: empty($secret) ? null : ['secret' => $secret],
411
    );
412
413
    $dotenv = get('dotenv', false);
414
    if (!empty($dotenv)) {
415
        $runParams->dotenv = $dotenv;
416
    }
417
418
    $run = function (string $command, ?RunParams $params = null) use ($runParams): string {
419
        $params = $params ?? $runParams;
420
        $host = currentHost();
421
        $command = parse($command);
422
        if ($host instanceof Localhost) {
423
            $process = Deployer::get()->processRunner;
424
            $output = $process->run($host, $command, $params);
425
        } else {
426
            $client = Deployer::get()->sshClient;
427
            $output = $client->run($host, $command, $params);
428
        }
429
        return rtrim($output);
430
    };
431
432
    if (preg_match('/^sudo\b/', $command)) {
433
        try {
434
            return $run($command);
435
        } catch (RunException) {
436
            $askpass = get('sudo_askpass', '/tmp/dep_sudo_pass');
437
            $password = get('sudo_pass', false);
438
            if ($password === false) {
439
                writeln("<fg=green;options=bold>run</> $command");
440
                $password = askHiddenResponse(" [sudo] password for {{remote_user}}: ");
441
            }
442
            $run("echo -e '#!/bin/sh\necho \"\$PASSWORD\"' > $askpass");
443
            $run("chmod a+x $askpass");
444
            $command = preg_replace('/^sudo\b/', 'sudo -A', $command);
445
            $output = $run(" SUDO_ASKPASS=$askpass PASSWORD=%sudo_pass% $command", $runParams->with(
446
                secrets: ['sudo_pass' => escapeshellarg($password)],
447
            ));
448
            $run("rm $askpass");
449
            return $output;
450
        }
451
    } else {
452
        return $run($command);
453
    }
454
}
455
456
457
/**
458
 * Execute commands on a local machine.
459
 *
460
 * Examples:
461
 *
462
 * ```php
463
 * $user = runLocally('git config user.name');
464
 * runLocally("echo $user");
465
 * ```
466
 *
467
 * @param string $command Command to run on localhost.
468
 * @param string|null $cwd Sets the process working directory. If not set {{working_path}} will be used.
469
 * @param int|null $timeout Sets the process timeout (max. runtime). The timeout in seconds (default: 300 sec, `null` to disable).
470
 * @param int|null $idleTimeout Sets the process idle timeout (max. time since last output) in seconds.
471
 * @param string|null $secret Placeholder `%secret%` can be used in command. Placeholder will be replaced with this value and will not appear in any logs.
472
 * @param array|null $env Array of environment variables: `runLocally('echo $KEY', env: ['key' => 'value']);`
473
 * @param bool|null $forceOutput Print command output in real-time.
474
 * @param bool|null $nothrow Don't throw an exception of non-zero exit code.
475
 * @param string|null $shell Shell to run in. Default is `bash -s`.
476
 *
477
 * @return string
478
 * @throws RunException
479 4
 * @throws TimeoutException
480 4
 */
481
function runLocally(
482
    string  $command,
483
    ?string $cwd = null,
484
    ?int    $timeout = null,
485
    ?int    $idleTimeout = null,
486
    ?string $secret = null,
487
    ?array  $env = null,
488
    ?bool   $forceOutput = false,
489
    ?bool   $nothrow = false,
490
    ?string $shell = null,
491
): string {
492
    $runParams = new RunParams(
493
        shell: $shell ?? 'bash -s',
494
        cwd: $cwd,
495
        env: $env,
496
        nothrow: $nothrow,
0 ignored issues
show
Bug introduced by
It seems like $nothrow can also be of type null; however, parameter $nothrow of Deployer\Ssh\RunParams::__construct() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

496
        /** @scrutinizer ignore-type */ nothrow: $nothrow,
Loading history...
497
        timeout: $timeout,
498 7
        idleTimeout: $idleTimeout,
499 7
        forceOutput: $forceOutput,
0 ignored issues
show
Bug introduced by
It seems like $forceOutput can also be of type null; however, parameter $forceOutput of Deployer\Ssh\RunParams::__construct() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

499
        /** @scrutinizer ignore-type */ forceOutput: $forceOutput,
Loading history...
500 7
        secrets: empty($secret) ? null : ['secret' => $secret],
501
    );
502
503
    $process = Deployer::get()->processRunner;
504
    $command = parse($command);
505
506
    $output = $process->run(new Localhost(), $command, $runParams);
507
    return rtrim($output);
508
}
509
510
/**
511
 * Run test command.
512
 * Example:
513
 *
514
 * ```php
515
 * if (test('[ -d {{release_path}} ]')) {
516
 * ...
517
 * }
518
 * ```
519
 *
520 10
 */
521
function test(string $command): bool
522
{
523
    $true = '+' . array_rand(array_flip(['accurate', 'appropriate', 'correct', 'legitimate', 'precise', 'right', 'true', 'yes', 'indeed']));
0 ignored issues
show
Bug introduced by
Are you sure array_rand(array_flip(ar...ue', 'yes', 'indeed'))) of type array|integer|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

523
    $true = '+' . /** @scrutinizer ignore-type */ array_rand(array_flip(['accurate', 'appropriate', 'correct', 'legitimate', 'precise', 'right', 'true', 'yes', 'indeed']));
Loading history...
524
    return trim(run("if $command; then echo $true; fi")) === $true;
525
}
526
527
/**
528
 * Run test command locally.
529
 * Example:
530
 *
531 12
 *     testLocally('[ -d {{local_release_path}} ]')
532 12
 *
533
 */
534 6
function testLocally(string $command): bool
535
{
536 12
    return runLocally("if $command; then echo +true; fi") === '+true';
537
}
538
539
/**
540
 * Iterate other hosts, allowing to call run a func in callback.
541
 *
542
 * ```php
543
 * on(select('stage=prod, role=db'), function ($host) {
544
 *     ...
545
 * });
546
 * ```
547
 *
548
 * ```php
549
 * on(host('example.org'), function ($host) {
550
 *     ...
551
 * });
552
 * ```
553
 *
554
 * ```php
555
 * on(Deployer::get()->hosts, function ($host) {
556
 *     ...
557
 * });
558
 * ```
559
 *
560
 * @param Host|Host[] $hosts
561
 */
562 10
function on($hosts, callable $callback): void
563
{
564
    if (!is_array($hosts) && !($hosts instanceof \Traversable)) {
565 10
        $hosts = [$hosts];
566
    }
567
568
    foreach ($hosts as $host) {
569
        if ($host instanceof Host) {
570
            $host->config()->load();
571
            Context::push(new Context($host));
572
            try {
573
                $callback($host);
574
                $host->config()->save();
575
            } catch (GracefulShutdownException $e) {
576
                Deployer::get()->messenger->renderException($e, $host);
577 4
            } finally {
578
                Context::pop();
579
            }
580 4
        } else {
581
            throw new \InvalidArgumentException("Function on can iterate only on Host instances.");
582
        }
583
    }
584
}
585
586
/**
587
 * Runs a task.
588
 * ```php
589
 * invoke('deploy:symlink');
590
 * ```
591
 *
592 1
 * @throws Exception
593
 */
594 1
function invoke(string $taskName): void
595
{
596
    $task = Deployer::get()->tasks->get($taskName);
597
    Deployer::get()->messenger->startTask($task);
598 1
    $task->run(Context::get());
599
    Deployer::get()->messenger->endTask($task);
600
}
601
602
/**
603 1
 * Upload files or directories to host.
604
 *
605 1
 * > To upload the _contents_ of a directory, include a trailing slash (eg `upload('build/', '{{release_path}}/public');`).
606 1
 * > Without the trailing slash, the build directory itself will be uploaded (resulting in `{{release_path}}/public/build`).
607
 *
608 1
 *  The `$config` array supports the following keys:
609 1
 *
610
 * - `flags` for overriding the default `-azP` passed to the `rsync` command
611
 * - `options` with additional flags passed directly to the `rsync` command
612
 * - `timeout` for `Process::fromShellCommandline()` (`null` by default)
613 1
 * - `progress_bar` to display upload/download progress
614
 * - `display_stats` to display rsync set of statistics
615
 *
616
 * Note: due to the way php escapes command line arguments, list-notation for the rsync `--exclude={'file','anotherfile'}` option will not work.
617
 * A workaround is to add a separate `--exclude=file` argument for each exclude to `options` (also, _do not_ wrap the filename/filter in quotes).
618
 * An alternative might be to write the excludes to a temporary file (one per line) and use `--exclude-from=temporary_file` argument instead.
619
 *
620
 * @param string|string[] $source
621
 * @param array $config
622
 * @phpstan-param array{flags?: string, options?: array, timeout?: int|null, progress_bar?: bool, display_stats?: bool} $config
623
 *
624
 * @throws RunException
625
 */
626
function upload($source, string $destination, array $config = []): void
627
{
628
    $rsync = Deployer::get()->rsync;
629
    $host = currentHost();
630
    $source = is_array($source) ? array_map('Deployer\parse', $source) : parse($source);
631
    $destination = parse($destination);
632
633
    if ($host instanceof Localhost) {
634
        $rsync->call($host, $source, $destination, $config);
635
    } else {
636
        $rsync->call($host, $source, "{$host->connectionString()}:$destination", $config);
637
    }
638
}
639
640
/**
641
 * Download file or directory from host
642
 *
643
 * @param array $config
644
 *
645
 * @throws RunException
646
 */
647
function download(string $source, string $destination, array $config = []): void
648
{
649
    $rsync = Deployer::get()->rsync;
650
    $host = currentHost();
651
    $source = parse($source);
652
    $destination = parse($destination);
653
654
    if ($host instanceof Localhost) {
655
        $rsync->call($host, $source, $destination, $config);
656
    } else {
657
        $rsync->call($host, "{$host->connectionString()}:$source", $destination, $config);
658
    }
659
}
660
661
/**
662
 * Writes an info message.
663
 */
664
function info(string $message): void
665
{
666
    writeln("<fg=green;options=bold>info</> " . parse($message));
667
}
668
669
/**
670
 * Writes an warning message.
671
 */
672
function warning(string $message): void
673
{
674
    $message = "<fg=yellow;options=bold>warning</> <comment>$message</comment>";
675
676
    if (Context::has()) {
677
        writeln($message);
678
    } else {
679
        Deployer::get()->output->writeln($message);
680
    }
681
}
682
683
/**
684
 * Writes a message to the output and adds a newline at the end.
685
 */
686
function writeln(string $message, int $options = 0): void
687
{
688
    $host = currentHost();
689
    output()->writeln("[$host] " . parse($message), $options);
690
}
691
692
/**
693
 * Parse set values.
694
 */
695
function parse(string $value): string
696
{
697
    return Context::get()->getConfig()->parse($value);
698
}
699
700
/**
701
 * Setup configuration option.
702
 * @param mixed $value
703
 * @throws Exception
704
 */
705
function set(string $name, $value): void
706
{
707
    if (!Context::has()) {
708
        Deployer::get()->config->set($name, $value);
709
    } else {
710
        Context::get()->getConfig()->set($name, $value);
711
    }
712
}
713
714
/**
715
 * Merge new config params to existing config array.
716
 *
717
 * @param array $array
718 5
 */
719
function add(string $name, array $array): void
720
{
721
    if (!Context::has()) {
722
        Deployer::get()->config->add($name, $array);
723
    } else {
724
        Context::get()->getConfig()->add($name, $array);
725
    }
726
}
727 8
728
/**
729
 * Get configuration value.
730
 *
731
 * @param mixed|null $default
732
 *
733
 * @return mixed
734
 */
735
function get(string $name, $default = null)
736
{
737
    if (!Context::has()) {
738 3
        return Deployer::get()->config->get($name, $default);
739
    } else {
740
        return Context::get()->getConfig()->get($name, $default);
741
    }
742
}
743 5
744 5
/**
745
 * Check if there is such configuration option.
746
 */
747 5
function has(string $name): bool
748
{
749
    if (!Context::has()) {
750
        return Deployer::get()->config->has($name);
751
    } else {
752 4
        return Context::get()->getConfig()->has($name);
753
    }
754
}
755
756
function ask(string $message, ?string $default = null, ?array $autocomplete = null): ?string
757 4
{
758 4
    if (defined('DEPLOYER_NO_ASK')) {
759
        throw new WillAskUser($message);
760
    }
761
    Context::required(__FUNCTION__);
762
763 4
    if (output()->isQuiet()) {
764
        return $default;
765
    }
766
767
    if (Deployer::isWorker()) {
768
        return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args());
769
    }
770
771
    /** @var QuestionHelper */
772
    $helper = Deployer::get()->getHelper('question');
773
774
    $tag = currentHost()->getTag();
775
    $message = parse($message);
776
    $message = "[$tag] <question>$message</question> " . (($default === null) ? "" : "(default: $default) ");
777
778
    $question = new Question($message, $default);
779
    if (!empty($autocomplete)) {
780
        $question->setAutocompleterValues($autocomplete);
781
    }
782
783
    return $helper->ask(input(), output(), $question);
784
}
785
786
/**
787
 * @param mixed $default
788
 * @return mixed
789
 * @throws Exception
790
 */
791
function askChoice(string $message, array $availableChoices, $default = null, bool $multiselect = false)
792
{
793
    if (defined('DEPLOYER_NO_ASK')) {
794
        throw new WillAskUser($message);
795
    }
796
    Context::required(__FUNCTION__);
797
798
    if (empty($availableChoices)) {
799
        throw new \InvalidArgumentException('Available choices should not be empty');
800
    }
801
802
    if ($default !== null && !array_key_exists($default, $availableChoices)) {
803
        throw new \InvalidArgumentException('Default choice is not available');
804
    }
805
806
    if (output()->isQuiet()) {
807
        if ($default === null) {
808
            $default = key($availableChoices);
809
        }
810
        return [$default => $availableChoices[$default]];
811
    }
812
813
    if (Deployer::isWorker()) {
814
        return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args());
815
    }
816
817
    /** @var QuestionHelper */
818
    $helper = Deployer::get()->getHelper('question');
819
820
    $tag = currentHost()->getTag();
821
    $message = parse($message);
822
    $message = "[$tag] <question>$message</question> " . (($default === null) ? "" : "(default: $default) ");
823
824
    $question = new ChoiceQuestion($message, $availableChoices, $default);
825
    $question->setMultiselect($multiselect);
826
827
    return $helper->ask(input(), output(), $question);
828
}
829
830
function askConfirmation(string $message, bool $default = false): bool
831
{
832
    if (defined('DEPLOYER_NO_ASK')) {
833
        throw new WillAskUser($message);
834
    }
835
    Context::required(__FUNCTION__);
836
837
    if (output()->isQuiet()) {
838
        return $default;
839
    }
840
841
    if (Deployer::isWorker()) {
842
        return Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args());
843
    }
844
845
    /** @var QuestionHelper */
846
    $helper = Deployer::get()->getHelper('question');
847
848
    $yesOrNo = $default ? 'Y/n' : 'y/N';
849
    $tag = currentHost()->getTag();
850
    $message = parse($message);
851
    $message = "[$tag] <question>$message</question> [$yesOrNo] ";
852
853
    $question = new ConfirmationQuestion($message, $default);
854
855
    return $helper->ask(input(), output(), $question);
856
}
857
858
function askHiddenResponse(string $message): string
859
{
860
    if (defined('DEPLOYER_NO_ASK')) {
861
        throw new WillAskUser($message);
862
    }
863
    Context::required(__FUNCTION__);
864
865
    if (output()->isQuiet()) {
866
        return '';
867
    }
868
869
    if (Deployer::isWorker()) {
870
        return (string) Deployer::masterCall(currentHost(), __FUNCTION__, ...func_get_args());
871
    }
872
873
    /** @var QuestionHelper */
874
    $helper = Deployer::get()->getHelper('question');
875
876
    $tag = currentHost()->getTag();
877
    $message = parse($message);
878
    $message = "[$tag] <question>$message</question> ";
879
880
    $question = new Question($message);
881
    $question->setHidden(true);
882
    $question->setHiddenFallback(false);
883
884
    return (string) $helper->ask(input(), output(), $question);
885
}
886
887
function input(): InputInterface
888
{
889
    return Deployer::get()->input;
890
}
891
892
function output(): OutputInterface
893
{
894
    return Deployer::get()->output;
895
}
896
897
/**
898
 * Check if command exists
899
 *
900
 * @throws RunException
901
 */
902
function commandExist(string $command): bool
903
{
904
    return test("hash $command 2>/dev/null");
905
}
906
907
/**
908
 * @throws RunException
909
 */
910
function commandSupportsOption(string $command, string $option): bool
911
{
912
    $man = run("(man $command 2>&1 || $command -h 2>&1 || $command --help 2>&1) | grep -- $option || true");
913
    if (empty($man)) {
914
        return false;
915
    }
916
    return str_contains($man, $option);
917
}
918
919
/**
920
 * @throws RunException
921
 */
922
function which(string $name): string
923
{
924
    $nameEscaped = escapeshellarg($name);
925
926
    // Try `command`, should cover all Bourne-like shells
927
    // Try `which`, should cover most other cases
928
    // Fallback to `type` command, if the rest fails
929
    $path = run("command -v $nameEscaped || which $nameEscaped || type -p $nameEscaped");
930
    if (empty($path)) {
931
        throw new \RuntimeException("Can't locate [$nameEscaped] - neither of [command|which|type] commands are available");
932
    }
933
934
    // Deal with issue when `type -p` outputs something like `type -ap` in some implementations
935
    return trim(str_replace("$name is", "", $path));
936
937
}
938
939
/**
940
 * Returns remote environments variables as an array.
941
 * ```php
942
 * $remotePath = remoteEnv()['PATH'];
943
 * run('echo $PATH', env: ['PATH' => "/home/user/bin:$remotePath"]);
944
 * ```
945
 */
946
function remoteEnv(): array
947
{
948
    $vars = [];
949
    $data = run('env');
950
    foreach (explode("\n", $data) as $line) {
951
        [$name, $value] = explode('=', $line, 2);
952
        $vars[$name] = $value;
953
    }
954
    return $vars;
955
}
956
957
/**
958
 * Creates a new exception.
959
 */
960
function error(string $message): Exception
961
{
962
    return new Exception(parse($message));
963
}
964
965
/**
966
 * Returns current timestamp in UTC timezone in ISO8601 format.
967
 */
968
function timestamp(): string
969
{
970
    return (new \DateTime('now', new \DateTimeZone('UTC')))->format(\DateTime::ISO8601);
971
}
972
973
/**
974
 * Example usage:
975
 * ```php
976
 * $result = fetch('{{domain}}', info: $info);
977
 * var_dump($info['http_code'], $result);
978
 * ```
979
 */
980
function fetch(string $url, string $method = 'get', array $headers = [], ?string $body = null, ?array &$info = null, bool $nothrow = false): string
981
{
982
    $url = parse($url);
983
    if (strtolower($method) === 'get') {
984
        $http = Httpie::get($url);
985
    } elseif (strtolower($method) === 'post') {
986
        $http = Httpie::post($url);
987
    } else {
988
        throw new \InvalidArgumentException("Unknown method \"$method\".");
989
    }
990
    $http = $http->nothrow($nothrow);
991
    foreach ($headers as $key => $value) {
992
        $http = $http->header($key, $value);
993
    }
994
    if ($body !== null) {
995
        $http = $http->body($body);
996
    }
997
    return $http->send($info);
998
}
999